Add a banner to ask the user to disable battery optimization when Event cannot be resolved from Push.
This commit is contained in:
parent
11c2467577
commit
7deed4cc86
22 changed files with 583 additions and 11 deletions
|
|
@ -7,6 +7,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".notifications.TestNotificationReceiver"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.battery
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
interface BatteryOptimization {
|
||||
/**
|
||||
* Tells if the application ignores battery optimizations.
|
||||
*
|
||||
* Ignoring them allows the app to run in background to make background sync with the homeserver.
|
||||
* This user option appears on Android M but Android O enforces its usage and kills apps not
|
||||
* authorised by the user to run in background.
|
||||
*
|
||||
* @return true if battery optimisations are ignored
|
||||
*/
|
||||
fun isIgnoringBatteryOptimizations(): Boolean
|
||||
|
||||
/**
|
||||
* Request the user to disable battery optimizations for this app.
|
||||
* This will open the system settings where the user can disable battery optimizations.
|
||||
* See https://developer.android.com/training/monitoring-device-state/doze-standby#exemption-cases
|
||||
*
|
||||
* @param activity The activity from which to start the settings intent.
|
||||
* @return true if the intent was successfully started, false if the activity was not found
|
||||
*/
|
||||
fun requestDisablingBatteryOptimization(activity: Activity?): Boolean
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidBatteryOptimization @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
) : BatteryOptimization {
|
||||
override fun isIgnoringBatteryOptimizations(): Boolean {
|
||||
return context.getSystemService<PowerManager>()
|
||||
?.isIgnoringBatteryOptimizations(context.packageName) == true
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun requestDisablingBatteryOptimization(activity: Activity?): Boolean {
|
||||
activity ?: return false
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
intent.data = ("package:" + context.packageName).toUri()
|
||||
return try {
|
||||
activity.startActivity(intent)
|
||||
true
|
||||
} catch (exception: ActivityNotFoundException) {
|
||||
Timber.w(exception, "Cannot request ignoring battery optimizations.")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.battery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
||||
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BatteryOptimizationPresenter @Inject constructor(
|
||||
private val pushDataStore: PushDataStore,
|
||||
private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore,
|
||||
private val batteryOptimization: BatteryOptimization,
|
||||
) : Presenter<BatteryOptimizationState> {
|
||||
@Composable
|
||||
override fun present(): BatteryOptimizationState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var isRequestSent by remember { mutableStateOf(false) }
|
||||
var localShouldDisplayBanner by remember { mutableStateOf(true) }
|
||||
val storeShouldDisplayBanner by pushDataStore.shouldDisplayBatteryOptimizationBannerFlow.collectAsState(initial = false)
|
||||
var isSystemIgnoringBatteryOptimizations by remember {
|
||||
mutableStateOf(batteryOptimization.isIgnoringBatteryOptimizations())
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
isSystemIgnoringBatteryOptimizations = batteryOptimization.isIgnoringBatteryOptimizations()
|
||||
if (isRequestSent) {
|
||||
localShouldDisplayBanner = false
|
||||
}
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
return BatteryOptimizationState(
|
||||
shouldDisplayBanner = localShouldDisplayBanner && storeShouldDisplayBanner && !isSystemIgnoringBatteryOptimizations,
|
||||
dismiss = {
|
||||
coroutineScope.launch {
|
||||
mutableBatteryOptimizationStore.onOptimizationBannerDismissed()
|
||||
}
|
||||
},
|
||||
openSettings = { activity ->
|
||||
isRequestSent = true
|
||||
if (batteryOptimization.requestDisablingBatteryOptimization(activity).not()) {
|
||||
// If not able to perform the request, ensure that we do not display the banner again
|
||||
coroutineScope.launch {
|
||||
mutableBatteryOptimizationStore.onOptimizationBannerDismissed()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,16 +10,25 @@ package io.element.android.libraries.push.impl.di
|
|||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||
import io.element.android.libraries.push.impl.battery.BatteryOptimizationPresenter
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object PushModule {
|
||||
@Provides
|
||||
fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat {
|
||||
return NotificationManagerCompat.from(context)
|
||||
interface PushModule {
|
||||
companion object {
|
||||
@Provides
|
||||
fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat {
|
||||
return NotificationManagerCompat.from(context)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
fun bindBatteryOptimizationPresenter(presenter: BatteryOptimizationPresenter): Presenter<BatteryOptimizationState>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||
private val incrementPushDataStore: IncrementPushDataStore,
|
||||
private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val buildMeta: BuildMeta,
|
||||
|
|
@ -102,6 +103,7 @@ class DefaultPushHandler @Inject constructor(
|
|||
sessionId = request.sessionId,
|
||||
reason = exception.message ?: exception.javaClass.simpleName,
|
||||
)
|
||||
mutableBatteryOptimizationStore.showBatteryOptimizationBanner()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
|
||||
import javax.inject.Inject
|
||||
|
||||
interface MutableBatteryOptimizationStore {
|
||||
suspend fun showBatteryOptimizationBanner()
|
||||
suspend fun onOptimizationBannerDismissed()
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMutableBatteryOptimizationStore @Inject constructor(
|
||||
private val defaultPushDataStore: DefaultPushDataStore,
|
||||
) : MutableBatteryOptimizationStore {
|
||||
override suspend fun showBatteryOptimizationBanner() {
|
||||
defaultPushDataStore.setBatteryOptimizationBannerState(1)
|
||||
}
|
||||
|
||||
override suspend fun onOptimizationBannerDismissed() {
|
||||
defaultPushDataStore.setBatteryOptimizationBannerState(2)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,10 +43,24 @@ class DefaultPushDataStore @Inject constructor(
|
|||
) : PushDataStore {
|
||||
private val pushCounter = intPreferencesKey("push_counter")
|
||||
|
||||
/**
|
||||
* Integer preference to track the state of the battery optimization banner.
|
||||
* Possible values:
|
||||
* [BATTERY_OPTIMIZATION_BANNER_STATE_INIT]: Should not show the banner
|
||||
* [BATTERY_OPTIMIZATION_BANNER_STATE_SHOW]: Should show the banner
|
||||
* [BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED]: Banner has been shown and user has dismissed it
|
||||
*/
|
||||
private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state")
|
||||
|
||||
override val pushCounterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
|
||||
preferences[pushCounter] ?: 0
|
||||
}
|
||||
|
||||
@Suppress("UnnecessaryParentheses")
|
||||
override val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
|
||||
(preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW
|
||||
}
|
||||
|
||||
suspend fun incrementPushCounter() {
|
||||
context.dataStore.edit { settings ->
|
||||
val currentCounterValue = settings[pushCounter] ?: 0
|
||||
|
|
@ -54,6 +68,18 @@ class DefaultPushDataStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun setBatteryOptimizationBannerState(newState: Int) {
|
||||
context.dataStore.edit { settings ->
|
||||
val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT
|
||||
settings[batteryOptimizationBannerState] = when (currentValue) {
|
||||
BATTERY_OPTIMIZATION_BANNER_STATE_INIT,
|
||||
BATTERY_OPTIMIZATION_BANNER_STATE_SHOW -> newState
|
||||
BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED -> currentValue
|
||||
else -> error("Invalid value for showBatteryOptimizationBanner: $currentValue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushDatabase.pushHistoryQueries.selectAll()
|
||||
.asFlow()
|
||||
|
|
@ -84,4 +110,10 @@ class DefaultPushDataStore @Inject constructor(
|
|||
it.clear()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BATTERY_OPTIMIZATION_BANNER_STATE_INIT = 0
|
||||
const val BATTERY_OPTIMIZATION_BANNER_STATE_SHOW = 1
|
||||
const val BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED = 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import io.element.android.libraries.push.api.history.PushHistoryItem
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushDataStore {
|
||||
val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean>
|
||||
val pushCounterFlow: Flow<Int>
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue