From a6c4b6431b1f0e5817e78f4cb546061de473fbd3 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 19 Feb 2022 20:16:25 +0100 Subject: [PATCH] Re-add permission banners to search results --- .../launcher2/ui/launcher/search/SearchVM.kt | 69 +++++++++++++++++++ .../search/calendar/CalendarResults.kt | 49 ++++++++++--- .../search/contacts/ContactResults.kt | 46 +++++++++++-- .../ui/launcher/search/files/FileResults.kt | 35 +++++++++- 4 files changed, 182 insertions(+), 17 deletions(-) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 8dc4f9b2..a7c46782 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.launcher.search +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData @@ -10,6 +11,9 @@ import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.contacts.ContactRepository import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.files.FileRepository +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.WebsearchRepository import de.mm20.launcher2.search.data.* import de.mm20.launcher2.unitconverter.UnitConverterRepository @@ -17,12 +21,17 @@ import de.mm20.launcher2.websites.WebsiteRepository import de.mm20.launcher2.wikipedia.WikipediaRepository import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject class SearchVM : ViewModel(), KoinComponent { private val favoritesRepository: FavoritesRepository by inject() + private val permissionsManager: PermissionsManager by inject() + private val dataStore: LauncherDataStore by inject() private val calendarRepository: CalendarRepository by inject() private val contactRepository: ContactRepository by inject() @@ -36,6 +45,7 @@ class SearchVM : ViewModel(), KoinComponent { val isSearching = MutableLiveData(false) val searchQuery = MutableLiveData("") + val isSearchEmpty = MutableLiveData(true) val favorites by lazy { favoritesRepository.getFavorites().asLiveData() @@ -60,6 +70,7 @@ class SearchVM : ViewModel(), KoinComponent { var searchJob: Job? = null fun search(query: String) { searchQuery.value = query + isSearchEmpty.value = query.isEmpty() try { searchJob?.cancel() } catch (e: CancellationException) { @@ -118,4 +129,62 @@ class SearchVM : ViewModel(), KoinComponent { } } + val missingCalendarPermission = combine( + permissionsManager.hasPermission(PermissionGroup.Calendar), + dataStore.data.map { it.calendarSearch.enabled }.distinctUntilChanged() + ) { perm, enabled -> !perm && enabled } + + fun requestCalendarPermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.Calendar) + } + + fun disableCalendarSearch() { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setCalendarSearch(it.calendarSearch.toBuilder().setEnabled(false)) + .build() + } + } + } + + + val missingContactsPermission = combine( + permissionsManager.hasPermission(PermissionGroup.Contacts), + dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged() + ) { perm, enabled -> !perm && enabled } + + fun requestContactsPermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.Contacts) + } + + fun disableContactsSearch() { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setContactsSearch(it.contactsSearch.toBuilder().setEnabled(false)) + .build() + } + } + } + + val missingFilesPermission = combine( + permissionsManager.hasPermission(PermissionGroup.ExternalStorage), + dataStore.data.map { it.fileSearch.localFiles }.distinctUntilChanged() + ) { perm, enabled -> !perm && enabled } + + fun requestFilesPermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.ExternalStorage) + } + + fun disableFilesSearch() { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder() + .setFileSearch(it.fileSearch.toBuilder().setLocalFiles(false)) + .build() + } + } + } + } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt index d48c385f..b3e44079 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt @@ -1,16 +1,26 @@ package de.mm20.launcher2.ui.launcher.search.calendar +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList @@ -18,19 +28,42 @@ import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList fun ColumnScope.CalendarResults() { val viewModel: SearchVM = viewModel() val calendarEvents by viewModel.calendarResults.observeAsState(emptyList()) + val context = LocalContext.current - AnimatedVisibility(calendarEvents.isNotEmpty()) { + val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true) + val missingPermission by viewModel.missingCalendarPermission.collectAsState(false) + AnimatedVisibility(calendarEvents.isNotEmpty() || (!isSearchEmpty && missingPermission)) { LauncherCard( modifier = Modifier .padding(bottom = 8.dp) .fillMaxWidth() ) { - SearchResultList( - items = calendarEvents, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - ) + Column { + AnimatedVisibility(!isSearchEmpty && missingPermission) { + MissingPermissionBanner( + text = stringResource(R.string.missing_permission_calendar_search), + onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) }, + modifier = Modifier.padding(16.dp), + secondaryAction = { + TextButton(onClick = { + viewModel.disableCalendarSearch() + }) { + Text( + stringResource(R.string.turn_off), + style = MaterialTheme.typography.labelLarge + ) + } + } + ) + } + + SearchResultList( + items = calendarEvents, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) + } } } -} \ No newline at end of file +} diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt index ed640c8e..ce7774d0 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt @@ -1,36 +1,68 @@ package de.mm20.launcher2.ui.launcher.search.contacts +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList @Composable fun ColumnScope.ContactResults() { val viewModel: SearchVM = viewModel() + val context = LocalContext.current val contacts by viewModel.contactResults.observeAsState(emptyList()) - AnimatedVisibility(contacts.isNotEmpty()) { + val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true) + val missingPermission by viewModel.missingContactsPermission.collectAsState(false) + AnimatedVisibility(contacts.isNotEmpty() || (!isSearchEmpty && missingPermission)) { LauncherCard( modifier = Modifier .padding(bottom = 8.dp) .fillMaxWidth() ) { - SearchResultList( - items = contacts, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - ) + Column { + AnimatedVisibility(!isSearchEmpty && missingPermission) { + MissingPermissionBanner( + text = stringResource(R.string.missing_permission_contact_search), + onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) }, + modifier = Modifier.padding(16.dp), + secondaryAction = { + TextButton(onClick = { + viewModel.disableContactsSearch() + }) { + Text( + stringResource(R.string.turn_off), + style = MaterialTheme.typography.labelLarge + ) + } + } + ) + } + SearchResultList( + items = contacts, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) + } } } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt index 67ac7486..57dff5a4 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt @@ -1,16 +1,26 @@ package de.mm20.launcher2.ui.launcher.search.files +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList @@ -18,19 +28,40 @@ import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList fun ColumnScope.FileResults() { val viewModel: SearchVM = viewModel() val files by viewModel.fileResults.observeAsState(emptyList()) + val context = LocalContext.current - AnimatedVisibility(files.isNotEmpty()) { + val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true) + val missingPermission by viewModel.missingFilesPermission.collectAsState(false) + AnimatedVisibility(files.isNotEmpty() || (!isSearchEmpty && missingPermission)) { LauncherCard( modifier = Modifier .padding(bottom = 8.dp) .fillMaxWidth() ) { + Column { + AnimatedVisibility(!isSearchEmpty && missingPermission) { + MissingPermissionBanner( + text = stringResource(R.string.missing_permission_files_search), + onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) }, + modifier = Modifier.padding(16.dp), + secondaryAction = { + TextButton(onClick = { + viewModel.disableFilesSearch() + }) { + Text( + stringResource(R.string.turn_off), + style = MaterialTheme.typography.labelLarge + ) + } + } + ) + } SearchResultList( items = files, modifier = Modifier .fillMaxWidth() .padding(12.dp) - ) + )} } } } \ No newline at end of file