Fix permission banners in search results

This commit is contained in:
MM20 2022-01-26 21:51:35 +01:00
parent 7a48a148ff
commit b8304f1c1c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 270 additions and 117 deletions

View File

@ -477,4 +477,5 @@
<string name="no_account_microsoft">Sie haben noch kein Microsoft-Konto verbunden</string> <string name="no_account_microsoft">Sie haben noch kein Microsoft-Konto verbunden</string>
<string name="no_account_google">Sie haben noch kein Google-Konto verbunden</string> <string name="no_account_google">Sie haben noch kein Google-Konto verbunden</string>
<string name="connect_account">Konto verbinden</string> <string name="connect_account">Konto verbinden</string>
<string name="turn_off">Deaktivieren</string>
</resources> </resources>

View File

@ -516,4 +516,6 @@
<string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string> <string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string>
<string name="no_account_google">You haven\'t connected a Google account yet</string> <string name="no_account_google">You haven\'t connected a Google account yet</string>
<string name="connect_account">Connect account</string> <string name="connect_account">Connect account</string>
<string name="turn_off">Turn off</string>
</resources> </resources>

View File

@ -7,7 +7,12 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.R import de.mm20.launcher2.permissions.R
class MissingPermission(override val label: String, val permissionGroup: PermissionGroup): Searchable() { class MissingPermission(
override val label: String,
val permissionGroup: PermissionGroup,
val secondaryActionLabel: String? = null,
val secondaryAction: (() -> Unit)? = null
): Searchable() {
override val key: String override val key: String
get() = "permission://${permissionGroup.ordinal}" get() = "permission://${permissionGroup.ordinal}"

View File

@ -8,15 +8,19 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@ -30,40 +34,84 @@ class CalendarView : FrameLayout, KoinComponent {
defStyleRes defStyleRes
) )
private val calendarEvents: LiveData<List<CalendarEvent>?>
init { init {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
val dataStore: LauncherDataStore = get()
View.inflate(context, R.layout.view_search_category_list, this) View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition() layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
val viewModel: SearchVM by (context as AppCompatActivity).viewModels() val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
calendarEvents = viewModel.calendarResults
calendarEvents.observe(context as AppCompatActivity, { val showMissingPermissionBanner = combine(
if (it == null) { dataStore.data.map { it.calendarSearch.enabled },
visibility = View.GONE permissionsManager.hasPermission(PermissionGroup.Calendar)
return@observe ) { calendarSearchEnabled, hasPermission ->
!hasPermission && calendarSearchEnabled
}.asLiveData()
val searchQuery = viewModel.searchQuery
val calendarResults = viewModel.calendarResults
val show = MediatorLiveData<Boolean>()
show.addSource(showMissingPermissionBanner) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !calendarResults.value.isNullOrEmpty())
}
show.addSource(calendarResults) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !calendarResults.value.isNullOrEmpty())
}
show.addSource(searchQuery) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !calendarResults.value.isNullOrEmpty())
}
show.observe(context as AppCompatActivity) {
visibility = if (it) {
View.VISIBLE
} else {
View.GONE
} }
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !permissionsManager.checkPermissionOnce( }
PermissionGroup.Calendar
) val list = findViewById<SearchListView>(R.id.list)
) { calendarResults.observe(context as AppCompatActivity) {
visibility = View.VISIBLE if (showMissingPermissionBanner.value == true) {
list.submitItems( list.submitItems(listOf(
listOf( MissingPermission(
MissingPermission( context.getString(R.string.permission_calendar_search),
context.getString(R.string.permission_calendar_search), PermissionGroup.Calendar,
PermissionGroup.Calendar secondaryActionLabel = context.getString(R.string.turn_off),
) secondaryAction = {
lifecycleScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarSearch(it.calendarSearch.toBuilder().setEnabled(false))
.build()
}
}
}
) )
) ) + it)
return@observe } else {
list.submitItems(it)
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE }
list.submitItems(it)
}) showMissingPermissionBanner.observe(context as AppCompatActivity) {
if (it == true) {
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_calendar_search),
PermissionGroup.Calendar,
secondaryActionLabel = context.getString(R.string.turn_off)
)
) + calendarResults.value!!)
} else {
list.submitItems(calendarResults.value)
}
}
} }
} }

