diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsFragment.kt index c440b282702..f9d34ac7cd6 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsFragment.kt @@ -2,6 +2,7 @@ package io.homeassistant.companion.android.settings import android.annotation.SuppressLint import android.app.UiModeManager +import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration @@ -107,6 +108,29 @@ class SettingsFragment( } } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + presenter.getSuggestionFlow().collect { suggestion -> + findPreference("settings_suggestion")?.let { + if (suggestion != null) { + it.setTitle(suggestion.title) + it.setSummary(suggestion.summary) + it.setIcon(suggestion.icon) + it.setOnPreferenceClickListener { + when (suggestion.id) { + SettingsPresenter.SUGGESTION_ASSISTANT_APP -> updateAssistantApp() + SettingsPresenter.SUGGESTION_NOTIFICATION_PERMISSION -> openNotificationSettings() + } + return@setOnPreferenceClickListener true + } + it.setOnPreferenceCancelListener { presenter.cancelSuggestion(requireContext(), suggestion.id) } + } + it.isVisible = suggestion != null + } + } + } + } + findPreference("server_add")?.let { it.setOnPreferenceClickListener { requestOnboardingResult.launch( @@ -320,6 +344,14 @@ class SettingsFragment( } } + private fun updateAssistantApp() { + // On Android Q+, this is a workaround as Android doesn't allow requesting the assistant role + val openIntent = Intent(Intent.ACTION_MAIN) + openIntent.component = ComponentName("com.android.settings", "com.android.settings.Settings\$ManageAssistActivity") + openIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(openIntent) + } + private fun updateBackgroundAccessPref() { findPreference("background")?.let { if (isIgnoringBatteryOptimizations()) { @@ -482,6 +514,7 @@ class SettingsFragment( override fun onResume() { super.onResume() activity?.title = getString(commonR.string.companion_app) + context?.let { presenter.updateSuggestions(it) } } override fun onDestroy() { diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsHomeSuggestion.kt b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsHomeSuggestion.kt new file mode 100644 index 00000000000..59f4c73f6a6 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsHomeSuggestion.kt @@ -0,0 +1,11 @@ +package io.homeassistant.companion.android.settings + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +data class SettingsHomeSuggestion( + val id: String, + @StringRes val title: Int, + @StringRes val summary: Int, + @DrawableRes val icon: Int +) diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenter.kt b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenter.kt index 80b5bd04593..0f377c73682 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenter.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenter.kt @@ -8,10 +8,18 @@ import io.homeassistant.companion.android.onboarding.OnboardApp import kotlinx.coroutines.flow.StateFlow interface SettingsPresenter { + companion object { + const val SUGGESTION_ASSISTANT_APP = "assistant_app" + const val SUGGESTION_NOTIFICATION_PERMISSION = "notification_permission" + } + fun init(view: SettingsView) fun getPreferenceDataStore(): PreferenceDataStore fun onFinish() + fun updateSuggestions(context: Context) + fun cancelSuggestion(context: Context, id: String) suspend fun addServer(result: OnboardApp.Output?) + fun getSuggestionFlow(): StateFlow fun getServersFlow(): StateFlow> fun getServerCount(): Int suspend fun getNotificationRateLimits(): RateLimitResponse? diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenterImpl.kt index 7f990eeef79..b848119fe46 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsPresenterImpl.kt @@ -1,9 +1,15 @@ package io.homeassistant.companion.android.settings +import android.app.role.RoleManager import android.content.Context +import android.os.Build +import android.provider.Settings import android.util.Log +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService import androidx.preference.PreferenceDataStore import io.homeassistant.companion.android.BuildConfig +import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.data.integration.DeviceRegistration import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse import io.homeassistant.companion.android.common.data.prefs.PrefsRepository @@ -29,11 +35,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import javax.inject.Inject +import io.homeassistant.companion.android.common.R as commonR class SettingsPresenterImpl @Inject constructor( private val serverManager: ServerManager, @@ -53,6 +61,8 @@ class SettingsPresenterImpl @Inject constructor( private lateinit var view: SettingsView + private var suggestionFlow = MutableStateFlow(null) + override fun getBoolean(key: String, defValue: Boolean): Boolean = runBlocking { return@runBlocking when (key) { "fullscreen" -> prefsRepository.isFullScreenEnabled() @@ -111,6 +121,8 @@ class SettingsPresenterImpl @Inject constructor( mainScope.cancel() } + override fun getSuggestionFlow(): StateFlow = suggestionFlow + override fun getServersFlow(): StateFlow> = serverManager.defaultServersFlow override fun getServerCount(): Int = serverManager.defaultServers.size @@ -206,4 +218,58 @@ class SettingsPresenterImpl @Inject constructor( ) } } + + override fun updateSuggestions(context: Context) { + mainScope.launch { getSuggestions(context, false) } + } + + override fun cancelSuggestion(context: Context, id: String) { + mainScope.launch { + val ignored = prefsRepository.getIgnoredSuggestions() + if (!ignored.contains(id)) { + prefsRepository.setIgnoredSuggestions(ignored + id) + } + getSuggestions(context, true) + } + } + + private suspend fun getSuggestions(context: Context, overwrite: Boolean) { + val suggestions = mutableListOf() + + // Assist + var assistantSuggestion = serverManager.defaultServers.any { it.version?.isAtLeast(2023, 5) == true } + if (assistantSuggestion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val roleManager = context.getSystemService() + assistantSuggestion = roleManager?.isRoleAvailable(RoleManager.ROLE_ASSISTANT) == true && !roleManager.isRoleHeld(RoleManager.ROLE_ASSISTANT) + } else if (assistantSuggestion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val defaultApp: String? = Settings.Secure.getString(context.contentResolver, "assistant") + assistantSuggestion = defaultApp?.contains(BuildConfig.APPLICATION_ID) == false + } + if (assistantSuggestion) { + suggestions += SettingsHomeSuggestion( + SettingsPresenter.SUGGESTION_ASSISTANT_APP, + commonR.string.suggestion_assist_title, + commonR.string.suggestion_assist_summary, + R.drawable.ic_comment_processing_outline + ) + } + + // Notifications + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !NotificationManagerCompat.from(context).areNotificationsEnabled()) { + suggestions += SettingsHomeSuggestion( + SettingsPresenter.SUGGESTION_NOTIFICATION_PERMISSION, + commonR.string.suggestion_notifications_title, + commonR.string.suggestion_notifications_summary, + commonR.drawable.ic_notifications + ) + } + + val ignored = prefsRepository.getIgnoredSuggestions() + val filteredSuggestions = suggestions.filter { !ignored.contains(it.id) } + if (overwrite || suggestionFlow.value == null) { + suggestionFlow.emit(filteredSuggestions.randomOrNull()) + } else if (filteredSuggestions.none { it.id == suggestionFlow.value?.id }) { + suggestionFlow.emit(null) + } + } } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/SettingsSuggestionPreference.kt b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsSuggestionPreference.kt new file mode 100644 index 00000000000..79e34ae5d18 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/settings/SettingsSuggestionPreference.kt @@ -0,0 +1,31 @@ +package io.homeassistant.companion.android.settings + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageButton +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import io.homeassistant.companion.android.R + +class SettingsSuggestionPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0 +) : Preference(context, attrs, defStyleAttr) { + + private var onCancelClickListener: View.OnClickListener? = null + + init { + layoutResource = R.layout.preference_suggestion + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.itemView.findViewById(R.id.cancel)?.setOnClickListener(onCancelClickListener) + } + + fun setOnPreferenceCancelListener(listener: View.OnClickListener?) { + onCancelClickListener = listener + } +} diff --git a/app/src/main/res/drawable/ic_comment_processing_outline.xml b/app/src/main/res/drawable/ic_comment_processing_outline.xml new file mode 100644 index 00000000000..228127e61c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_comment_processing_outline.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/preference_suggestion_background.xml b/app/src/main/res/drawable/preference_suggestion_background.xml new file mode 100644 index 00000000000..86a198e9652 --- /dev/null +++ b/app/src/main/res/drawable/preference_suggestion_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_suggestion.xml b/app/src/main/res/layout/preference_suggestion.xml new file mode 100644 index 00000000000..62627465f30 --- /dev/null +++ b/app/src/main/res/layout/preference_suggestion.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index aa60ba6d596..851f332cf83 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -2,6 +2,9 @@ + diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt index 392b41a71bc..95df8eec6a2 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt @@ -62,4 +62,8 @@ interface PrefsRepository { suspend fun saveKeyAlias(alias: String) suspend fun getKeyAlias(): String? + + suspend fun getIgnoredSuggestions(): List + + suspend fun setIgnoredSuggestions(ignored: List) } diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt index f7d98ef6aa4..1c0339e9126 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt @@ -30,6 +30,7 @@ class PrefsRepositoryImpl @Inject constructor( private const val PREF_WEBVIEW_DEBUG_ENABLED = "webview_debug_enabled" private const val PREF_KEY_ALIAS = "key-alias" private const val PREF_CRASH_REPORTING_DISABLED = "crash_reporting" + private const val PREF_IGNORED_SUGGESTIONS = "ignored_suggestions" } init { @@ -188,4 +189,12 @@ class PrefsRepositoryImpl @Inject constructor( override suspend fun getKeyAlias(): String? { return localStorage.getString(PREF_KEY_ALIAS) } + + override suspend fun getIgnoredSuggestions(): List { + return localStorage.getStringSet(PREF_IGNORED_SUGGESTIONS)?.toList() ?: emptyList() + } + + override suspend fun setIgnoredSuggestions(ignored: List) { + localStorage.putStringSet(PREF_IGNORED_SUGGESTIONS, ignored.toSet()) + } } diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index fcc3a3260e2..b02635e181d 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -34,6 +34,7 @@ #FF8B66 #F1F3F4 #B3E5FC + #1F03A9F4 #49454E #6649454E diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index b0f9d8dd278..7be7f80359b 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -768,6 +768,10 @@ Request to install app on wear device sent successfully Play Store Request Failed. Wear device(s) may not support Play Store, that is, the Wear device may be version 1.0. Successful + Use Assist from anywhere + Set Home Assistant as your assistant app + Enable notifications + Allow Home Assistant to send notifications Sun Switches Processing Tag