Skip to content

Commit b0e85e2

Browse files
authored
Add battery optimization permission to onboarding (#6095)
* Request battery optimization permission when leaving Location screens * Remove battery optimization for LocationForSecureConnectionScreen * Cleanup * Fix crash on Android 6 with Github icon * Apply suggestion
1 parent 5e0098b commit b0e85e2

File tree

6 files changed

+84
-39
lines changed

6 files changed

+84
-39
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingScreen.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.homeassistant.companion.android.onboarding.locationsharing
22

3+
import androidx.activity.compose.rememberLauncherForActivityResult
4+
import androidx.activity.result.contract.ActivityResultContracts
35
import androidx.compose.foundation.Image
46
import androidx.compose.foundation.layout.Arrangement
57
import androidx.compose.foundation.layout.Column
@@ -17,6 +19,7 @@ import androidx.compose.material3.Text
1719
import androidx.compose.runtime.Composable
1820
import androidx.compose.ui.Alignment
1921
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.platform.LocalContext
2023
import androidx.compose.ui.res.painterResource
2124
import androidx.compose.ui.res.stringResource
2225
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -29,6 +32,7 @@ import io.homeassistant.companion.android.common.compose.theme.HADimens
2932
import io.homeassistant.companion.android.common.compose.theme.HATextStyle
3033
import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview
3134
import io.homeassistant.companion.android.common.compose.theme.MaxButtonWidth
35+
import io.homeassistant.companion.android.common.util.createBatteryOptimizationIntent
3236
import io.homeassistant.companion.android.util.compose.HAPreviews
3337
import io.homeassistant.companion.android.util.compose.rememberLocationPermission
3438

@@ -114,10 +118,20 @@ private fun LocationSharingContent(
114118
@Composable
115119
@OptIn(ExperimentalPermissionsApi::class)
116120
private fun BottomButtons(onGoToNextScreen: () -> Unit, onLocationSharingResponse: (enabled: Boolean) -> Unit) {
121+
val context = LocalContext.current
122+
val batteryOptimizationLauncher = rememberLauncherForActivityResult(
123+
contract = ActivityResultContracts.StartActivityForResult(),
124+
onResult = { onGoToNextScreen() },
125+
)
117126
val permissions = rememberLocationPermission(
118127
onPermissionResult = {
119128
// We ignore the result and proceed even if the user rejected the permission
120-
onGoToNextScreen()
129+
val intent = context.createBatteryOptimizationIntent()
130+
if (intent != null) {
131+
batteryOptimizationLauncher.launch(intent)
132+
} else {
133+
onGoToNextScreen()
134+
}
121135
},
122136
)
123137
Column(

app/src/main/res/drawable/github.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@
55
android:viewportHeight="96">
66
<path
77
android:pathData="M48.854,0C21.839,0 0,22 0,49.217c0,21.756 13.993,40.172 33.405,46.69 2.427,0.49 3.316,-1.059 3.316,-2.362 0,-1.141 -0.08,-5.052 -0.08,-9.127 -13.59,2.934 -16.42,-5.867 -16.42,-5.867 -2.184,-5.704 -5.42,-7.17 -5.42,-7.17 -4.448,-3.015 0.324,-3.015 0.324,-3.015 4.934,0.326 7.523,5.052 7.523,5.052 4.367,7.496 11.404,5.378 14.235,4.074 0.404,-3.178 1.699,-5.378 3.074,-6.6 -10.839,-1.141 -22.243,-5.378 -22.243,-24.283 0,-5.378 1.94,-9.778 5.014,-13.2 -0.485,-1.222 -2.184,-6.275 0.486,-13.038 0,0 4.125,-1.304 13.426,5.052a46.97,46.97 0,0 1,12.214 -1.63c4.125,0 8.33,0.571 12.213,1.63 9.302,-6.356 13.427,-5.052 13.427,-5.052 2.67,6.763 0.97,11.816 0.485,13.038 3.155,3.422 5.015,7.822 5.015,13.2 0,18.905 -11.404,23.06 -22.324,24.283 1.78,1.548 3.316,4.481 3.316,9.126 0,6.6 -0.08,11.897 -0.08,13.526 0,1.304 0.89,2.853 3.316,2.364 19.412,-6.52 33.405,-24.935 33.405,-46.691C97.707,22 75.788,0 48.854,0z"
8-
android:fillColor="#24292f"
9-
android:fillType="evenOdd"/>
8+
android:fillColor="#24292f" />
109
</vector>

app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingScreenTest.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class LocationSharingScreenTest {
4141
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
4242

4343
@Test
44-
fun `Given screen displayed when clicking on share and granting permission then go to next screen and location response set to true`() {
44+
fun `Given screen displayed when clicking on share and granting permission then go to next screen and location response set to true and ask for battery optimization`() {
4545
composeTestRule.apply {
4646
testScreen {
4747
// Check all elements are displayed
@@ -52,12 +52,13 @@ class LocationSharingScreenTest {
5252
assertTrue(locationSharingResponse == true)
5353
assertTrue(goToNextScreenClicked)
5454
registry.assertLocationPermissionRequested()
55+
registry.assertBatteryOptimizationRequested()
5556
}
5657
}
5758
}
5859

5960
@Test
60-
fun `Given screen displayed when clicking do not share then go to next screen and location response set to false`() {
61+
fun `Given screen displayed when clicking do not share then go to next screen and location response set to false without battery optimization`() {
6162
composeTestRule.apply {
6263
testScreen(false) {
6364
// Check all elements are displayed
@@ -68,12 +69,13 @@ class LocationSharingScreenTest {
6869
assertTrue(locationSharingResponse == false)
6970
assertTrue(goToNextScreenClicked)
7071
registry.assertLocationPermissionNotRequested()
72+
registry.assertBatteryOptimizationNotRequested()
7173
}
7274
}
7375
}
7476

7577
@Test
76-
fun `Given screen displayed when clicking on share and not granting permission then go to next screen and location response set to true`() {
78+
fun `Given screen displayed when clicking on share and not granting permission then go to next screen and location response set to true and still ask for battery optimization`() {
7779
composeTestRule.apply {
7880
testScreen(false) {
7981
// Check all elements are displayed
@@ -85,6 +87,8 @@ class LocationSharingScreenTest {
8587
assertTrue(goToNextScreenClicked)
8688
// background is only requested if foreground is granted
8789
registry.assertLocationPermissionRequested(false)
90+
// Battery optimization is still requested even when permission is denied
91+
registry.assertBatteryOptimizationRequested()
8892
}
8993
}
9094
}

app/src/test/kotlin/io/homeassistant/companion/android/util/LocationPermissionActivityResultRegistry.kt

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
package io.homeassistant.companion.android.util
22

3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.provider.Settings
6+
import androidx.activity.result.ActivityResult
37
import androidx.activity.result.ActivityResultRegistry
48
import androidx.activity.result.contract.ActivityResultContract
59
import androidx.activity.result.contract.ActivityResultContracts
610
import androidx.core.app.ActivityOptionsCompat
711
import org.junit.jupiter.api.Assertions.assertEquals
12+
import org.junit.jupiter.api.Assertions.assertFalse
13+
import org.junit.jupiter.api.Assertions.assertTrue
814

915
/**
1016
* Fake activity result registry to invoke the callback of rememberLocationPermission
17+
* and rememberBatteryOptimizationLauncher.
1118
*/
1219
class LocationPermissionActivityResultRegistry(val granted: Boolean) : ActivityResultRegistry() {
1320
val permissionRequested = mutableListOf<String>()
21+
var batteryOptimizationRequested = false
22+
private set
1423

24+
@Suppress("UNCHECKED_CAST")
1525
override fun <I, O> onLaunch(requestCode: Int, contract: ActivityResultContract<I, O>, input: I, options: ActivityOptionsCompat?) {
16-
if (contract is ActivityResultContracts.RequestMultiplePermissions) {
17-
(input as? Array<String>)?.toList()?.let {
18-
permissionRequested.addAll(it)
26+
when (contract) {
27+
is ActivityResultContracts.RequestMultiplePermissions -> {
28+
(input as? Array<String>)?.toList()?.let {
29+
permissionRequested.addAll(it)
30+
}
31+
dispatchResult(requestCode, mapOf("android.permission.ACCESS_FINE_LOCATION" to granted, "android.permission.ACCESS_COARSE_LOCATION" to granted))
32+
}
33+
is ActivityResultContracts.RequestPermission -> {
34+
permissionRequested.add(input.toString())
35+
dispatchResult(requestCode, granted)
36+
}
37+
is ActivityResultContracts.StartActivityForResult -> {
38+
val intent = input as? Intent
39+
if (intent?.action == Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) {
40+
batteryOptimizationRequested = true
41+
dispatchResult(requestCode, ActivityResult(Activity.RESULT_OK, null) as O)
42+
}
1943
}
20-
dispatchResult(requestCode, mapOf("android.permission.ACCESS_FINE_LOCATION" to granted, "android.permission.ACCESS_COARSE_LOCATION" to granted))
21-
} else if (contract is ActivityResultContracts.RequestPermission) {
22-
permissionRequested.add(input.toString())
23-
dispatchResult(requestCode, granted)
2444
}
2545
}
2646

@@ -38,4 +58,12 @@ class LocationPermissionActivityResultRegistry(val granted: Boolean) : ActivityR
3858
fun assertLocationPermissionNotRequested() {
3959
assertEquals(emptyList<String>(), permissionRequested)
4060
}
61+
62+
fun assertBatteryOptimizationRequested() {
63+
assertTrue(batteryOptimizationRequested)
64+
}
65+
66+
fun assertBatteryOptimizationNotRequested() {
67+
assertFalse(batteryOptimizationRequested)
68+
}
4169
}

app/src/test/kotlin/io/homeassistant/companion/android/webview/addto/EntityAddToActionTest.kt

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,12 @@ import io.homeassistant.companion.android.common.R as commonR
55
import io.homeassistant.companion.android.common.util.isAutomotive
66
import io.mockk.every
77
import io.mockk.mockk
8-
import io.mockk.mockkStatic
9-
import io.mockk.unmockkStatic
10-
import org.junit.jupiter.api.AfterEach
118
import org.junit.jupiter.api.Assertions.assertEquals
12-
import org.junit.jupiter.api.BeforeEach
139
import org.junit.jupiter.api.Test
1410

1511
class EntityAddToActionTest {
1612

17-
private lateinit var context: Context
18-
19-
@BeforeEach
20-
fun setUp() {
21-
context = mockk(relaxed = true)
22-
mockkStatic("io.homeassistant.companion.android.common.util.ContextExtKt")
23-
}
24-
25-
@AfterEach
26-
fun tearDown() {
27-
unmockkStatic("io.homeassistant.companion.android.common.util.ContextExtKt")
28-
}
13+
private val context: Context = mockk(relaxed = true)
2914

3015
@Test
3116
fun `Given automotive context when testing AndroidAutoFavorite then return correct attributes`() {

common/src/main/kotlin/io/homeassistant/companion/android/common/util/ContextExt.kt

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.homeassistant.companion.android.common.util
22

3+
import android.annotation.SuppressLint
34
import android.content.Context
45
import android.content.Intent
56
import android.content.SharedPreferences
6-
import android.os.Build
77
import android.os.PowerManager
88
import android.provider.Settings
99
import androidx.core.content.getSystemService
@@ -48,12 +48,28 @@ fun Context.isAutomotive(): Boolean {
4848
*/
4949
fun Context.maybeAskForIgnoringBatteryOptimizations() {
5050
if (!isIgnoringBatteryOptimizations()) {
51-
startActivity(
52-
Intent(
53-
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
54-
"package:$packageName".toUri(),
55-
),
51+
startActivity(createBatteryOptimizationIntent())
52+
}
53+
}
54+
55+
/**
56+
* Creates an [Intent] to request ignoring battery optimizations.
57+
*
58+
* This intent can be used with an [androidx.activity.result.ActivityResultLauncher] to
59+
* wait for the user to respond to the battery optimization dialog before proceeding.
60+
*
61+
* @return An [Intent] configured to request battery optimization exemption, or `null` if
62+
* the app is already ignoring battery optimizations or not available.
63+
*/
64+
@SuppressLint("BatteryLife")
65+
fun Context.createBatteryOptimizationIntent(): Intent? {
66+
return if (!isIgnoringBatteryOptimizations()) {
67+
Intent(
68+
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
69+
"package:$packageName".toUri(),
5670
)
71+
} else {
72+
null
5773
}
5874
}
5975

@@ -64,8 +80,7 @@ fun Context.maybeAskForIgnoringBatteryOptimizations() {
6480
* @return `true` if the app is ignoring battery optimizations, `false` otherwise.
6581
*/
6682
fun Context.isIgnoringBatteryOptimizations(): Boolean {
67-
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.M ||
68-
getSystemService<PowerManager>()
69-
?.isIgnoringBatteryOptimizations(packageName ?: "")
70-
?: false
83+
return getSystemService<PowerManager>()
84+
?.isIgnoringBatteryOptimizations(packageName ?: "")
85+
?: false
7186
}

0 commit comments

Comments
 (0)