View File

@ -8,20 +8,23 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class ContactView : FrameLayout, KoinComponent { class ContactView : FrameLayout, KoinComponent {
private val contacts: LiveData<List<Contact>?>
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@ -33,36 +36,82 @@ class ContactView : FrameLayout, KoinComponent {
init { init {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
val dataStore: LauncherDataStore = get()
View.inflate(context, R.layout.view_search_category_list, this) View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition() layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val viewModel: SearchVM by (context as AppCompatActivity).viewModels() val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
contacts = viewModel.contactResults
val showMissingPermissionBanner = combine(
dataStore.data.map { it.contactsSearch.enabled },
permissionsManager.hasPermission(PermissionGroup.Contacts)
) { contactSearchEnabled, hasPermission ->
!hasPermission && contactSearchEnabled
}.asLiveData()
val searchQuery = viewModel.searchQuery
val contactResults = viewModel.contactResults
val show = MediatorLiveData<Boolean>()
show.addSource(showMissingPermissionBanner) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !contactResults.value.isNullOrEmpty())
}
show.addSource(contactResults) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !contactResults.value.isNullOrEmpty())
}
show.addSource(searchQuery) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !contactResults.value.isNullOrEmpty())
}
show.observe(context as AppCompatActivity) {
visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
contacts.observe(context as AppCompatActivity, { contactResults.observe(context as AppCompatActivity) {
if (it == null) { if (showMissingPermissionBanner.value == true) {
visibility = View.GONE list.submitItems(listOf(
return@observe MissingPermission(
} context.getString(R.string.permission_contact_search),
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !permissionsManager.checkPermissionOnce( PermissionGroup.Contacts,
PermissionGroup.Contacts secondaryActionLabel = context.getString(R.string.turn_off),
) secondaryAction = {
) { lifecycleScope.launch {
visibility = View.VISIBLE dataStore.updateData {
list.submitItems( it.toBuilder()
listOf( .setContactsSearch(it.contactsSearch.toBuilder().setEnabled(false))
MissingPermission( .build()
context.getString(R.string.permission_contact_search), }
PermissionGroup.Contacts }
) }
) )
) ) + it)
return@observe } else {
list.submitItems(it)
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE }
list.submitItems(it)
}) showMissingPermissionBanner.observe(context as AppCompatActivity) {
if (it == true) {
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_contact_search),
PermissionGroup.Contacts,
secondaryActionLabel = context.getString(R.string.turn_off)
)
) + contactResults.value!!)
} else {
list.submitItems(contactResults.value)
}
}
} }
} }

View File

