diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt index d0be92affa4..446b7155f25 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt @@ -28,6 +28,8 @@ import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.compose.theme.HATheme import io.homeassistant.companion.android.sensors.SensorReceiver import io.homeassistant.companion.android.sensors.SensorWorker +import io.homeassistant.companion.android.util.ChangeLog +import io.homeassistant.companion.android.util.CheckLocationDisabledUseCase import io.homeassistant.companion.android.util.PLAY_SERVICES_FLAVOR_DOC_URL import io.homeassistant.companion.android.util.PlayServicesAvailability import io.homeassistant.companion.android.util.compose.HAApp @@ -57,6 +59,12 @@ class LaunchActivity : AppCompatActivity() { @Inject internal lateinit var playServicesAvailability: PlayServicesAvailability + @Inject + internal lateinit var checkLocationDisabled: CheckLocationDisabledUseCase + + @Inject + internal lateinit var changeLog: ChangeLog + /** * Represents deep link actions that can be passed to [LaunchActivity] to navigate to specific destinations. */ @@ -151,6 +159,8 @@ class LaunchActivity : AppCompatActivity() { SensorWorker.start(this) lifecycleScope.launch { WebsocketManager.start(this@LaunchActivity) + checkLocationDisabled() + changeLog.showChangeLog(this@LaunchActivity, forceShow = false) } } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt b/app/src/main/kotlin/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt index 9e8e1247b1c..5248d7eed60 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt @@ -109,7 +109,6 @@ class SsidFragment : Fragment() { arrayOf( getString(commonR.string.manage_ssids_wifi), ), - showAsNotification = false, ) } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/CheckLocationDisabledUseCase.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/CheckLocationDisabledUseCase.kt new file mode 100644 index 00000000000..0bfc9dfc33a --- /dev/null +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/CheckLocationDisabledUseCase.kt @@ -0,0 +1,74 @@ +package io.homeassistant.companion.android.util + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.homeassistant.companion.android.common.R +import io.homeassistant.companion.android.common.data.servers.ServerManager +import io.homeassistant.companion.android.common.util.DisabledLocationHandler +import io.homeassistant.companion.android.sensors.SensorReceiver +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Checks whether location is disabled while features that depend on it are active, + * and shows or removes a system notification accordingly. + * + * Features that depend on location include SSID-based home network detection and + * any enabled sensor that requires location permissions. + */ +class CheckLocationDisabledUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val serverManager: ServerManager, +) { + + /** + * Evaluates whether location is required by any active feature and manages + * the location-disabled notification. + */ + suspend operator fun invoke() = withContext(Dispatchers.Default) { + if (DisabledLocationHandler.isLocationEnabled(context)) { + DisabledLocationHandler.removeLocationDisabledWarning(context) + return@withContext + } + + val settingsRequiringLocation = mutableListOf() + + if (isSsidUsed()) { + settingsRequiringLocation.add(context.getString(R.string.pref_connection_homenetwork)) + } + + for (manager in SensorReceiver.MANAGERS) { + for (sensor in manager.getAvailableSensors(context)) { + if (!manager.isEnabled(context, sensor)) continue + + val permissions = manager.requiredPermissions(context, sensor.id) + val needsLocation = + DisabledLocationHandler.containsLocationPermission( + permissions, + fineLocation = true, + ) || + DisabledLocationHandler.containsLocationPermission( + permissions, + fineLocation = false, + ) + + if (needsLocation) { + settingsRequiringLocation.add(context.getString(sensor.name)) + } + } + } + + if (settingsRequiringLocation.isNotEmpty()) { + DisabledLocationHandler.showLocationDisabledNotification( + context, + settingsRequiringLocation.toTypedArray(), + ) + } else { + DisabledLocationHandler.removeLocationDisabledWarning(context) + } + } + + private suspend fun isSsidUsed(): Boolean = + serverManager.getServer()?.connection?.internalSsids?.isNotEmpty() == true +} diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/compose/HANavHost.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/compose/HANavHost.kt index 9ca3c50bf03..076fd40717d 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/compose/HANavHost.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/compose/HANavHost.kt @@ -1,9 +1,7 @@ package io.homeassistant.companion.android.util.compose import android.app.Activity -import android.content.Intent import android.net.Uri -import android.provider.Settings import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.fragment.app.FragmentActivity @@ -150,15 +148,7 @@ private fun openSystemLocationSettings(activity: Activity) { if (DisabledLocationHandler.isLocationEnabled(activity)) { return } - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - } - if (intent.resolveActivity(activity.packageManager) == null) { - intent.action = Settings.ACTION_SETTINGS - } - activity.startActivity(intent) + activity.startActivity(DisabledLocationHandler.locationSettingsIntent(activity)) } private fun showServerSwitcher(activity: Activity?, onServerSelected: (Int) -> Unit) { diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt index 900706926b4..4ec881aa913 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -99,7 +99,6 @@ import io.homeassistant.companion.android.common.data.keychain.NamedKeyChain import io.homeassistant.companion.android.common.data.prefs.NightModeTheme import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.util.AppVersionProvider -import io.homeassistant.companion.android.common.util.DisabledLocationHandler import io.homeassistant.companion.android.common.util.FailFast import io.homeassistant.companion.android.common.util.GestureAction import io.homeassistant.companion.android.common.util.GestureDirection @@ -138,6 +137,7 @@ import io.homeassistant.companion.android.settings.SettingsActivity import io.homeassistant.companion.android.settings.server.ServerChooserFragment import io.homeassistant.companion.android.themes.NightModeManager import io.homeassistant.companion.android.util.ChangeLog +import io.homeassistant.companion.android.util.CheckLocationDisabledUseCase import io.homeassistant.companion.android.util.DataUriDownloadManager import io.homeassistant.companion.android.util.LifecycleHandler import io.homeassistant.companion.android.util.OnSwipeListener @@ -263,6 +263,9 @@ class WebViewActivity : @Inject lateinit var dataUriDownloadManager: DataUriDownloadManager + @Inject + lateinit var checkLocationDisabled: CheckLocationDisabledUseCase + @Inject lateinit var dataSourceFactory: DataSource.Factory @@ -1354,7 +1357,7 @@ class WebViewActivity : window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } - checkAndWarnForDisabledLocation() + checkLocationDisabled() changeLog.showChangeLog(this@WebViewActivity, false) } @@ -1378,40 +1381,6 @@ class WebViewActivity : if (!isFinishing && !isRelaunching) SensorReceiver.updateAllSensors(this) } - private suspend fun checkAndWarnForDisabledLocation() { - var showLocationDisabledWarning = false - val settingsWithLocationPermissions = mutableListOf() - if (!DisabledLocationHandler.isLocationEnabled(this) && presenter.isSsidUsed()) { - showLocationDisabledWarning = true - settingsWithLocationPermissions.add(getString(commonR.string.pref_connection_homenetwork)) - } - for (manager in SensorReceiver.MANAGERS) { - for (basicSensor in manager.getAvailableSensors(this)) { - if (manager.isEnabled(this, basicSensor)) { - val permissions = manager.requiredPermissions(this, basicSensor.id) - - val fineLocation = DisabledLocationHandler.containsLocationPermission(permissions, true) - val coarseLocation = DisabledLocationHandler.containsLocationPermission(permissions, false) - - if ((fineLocation || coarseLocation)) { - if (!DisabledLocationHandler.isLocationEnabled(this)) showLocationDisabledWarning = true - settingsWithLocationPermissions.add(getString(basicSensor.name)) - } - } - } - } - - if (showLocationDisabledWarning) { - DisabledLocationHandler.showLocationDisabledWarnDialog( - this@WebViewActivity, - settingsWithLocationPermissions.toTypedArray(), - true, - ) - } else { - DisabledLocationHandler.removeLocationDisabledWarning(this@WebViewActivity) - } - } - fun exoPlayHls(json: JsonObject) { val payload = json["payload"]?.jsonObjectOrNull() val uri = payload?.getStringOrNull("url")?.toUri() ?: return diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/webview/insecure/BlockInsecureFragment.kt b/app/src/main/kotlin/io/homeassistant/companion/android/webview/insecure/BlockInsecureFragment.kt index 533dcdabcab..4c68ac688a4 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/webview/insecure/BlockInsecureFragment.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/webview/insecure/BlockInsecureFragment.kt @@ -1,8 +1,6 @@ package io.homeassistant.companion.android.webview.insecure -import android.content.Intent import android.os.Bundle -import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -104,15 +102,7 @@ class BlockInsecureFragment : Fragment() { retry() return } - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - } - if (intent.resolveActivity(requireContext().packageManager) == null) { - intent.action = Settings.ACTION_SETTINGS - } - startActivity(intent) + startActivity(DisabledLocationHandler.locationSettingsIntent(requireContext())) } private fun retry() { diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/CheckLocationDisabledUseCaseTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/CheckLocationDisabledUseCaseTest.kt new file mode 100644 index 00000000000..483e8146b5d --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/CheckLocationDisabledUseCaseTest.kt @@ -0,0 +1,166 @@ +package io.homeassistant.companion.android.frontend + +import android.Manifest +import android.content.Context +import io.homeassistant.companion.android.common.data.servers.ServerManager +import io.homeassistant.companion.android.common.sensors.SensorManager.BasicSensor +import io.homeassistant.companion.android.common.util.DisabledLocationHandler +import io.homeassistant.companion.android.database.server.Server +import io.homeassistant.companion.android.database.server.ServerConnectionInfo +import io.homeassistant.companion.android.database.server.ServerSessionInfo +import io.homeassistant.companion.android.database.server.ServerUserInfo +import io.homeassistant.companion.android.sensors.SensorReceiver +import io.homeassistant.companion.android.testing.unit.ConsoleLogExtension +import io.homeassistant.companion.android.util.CheckLocationDisabledUseCase +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.runs +import io.mockk.unmockkObject +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(ConsoleLogExtension::class) +class CheckLocationDisabledUseCaseTest { + + private val context: Context = mockk(relaxed = true) + private val serverManager: ServerManager = mockk(relaxed = true) + + private lateinit var useCase: CheckLocationDisabledUseCase + + @BeforeEach + fun setUp() { + mockkObject(DisabledLocationHandler) + mockkObject(SensorReceiver) + every { SensorReceiver.MANAGERS } returns emptyList() + every { DisabledLocationHandler.removeLocationDisabledWarning(any()) } just runs + every { DisabledLocationHandler.showLocationDisabledNotification(any(), any()) } just runs + useCase = CheckLocationDisabledUseCase( + context = context, + serverManager = serverManager, + ) + } + + @AfterEach + fun tearDown() { + unmockkObject(DisabledLocationHandler) + unmockkObject(SensorReceiver) + } + + @Test + fun `Given location is enabled when invoked then removes warning`() = runTest { + every { DisabledLocationHandler.isLocationEnabled(context) } returns true + + useCase() + + coVerify { DisabledLocationHandler.removeLocationDisabledWarning(context) } + coVerify(exactly = 0) { + DisabledLocationHandler.showLocationDisabledNotification(any(), any()) + } + } + + @Test + fun `Given location is disabled and SSID is used when invoked then shows notification`() = runTest { + every { DisabledLocationHandler.isLocationEnabled(context) } returns false + coEvery { serverManager.getServer(any()) } returns serverWithSsids(listOf("HomeWifi")) + + useCase() + + coVerify { + DisabledLocationHandler.showLocationDisabledNotification(context, any()) + } + } + + @Test + fun `Given location is disabled and sensor requires location when invoked then shows notification`() = runTest { + val locationSensor = BasicSensor(id = "test_sensor", type = "sensor") + val sensorManager: io.homeassistant.companion.android.common.sensors.SensorManager = + mockk(relaxed = true) + + every { DisabledLocationHandler.isLocationEnabled(context) } returns false + coEvery { serverManager.getServer(any()) } returns serverWithSsids(emptyList()) + every { SensorReceiver.MANAGERS } returns listOf(sensorManager) + coEvery { sensorManager.getAvailableSensors(context) } returns listOf(locationSensor) + coEvery { sensorManager.isEnabled(context, locationSensor) } returns true + every { sensorManager.requiredPermissions(context, "test_sensor") } returns arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + ) + + useCase() + + coVerify { + DisabledLocationHandler.showLocationDisabledNotification(context, any()) + } + } + + @Test + fun `Given location is disabled but no feature needs it when invoked then removes warning`() = runTest { + every { DisabledLocationHandler.isLocationEnabled(context) } returns false + coEvery { serverManager.getServer(any()) } returns serverWithSsids(emptyList()) + + useCase() + + coVerify { DisabledLocationHandler.removeLocationDisabledWarning(context) } + coVerify(exactly = 0) { + DisabledLocationHandler.showLocationDisabledNotification(any(), any()) + } + } + + @Test + fun `Given sensor does not require location when invoked then removes warning`() = runTest { + val nonLocationSensor = BasicSensor(id = "battery_sensor", type = "sensor") + val sensorManager: io.homeassistant.companion.android.common.sensors.SensorManager = + mockk(relaxed = true) + + every { DisabledLocationHandler.isLocationEnabled(context) } returns false + coEvery { serverManager.getServer(any()) } returns serverWithSsids(emptyList()) + every { SensorReceiver.MANAGERS } returns listOf(sensorManager) + coEvery { sensorManager.getAvailableSensors(context) } returns listOf(nonLocationSensor) + coEvery { sensorManager.isEnabled(context, nonLocationSensor) } returns true + every { sensorManager.requiredPermissions(context, "battery_sensor") } returns emptyArray() + + useCase() + + coVerify { DisabledLocationHandler.removeLocationDisabledWarning(context) } + coVerify(exactly = 0) { + DisabledLocationHandler.showLocationDisabledNotification(any(), any()) + } + } + + @Test + fun `Given sensor requires location but is disabled when invoked then removes warning`() = runTest { + val locationSensor = BasicSensor(id = "gps_sensor", type = "sensor") + val sensorManager: io.homeassistant.companion.android.common.sensors.SensorManager = + mockk(relaxed = true) + + every { DisabledLocationHandler.isLocationEnabled(context) } returns false + coEvery { serverManager.getServer(any()) } returns serverWithSsids(emptyList()) + every { SensorReceiver.MANAGERS } returns listOf(sensorManager) + coEvery { sensorManager.getAvailableSensors(context) } returns listOf(locationSensor) + coEvery { sensorManager.isEnabled(context, locationSensor) } returns false + + useCase() + + coVerify { DisabledLocationHandler.removeLocationDisabledWarning(context) } + coVerify(exactly = 0) { + DisabledLocationHandler.showLocationDisabledNotification(any(), any()) + } + } +} + +private fun serverWithSsids(ssids: List): Server = Server( + id = 1, + _name = "Test Server", + connection = ServerConnectionInfo( + externalUrl = "https://example.com", + internalSsids = ssids, + ), + session = ServerSessionInfo(), + user = ServerUserInfo(), +) diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt index 693b1c7398b..50268bc0940 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt @@ -10,10 +10,12 @@ import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.UninstallModules import io.homeassistant.companion.android.common.data.servers.ServerManager +import io.homeassistant.companion.android.common.util.DisabledLocationHandler import io.homeassistant.companion.android.di.ServerManagerModule import io.homeassistant.companion.android.sensors.SensorReceiver import io.homeassistant.companion.android.sensors.SensorWorker import io.homeassistant.companion.android.testing.unit.ConsoleLogRule +import io.homeassistant.companion.android.util.ChangeLog import io.homeassistant.companion.android.websocket.WebsocketManager import io.mockk.Runs import io.mockk.coEvery @@ -21,7 +23,9 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.mockkObject +import io.mockk.unmockkConstructor import io.mockk.unmockkObject import io.mockk.verify import org.junit.After @@ -56,9 +60,13 @@ class LaunchActivityTest { mockkObject(SensorWorker.Companion) mockkObject(WebsocketManager.Companion) mockkObject(SensorReceiver.Companion) + mockkObject(DisabledLocationHandler) every { SensorWorker.start(any()) } just Runs coEvery { WebsocketManager.start(any()) } just Runs every { SensorReceiver.updateAllSensors(any()) } just Runs + every { DisabledLocationHandler.isLocationEnabled(any()) } returns true + mockkConstructor(ChangeLog::class) + coEvery { anyConstructed().showChangeLog(any(), any()) } just Runs } @After @@ -66,13 +74,17 @@ class LaunchActivityTest { unmockkObject(SensorWorker.Companion) unmockkObject(WebsocketManager.Companion) unmockkObject(SensorReceiver.Companion) + unmockkObject(DisabledLocationHandler) + unmockkConstructor(ChangeLog::class) } @Test - fun `Given activity resumes then sensor worker and websocket manager are started`() { + fun `Given activity resumes then sensor worker and websocket manager are started and changelog is shown`() { ActivityScenario.launch(LaunchActivity::class.java).use { verify { SensorWorker.start(any()) } coVerify { WebsocketManager.start(any()) } + verify { DisabledLocationHandler.isLocationEnabled(any()) } + coVerify { anyConstructed().showChangeLog(any(), eq(false)) } } } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/util/DisabledLocationHandler.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/util/DisabledLocationHandler.kt index eaf14103343..32dccba9729 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/util/DisabledLocationHandler.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/util/DisabledLocationHandler.kt @@ -2,7 +2,6 @@ package io.homeassistant.companion.android.common.util import android.Manifest import android.annotation.SuppressLint -import android.app.Activity import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -29,7 +28,7 @@ object DisabledLocationHandler { } fun isLocationEnabled(context: Context): Boolean { - val lm: LocationManager = context.getSystemService()!! + val lm: LocationManager = context.getSystemService() ?: return false return if (VERSION.SDK_INT >= VERSION_CODES.P) { lm.isLocationEnabled @@ -56,18 +55,96 @@ object DisabledLocationHandler { } } - fun removeLocationDisabledWarning(activity: Activity) { - NotificationManagerCompat.from(activity) + fun removeLocationDisabledWarning(context: Context) { + NotificationManagerCompat.from(context) .cancel(DISABLED_LOCATION_WARN_ID, DISABLED_LOCATION_WARN_ID.hashCode()) } - // Suppressing QueryPermissionsNeeded: System Settings intents are always visible per Android's - // package visibility documentation, and the app has QUERY_ALL_PACKAGES permission. + /** + * Creates an [Intent] that opens the system location settings screen. + * + * Falls back to general settings if the location settings activity is not available. + */ @SuppressLint("QueryPermissionsNeeded") + fun locationSettingsIntent(context: Context): Intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + if (resolveActivity(context.packageManager) == null) { + action = Settings.ACTION_SETTINGS + } + } + + /** + * Shows a persistent notification warning the user that location is disabled, + * listing the [settings] that require it. Tapping the notification opens location settings. + * + * No-ops if the notification is already visible. + */ + @SuppressLint("MissingPermission") + fun showLocationDisabledNotification(context: Context, settings: Array) { + val notificationManager = NotificationManagerCompat.from(context) + if (notificationManager.getActiveNotification( + DISABLED_LOCATION_WARN_ID, + DISABLED_LOCATION_WARN_ID.hashCode(), + ) != null + ) { + return + } + + val parameters = settings.joinToString(separator = "\n") { "- $it" } + + if (VERSION.SDK_INT >= VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_LOCATION_DISABLED, + context.applicationContext.getString(commonR.string.location_warn_channel), + NotificationManager.IMPORTANCE_DEFAULT, + ) + notificationManager.createNotificationChannel(channel) + } + + val pendingIntent = PendingIntent.getActivity( + context, + 0, + locationSettingsIntent(context), + PendingIntent.FLAG_IMMUTABLE, + ) + + val notification = NotificationCompat.Builder(context, CHANNEL_LOCATION_DISABLED) + .setSmallIcon(commonR.drawable.ic_stat_ic_notification) + .setColor(Color.RED) + .setOngoing(true) + .setContentTitle(context.applicationContext.getString(commonR.string.location_disabled_title)) + .setContentText( + context.applicationContext.getString( + commonR.string.location_disabled_notification_short_message, + ), + ) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText( + context.applicationContext.getString( + commonR.string.location_disabled_notification_message, + parameters, + ), + ), + ) + .setContentIntent(pendingIntent) + .build() + + notificationManager.notify(DISABLED_LOCATION_WARN_ID, DISABLED_LOCATION_WARN_ID.hashCode(), notification) + } + + /** + * Shows a dialog warning the user that location is disabled, listing the [settings] that + * require it. The positive button opens location settings. + * + * @param withDisableOption when true and [callback] is provided, the negative button label + * becomes a "disable" option that triggers [callback] instead of simply dismissing. + */ fun showLocationDisabledWarnDialog( context: Context, settings: Array, - showAsNotification: Boolean = false, withDisableOption: Boolean = false, callback: (() -> Unit)? = null, ) { @@ -78,92 +155,26 @@ object DisabledLocationHandler { commonR.string.confirm_negative } - val intent = Intent( - Settings.ACTION_LOCATION_SOURCE_SETTINGS, - ) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - - if (intent.resolveActivity(context.packageManager) == null) { - intent.action = Settings.ACTION_SETTINGS - } - - var parameters = "" - for (setting in settings) { - parameters += "- $setting\n" - } - - if ((!withDisableOption || callback == null) && showAsNotification) { - val notificationManager = NotificationManagerCompat.from(context) - if (notificationManager.getActiveNotification( - DISABLED_LOCATION_WARN_ID, - DISABLED_LOCATION_WARN_ID.hashCode(), - ) == - null - ) { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - val channel = - NotificationChannel( - CHANNEL_LOCATION_DISABLED, - context.applicationContext.getString(commonR.string.location_warn_channel), - NotificationManager.IMPORTANCE_DEFAULT, - ) - notificationManager.createNotificationChannel(channel) - } - - val pendingIntent = PendingIntent.getActivity( - context, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE, - ) + val parameters = settings.joinToString(separator = "\n") { "- $it" } - val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_LOCATION_DISABLED) - .setSmallIcon(commonR.drawable.ic_stat_ic_notification) - .setColor(Color.RED) - .setOngoing(true) - .setContentTitle(context.applicationContext.getString(commonR.string.location_disabled_title)) - .setContentText( - context.applicationContext.getString( - commonR.string.location_disabled_notification_short_message, - ), - ) - .setStyle( - NotificationCompat.BigTextStyle() - .bigText( - context.applicationContext.getString( - commonR.string.location_disabled_notification_message, - parameters, - ), - ), - ) - .setContentIntent(pendingIntent) - - NotificationManagerCompat.from( - context, - ).notify(DISABLED_LOCATION_WARN_ID, DISABLED_LOCATION_WARN_ID.hashCode(), notificationBuilder.build()) - } - } else { - AlertDialog.Builder(context) - .setTitle(commonR.string.location_disabled_title) - .setMessage( + AlertDialog.Builder(context) + .setTitle(commonR.string.location_disabled_title) + .setMessage( + context.applicationContext.getString( + commonR.string.location_disabled_dialog_message, context.applicationContext.getString( - commonR.string.location_disabled_dialog_message, - context.applicationContext.getString( - commonR.string.location_disabled_notification_message, - parameters, - ), + commonR.string.location_disabled_notification_message, + parameters, ), - ) - .setPositiveButton(positionTextId) { _, _ -> - context.applicationContext.startActivity(intent) + ), + ) + .setPositiveButton(positionTextId) { _, _ -> + context.applicationContext.startActivity(locationSettingsIntent(context)) + } + .setNegativeButton(negativeTextId) { _, _ -> + if (withDisableOption && callback != null) { + callback() } - .setNegativeButton(negativeTextId) { _, _ -> - if (withDisableOption && callback != null) { - callback() - } - }.show() - } + }.show() } }