Skip to content

Commit

Permalink
Battery optimization (#44)
Browse files Browse the repository at this point in the history
* Add battery state in user session

* Start location tracking on boot complete

* Turn off battery optimization

* Minor fix

* Minor fix

* Lint fix
  • Loading branch information
cp-radhika-s authored May 8, 2024
1 parent 736bd1e commit f50059b
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 10 deletions.
19 changes: 18 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<application
android:name=".YourSpaceApplication"
Expand Down Expand Up @@ -69,6 +70,22 @@
tools:node="remove" />
</provider>

<receiver android:name=".domain.receiver.BatteryBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BATTERY_LOW" />
<action android:name="android.intent.action.BATTERY_OKAY" />
</intent-filter>
</receiver>

<receiver
android:name=".domain.receiver.BootCompleteReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.canopas.yourspace.domain.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.BatteryManager
import com.canopas.yourspace.data.service.auth.AuthService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class BatteryBroadcastReceiver @Inject constructor(
private val authService: AuthService
) : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BATTERY_OKAY || intent?.action == Intent.ACTION_BATTERY_LOW) {
CoroutineScope(Dispatchers.IO).launch {
try {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val batteryPct = level / scale.toFloat() * 100
authService.updateBatteryStatus(batteryPct)
} catch (e: Exception) {
Timber.e(e, "Failed to update battery status")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.canopas.yourspace.domain.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.service.location.LocationManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class BootCompleteReceiver : BroadcastReceiver() {

@Inject
lateinit var locationManager: LocationManager

@Inject
lateinit var authService: AuthService

override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action && authService.currentUser != null) {
locationManager.startService()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.Activity
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
Expand All @@ -14,6 +15,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand Down Expand Up @@ -100,6 +102,8 @@ fun PermissionDialog(
title: String,
subTitle1: String,
subTitle2: String? = null,
dismissBtn: String? = null,
confirmBtn: String? = null,
onDismiss: () -> Unit,
goToSettings: () -> Unit
) {
Expand All @@ -120,15 +124,15 @@ fun PermissionDialog(
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth(),
style = AppTheme.appTypography.header1
style = AppTheme.appTypography.header3
)
Text(
text = subTitle1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(vertical = 10.dp)
.fillMaxWidth(),
style = AppTheme.appTypography.body2
style = AppTheme.appTypography.body1
)

if (subTitle2 != null) {
Expand All @@ -142,12 +146,21 @@ fun PermissionDialog(
)
}

Spacer(modifier = Modifier.padding(10.dp))

PrimaryButton(
modifier = Modifier
.padding(vertical = 10.dp),
label = stringResource(id = R.string.common_background_access_permission_btn),
label = if (!confirmBtn.isNullOrEmpty()) confirmBtn else stringResource(id = R.string.common_background_access_permission_btn),
onClick = goToSettings
)

dismissBtn?.let {
Spacer(modifier = Modifier.padding(4.dp))
PrimaryTextButton(
label = dismissBtn,
onClick = onDismiss,
containerColor = Color.Transparent
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.canopas.yourspace.ui.flow.home.home

import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.annotation.DrawableRes
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState
Expand Down Expand Up @@ -37,7 +40,9 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.canopas.yourspace.R
import com.canopas.yourspace.data.utils.isBackgroundLocationPermissionGranted
import com.canopas.yourspace.data.utils.isBatteryOptimizationEnabled
import com.canopas.yourspace.ui.component.AppProgressIndicator
import com.canopas.yourspace.ui.component.PermissionDialog
import com.canopas.yourspace.ui.flow.home.activity.ActivityScreen
import com.canopas.yourspace.ui.flow.home.home.component.SpaceSelectionMenu
import com.canopas.yourspace.ui.flow.home.home.component.SpaceSelectionPopup
Expand All @@ -59,6 +64,12 @@ fun HomeScreen() {
}
}

LaunchedEffect(Unit) {
if (context.isBatteryOptimizationEnabled) {
viewModel.showBatteryOptimizationDialog()
}
}

Scaffold(
containerColor = AppTheme.colorScheme.surface,
content = {
Expand Down Expand Up @@ -144,6 +155,23 @@ fun HomeTopBar() {
}
}
}

if (state.showBatteryOptimizationPopup) {
val context = LocalContext.current
PermissionDialog(
title = stringResource(R.string.battery_optimization_dialog_title),
subTitle1 = stringResource(R.string.battery_optimization_dialog_message),
dismissBtn = stringResource(R.string.common_btn_cancel),
confirmBtn = stringResource(R.string.battery_optimization_dialog_btn_change_now),
onDismiss = { viewModel.dismissBatteryOptimizationDialog() },
goToSettings = {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:${context.packageName}")
context.startActivity(intent)
viewModel.dismissBatteryOptimizationDialog()
}
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.canopas.yourspace.data.utils.AppDispatcher
import com.canopas.yourspace.ui.navigation.AppDestinations
import com.canopas.yourspace.ui.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
Expand Down Expand Up @@ -169,6 +170,15 @@ class HomeScreenViewModel @Inject constructor(
if (spaceRepository.currentSpaceId.isEmpty()) return
navigator.navigateTo(AppDestinations.spaceThreads.path)
}

fun showBatteryOptimizationDialog() = viewModelScope.launch(appDispatcher.IO) {
delay(500)
_state.value = _state.value.copy(showBatteryOptimizationPopup = true)
}

fun dismissBatteryOptimizationDialog() {
_state.value = _state.value.copy(showBatteryOptimizationPopup = false)
}
}

data class HomeScreenState(
Expand All @@ -180,5 +190,6 @@ data class HomeScreenState(
val showSpaceSelectionPopup: Boolean = false,
val locationEnabled: Boolean = true,
val enablingLocation: Boolean = false,
val showBatteryOptimizationPopup: Boolean = false,
val error: Exception? = null
)
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,8 @@
<string name="support_request_sent">Thanks! Your feedback has been recorded.</string>
<string name="setting_spaces">Spaces</string>

<string name="battery_optimization_dialog_title">Turn off Battery Optimization for Location Sharing</string>
<string name="battery_optimization_dialog_message">For location sharing feature to work properly for your Spaces, turn off Battery Optimization in your phone settings for the YourSpace app. This won\'t affect your other apps.</string>
<string name="battery_optimization_dialog_btn_change_now">Change now</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.canopas.yourspace.data.models.user

import androidx.annotation.Keep
import com.google.firebase.firestore.Exclude
import com.squareup.moshi.JsonClass
import java.util.UUID

const val LOGIN_TYPE_GOOGLE = 1
Expand Down Expand Up @@ -31,6 +32,7 @@ data class ApiUser(
}

@Keep
@JsonClass(generateAdapter = true)
data class ApiUserSession(
val id: String = UUID.randomUUID().toString(),
val user_id: String = "",
Expand All @@ -40,6 +42,6 @@ data class ApiUserSession(
val platform: Int = LOGIN_DEVICE_TYPE_ANDROID,
val session_active: Boolean = true,
val app_version: Long? = 0,
val battery_status: String? = "",
val battery_pct: Float? = 0f,
val created_at: Long? = System.currentTimeMillis()
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.canopas.yourspace.data.service.auth

import com.canopas.yourspace.data.models.user.ApiUser
import com.canopas.yourspace.data.models.user.ApiUserSession
import com.canopas.yourspace.data.service.location.LocationManager
import com.canopas.yourspace.data.service.user.ApiUserService
import com.canopas.yourspace.data.storage.UserPreferences
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
Expand All @@ -13,7 +14,8 @@ import javax.inject.Singleton
class AuthService @Inject constructor(
private val userPreferences: UserPreferences,
private val apiUserService: ApiUserService,
private val firebaseAuth: FirebaseAuth
private val firebaseAuth: FirebaseAuth,
private val locationManager: LocationManager
) {
private val authStateChangeListeners = HashSet<AuthStateChangeListener>()

Expand Down Expand Up @@ -99,6 +101,7 @@ class AuthService @Inject constructor(
userPreferences.setOnboardShown(false)
userPreferences.currentSpace = ""
firebaseAuth.signOut()
locationManager.stopService()
}

suspend fun deleteAccount() {
Expand All @@ -109,6 +112,18 @@ class AuthService @Inject constructor(

suspend fun getUser(): ApiUser? = apiUserService.getUser(currentUser?.id ?: "")
suspend fun getUserFlow() = apiUserService.getUserFlow(currentUser?.id ?: "")

suspend fun updateBatteryStatus(batteryPercentage: Float) {
val currentUser = currentUser ?: return
val session = currentUserSession ?: return
apiUserService.updateBatteryPct(currentUser.id, session.id, batteryPercentage)
}

suspend fun updateUserSessionState(state: Int) {
val currentUser = currentUser ?: return
val session = currentUserSession ?: return
apiUserService.updateSessionState(currentUser.id, session.id, state)
}
}

interface AuthStateChangeListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ class LocationManager @Inject constructor(@ApplicationContext private val contex
setWaitForAccurateLocation(true)
}.build()

fun startLocationTracking() {
internal fun startLocationTracking() {
if (context.hasFineLocationPermission) {
locationClient.requestLocationUpdates(request, locationUpdatePendingIntent)
}
}

fun stopLocationTracking() {
internal fun stopLocationTracking() {
if (!context.isBackgroundLocationPermissionGranted) {
locationClient.flushLocations()
locationClient.removeLocationUpdates(locationUpdatePendingIntent)
Expand All @@ -78,4 +78,8 @@ class LocationManager @Inject constructor(@ApplicationContext private val contex
fun startService() {
context.startService(Intent(context, BackgroundLocationService::class.java))
}

fun stopService() {
context.stopService(Intent(context, BackgroundLocationService::class.java))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,22 @@ class ApiUserService @Inject constructor(
suspend fun addSpaceId(userId: String, spaceId: String) {
userRef.document(userId).update("space_ids", FieldValue.arrayUnion(spaceId)).await()
}

suspend fun updateBatteryPct(userId: String, sessionId: String, batteryPct: Float) {
sessionRef(userId).document(sessionId).update(
"battery_pct",
batteryPct,
"updated_at",
FieldValue.serverTimestamp()
).await()
}

suspend fun updateSessionState(id: String, id1: String, state: Int) {
sessionRef(id).document(id1).update(
"user_state",
state,
"updated_at",
FieldValue.serverTimestamp()
).await()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ActivityCompat

Expand Down Expand Up @@ -53,3 +54,13 @@ val Context.hasNotificationPermission
}

val Context.hasAllPermission get() = isLocationPermissionGranted

val Context.isBatteryOptimizationEnabled: Boolean
get() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager.isIgnoringBatteryOptimizations(packageName).not()
} else {
false
}
}

0 comments on commit f50059b

Please sign in to comment.