@ -9,18 +9,24 @@ import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class FileView : FrameLayout, KoinComponent { class FileView : FrameLayout, KoinComponent {
private val files: LiveData<List<File>?>
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@ -32,36 +38,82 @@ class FileView : FrameLayout, KoinComponent {
init { init {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
val dataStore: LauncherDataStore = get()
View.inflate(context, R.layout.view_search_category_list, this) View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition() layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
val viewModel: SearchVM by (context as AppCompatActivity).viewModels() val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
files = viewModel.fileResults
files.observe(context as AppCompatActivity, { val showMissingPermissionBanner = combine(
if (it == null) { dataStore.data.map { it.fileSearch.localFiles },
visibility = View.GONE permissionsManager.hasPermission(PermissionGroup.ExternalStorage)
return@observe ) { localFileSearchEnabled, hasPermission ->
!hasPermission && localFileSearchEnabled
}.asLiveData()
val searchQuery = viewModel.searchQuery
val fileResults = viewModel.fileResults
val show = MediatorLiveData<Boolean>()
show.addSource(showMissingPermissionBanner) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !fileResults.value.isNullOrEmpty())
}
show.addSource(fileResults) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !fileResults.value.isNullOrEmpty())
}
show.addSource(searchQuery) {
show.value = !searchQuery.value.isNullOrBlank() &&
(showMissingPermissionBanner.value == true || !fileResults.value.isNullOrEmpty())
}
show.observe(context as AppCompatActivity) {
visibility = if (it) {
View.VISIBLE
} else {
View.GONE
} }
if (it.isEmpty() && !permissionsManager.checkPermissionOnce( }
PermissionGroup.ExternalStorage
) val list = findViewById<SearchListView>(R.id.list)
) { fileResults.observe(context as AppCompatActivity) {
visibility = View.VISIBLE if (showMissingPermissionBanner.value == true) {
list.submitItems( list.submitItems(listOf(
listOf( MissingPermission(
MissingPermission( context.getString(R.string.permission_files_search),
context.getString(R.string.permission_files_search), PermissionGroup.ExternalStorage,
PermissionGroup.ExternalStorage secondaryActionLabel = context.getString(R.string.turn_off),
) secondaryAction = {
lifecycleScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(it.fileSearch.toBuilder().setLocalFiles(false))
.build()
}
}
}
) )
) ) + it)
return@observe } else {
list.submitItems(it)
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE }
list.submitItems(it)
}) showMissingPermissionBanner.observe(context as AppCompatActivity) {
if (it == true) {
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_files_search),
PermissionGroup.ExternalStorage,
secondaryActionLabel = context.getString(R.string.turn_off)
)
) + fileResults.value!!)
} else {
list.submitItems(fileResults.value)
}
}
} }
} }

View File

@ -1,30 +1,53 @@
package de.mm20.launcher2.ui.legacy.search package de.mm20.launcher2.ui.legacy.search
import android.app.Activity
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.ui.platform.ComposeView
import androidx.transition.Scene import androidx.transition.Scene
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.legacy.searchable.SearchableView import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.InnerCardView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class PermissionListRepresentation : Representation, KoinComponent { class PermissionListRepresentation : Representation, KoinComponent {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene { override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val missingPermission = searchable as MissingPermission val missingPermission = searchable as MissingPermission
val context = rootView.context val context = rootView.context
val scene = Scene.getSceneForLayout(rootView, R.layout.view_permission_list, rootView.context) val scene =
Scene.getSceneForLayout(rootView, R.layout.view_permission_list, rootView.context)
scene.setEnterAction { scene.setEnterAction {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
rootView.findViewById<TextView>(R.id.permissionText).text = missingPermission.label rootView.findViewById<ComposeView>(R.id.composeView).setContent {
rootView.findViewById<LauncherIconView>(R.id.permissionIcon).icon = missingPermission.getPlaceholderIcon(context) LegacyLauncherTheme {
rootView.findViewById<InnerCardView>(R.id.card).setOnClickListener { MissingPermissionBanner(
permissionsManager.requestPermission(context as AppCompatActivity, missingPermission.permissionGroup) text = missingPermission.label,
onClick = {
permissionsManager.requestPermission(
context as AppCompatActivity,
missingPermission.permissionGroup
)
},
secondaryAction = {
val secondaryAction = missingPermission.secondaryAction
val secondaryActionLabel = missingPermission.secondaryActionLabel
if (secondaryAction != null && secondaryActionLabel != null)
TextButton(onClick = secondaryAction) {
Text(text = secondaryActionLabel, style = MaterialTheme.typography.labelLarge)
}
}
)
}
} }
} }
return scene return scene

View File

@ -1,37 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<de.mm20.launcher2.ui.legacy.view.InnerCardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/composeView"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:foreground="?selectableItemBackground" android:transitionName="root" />
android:transitionName="root">
<TextView
android:id="@+id/permissionText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="12dp"
android:layout_marginStart="56dp"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBottom_toTopOf="@+id/guideline4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/calendarColor"
android:layout_gravity="center_vertical"
tools:text="Information" />
<de.mm20.launcher2.ui.legacy.view.LauncherIconView
android:id="@+id/permissionIcon"
android:elevation="1dp"
android:layout_gravity="center_vertical"
android:layout_width="48dp"
android:layout_margin="8dp"
android:layout_height="48dp" />
</de.mm20.launcher2.ui.legacy.view.InnerCardView>