diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b5d316e1..316a39f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,8 @@ - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/canopas/yourspace/domain/receiver/BatteryBroadcastReceiver.kt b/app/src/main/java/com/canopas/yourspace/domain/receiver/BatteryBroadcastReceiver.kt new file mode 100644 index 00000000..8c174fc4 --- /dev/null +++ b/app/src/main/java/com/canopas/yourspace/domain/receiver/BatteryBroadcastReceiver.kt @@ -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") + } + } + } + } +} diff --git a/app/src/main/java/com/canopas/yourspace/domain/receiver/BootCompleteReceiver.kt b/app/src/main/java/com/canopas/yourspace/domain/receiver/BootCompleteReceiver.kt new file mode 100644 index 00000000..2c1f6e28 --- /dev/null +++ b/app/src/main/java/com/canopas/yourspace/domain/receiver/BootCompleteReceiver.kt @@ -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() + } + } +} diff --git a/app/src/main/java/com/canopas/yourspace/ui/component/BackgroudPermissionCheck.kt b/app/src/main/java/com/canopas/yourspace/ui/component/BackgroudPermissionCheck.kt index bd5320b3..ee15c56b 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/component/BackgroudPermissionCheck.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/component/BackgroudPermissionCheck.kt @@ -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 @@ -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 @@ -100,6 +102,8 @@ fun PermissionDialog( title: String, subTitle1: String, subTitle2: String? = null, + dismissBtn: String? = null, + confirmBtn: String? = null, onDismiss: () -> Unit, goToSettings: () -> Unit ) { @@ -120,7 +124,7 @@ fun PermissionDialog( modifier = Modifier .padding(top = 5.dp) .fillMaxWidth(), - style = AppTheme.appTypography.header1 + style = AppTheme.appTypography.header3 ) Text( text = subTitle1, @@ -128,7 +132,7 @@ fun PermissionDialog( modifier = Modifier .padding(vertical = 10.dp) .fillMaxWidth(), - style = AppTheme.appTypography.body2 + style = AppTheme.appTypography.body1 ) if (subTitle2 != null) { @@ -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 + ) + } } } } diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreen.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreen.kt index 139a7e47..484a1788 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreen.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreen.kt @@ -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 @@ -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 @@ -59,6 +64,12 @@ fun HomeScreen() { } } + LaunchedEffect(Unit) { + if (context.isBatteryOptimizationEnabled) { + viewModel.showBatteryOptimizationDialog() + } + } + Scaffold( containerColor = AppTheme.colorScheme.surface, content = { @@ -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 diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreenViewModel.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreenViewModel.kt index 697276b3..e1ec3924 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreenViewModel.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/home/home/HomeScreenViewModel.kt @@ -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 @@ -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( @@ -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 ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2543d9f2..3b2d8f6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,4 +203,8 @@ Thanks! Your feedback has been recorded. Spaces + Turn off Battery Optimization for Location Sharing + 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. + Change now + \ No newline at end of file diff --git a/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt b/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt index 11aff737..1eb8a0d2 100644 --- a/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt +++ b/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt @@ -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 @@ -31,6 +32,7 @@ data class ApiUser( } @Keep +@JsonClass(generateAdapter = true) data class ApiUserSession( val id: String = UUID.randomUUID().toString(), val user_id: String = "", @@ -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() ) diff --git a/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt b/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt index 8345978b..f3a2ab5b 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt @@ -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 @@ -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() @@ -99,6 +101,7 @@ class AuthService @Inject constructor( userPreferences.setOnboardShown(false) userPreferences.currentSpace = "" firebaseAuth.signOut() + locationManager.stopService() } suspend fun deleteAccount() { @@ -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 { diff --git a/data/src/main/java/com/canopas/yourspace/data/service/location/LocationManager.kt b/data/src/main/java/com/canopas/yourspace/data/service/location/LocationManager.kt index 500ea262..f1d1b223 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/location/LocationManager.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/location/LocationManager.kt @@ -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) @@ -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)) + } } diff --git a/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt b/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt index 37b7c54a..a6c4d43c 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt @@ -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() + } } diff --git a/data/src/main/java/com/canopas/yourspace/data/utils/PermissionExts.kt b/data/src/main/java/com/canopas/yourspace/data/utils/PermissionExts.kt index 66110f1c..78ff19a2 100644 --- a/data/src/main/java/com/canopas/yourspace/data/utils/PermissionExts.kt +++ b/data/src/main/java/com/canopas/yourspace/data/utils/PermissionExts.kt @@ -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 @@ -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 + } + }