From 230dce410c5e4b7726f2899b18835d2d94119ce2 Mon Sep 17 00:00:00 2001 From: Simon Alveteg Date: Sun, 14 Jul 2024 14:27:46 +0200 Subject: [PATCH] Split app into two activities (#110) * Move settings-related screens into second activity #93 * Add drop shadow to appcards * Open google wallpapers by default * Switch to StateFlow * fix bug where usage of favorites didn't update #88 --- .idea/deploymentTargetSelector.xml | 3 + app/src/main/AndroidManifest.xml | 24 +++- .../com/alveteg/simon/minutelauncher/Event.kt | 25 +--- .../alveteg/simon/minutelauncher/NavGraph.kt | 57 ++------- .../data/ApplicationRepository.kt | 27 ++-- .../simon/minutelauncher/home/AppCard.kt | 21 +++- .../simon/minutelauncher/home/FavoriteCard.kt | 19 ++- .../simon/minutelauncher/home/FavoriteList.kt | 41 +++--- .../{MainActivity.kt => home/HomeActivity.kt} | 20 +-- .../simon/minutelauncher/home/HomeEvent.kt | 20 +++ .../simon/minutelauncher/home/HomeScreen.kt | 24 ++-- .../HomeViewModel.kt} | 77 ++++-------- .../simon/minutelauncher/home/ScreenState.kt | 3 +- .../home/dashboard/Dashboard.kt | 17 +-- .../home/dashboard/DashboardActionBar.kt | 30 ++--- .../home/dashboard/DashboardBottomSheet.kt | 1 + .../home/dashboard/SearchBar.kt | 5 +- .../minutelauncher/home/modal/AppModal.kt | 1 + .../home/modal/AppModalActionBar.kt | 7 +- .../home/modal/AppModalBottomSheet.kt | 16 +-- .../home/modal/TimerBottomSheet.kt | 5 +- .../home/stats/UsageBarGraph.kt | 17 +-- .../minutelauncher/settings/GestureList.kt | 8 +- .../minutelauncher/settings/GestureScreen.kt | 13 +- .../settings/SettingsActivity.kt | 37 ++++++ .../minutelauncher/settings/SettingsEvent.kt | 14 +++ .../settings/SettingsViewModel.kt | 117 ++++++++++++++++++ .../minutelauncher/settings/TimerScreen.kt | 14 +-- app/src/main/res/values/themes.xml | 4 + 29 files changed, 396 insertions(+), 271 deletions(-) rename app/src/main/java/com/alveteg/simon/minutelauncher/{MainActivity.kt => home/HomeActivity.kt} (68%) create mode 100644 app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeEvent.kt rename app/src/main/java/com/alveteg/simon/minutelauncher/{data/LauncherViewModel.kt => home/HomeViewModel.kt} (77%) create mode 100644 app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsActivity.kt create mode 100644 app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsEvent.kt create mode 100644 app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsViewModel.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..9067556 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,6 +5,9 @@ + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c169c4e..dfebd3e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,12 @@ - + tools:ignore="ProtectedPermissions" /> @@ -27,22 +28,35 @@ android:theme="@style/Theme.MinuteLauncher" tools:targetApi="31"> + + + + + + android:exported="false" + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/Event.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/Event.kt index 8431550..9824beb 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/Event.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/Event.kt @@ -1,25 +1,4 @@ package com.alveteg.simon.minutelauncher -import com.alveteg.simon.minutelauncher.data.AccessTimer -import com.alveteg.simon.minutelauncher.data.AccessTimerMapping -import com.alveteg.simon.minutelauncher.data.App -import com.alveteg.simon.minutelauncher.data.AppInfo -import com.alveteg.simon.minutelauncher.data.FavoriteAppInfo -import com.alveteg.simon.minutelauncher.utilities.Gesture - - -sealed class Event { - data class OpenApplication(val appInfo: AppInfo) : Event() - data class LaunchActivity(val appInfo: AppInfo) : Event() - data class UpdateSearch(val searchTerm: String) : Event() - data class ToggleFavorite(val app: App) : Event() - data class HandleGesture(val gesture: Gesture) : Event() - data class SetAppGesture(val app: App, val gesture: Gesture) : Event() - data class ClearAppGesture(val gesture: Gesture) : Event() - data class UpdateFavoriteOrder(val favorites: List) : Event() - data class UpdateApp(val app: App) : Event() - data object OpenGestureSettings : Event() - data class OpenGestureList(val gesture: Gesture) : Event() - data object OpenTimerSettings : Event() - data class SetDefaultTimer(val accessTimerMapping: AccessTimerMapping) : Event() -} +interface Event { +} \ No newline at end of file diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/NavGraph.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/NavGraph.kt index 1e3af56..847b316 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/NavGraph.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/NavGraph.kt @@ -1,80 +1,41 @@ package com.alveteg.simon.minutelauncher -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import com.alveteg.simon.minutelauncher.home.HomeScreen import com.alveteg.simon.minutelauncher.settings.GestureList import com.alveteg.simon.minutelauncher.settings.GestureScreen +import com.alveteg.simon.minutelauncher.settings.SettingsScreen import com.alveteg.simon.minutelauncher.settings.TimerScreen import com.alveteg.simon.minutelauncher.utilities.Gesture @Composable fun LauncherNavHost( navController: NavHostController, + startDestination: String ) { - NavHost(navController = navController, - startDestination = MinuteRoute.HOME, - enterTransition = { EnterTransition.None }, - exitTransition = { fadeOut(tween(delayMillis = 2000)) }, - popEnterTransition = { EnterTransition.None }, - popExitTransition = { ExitTransition.None }) { - composable( - route = MinuteRoute.HOME - ) { - HomeScreen(onNavigate = { navController.navigationEvent(event = it) }) - } - composable(route = MinuteRoute.GESTURE_SETTINGS, - enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Up - ) - }, - exitTransition = { fadeOut() }, - popEnterTransition = { fadeIn() }, - popExitTransition = { fadeOut() }) { + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable(route = SettingsScreen.GESTURE_SETTINGS) { GestureScreen(onNavigate = { navController.navigationEvent(event = it) }) } - composable( - route = MinuteRoute.GESTURE_SETTINGS_LIST + "/{gesture}", - enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) } - ) { backStackEntry -> + composable(route = SettingsScreen.GESTURE_SETTINGS_LIST + "/{gesture}") { backStackEntry -> val gesture = backStackEntry.arguments?.getString("gesture") ?: Gesture.NONE.toString() GestureList( onNavigate = { navController.navigationEvent(event = it) }, gesture = Gesture.valueOf(gesture) ) } - composable( - route = MinuteRoute.TIMER_SETTINGS, - enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Up - ) - }, - exitTransition = { fadeOut() }, - popEnterTransition = { fadeIn() }, - popExitTransition = { fadeOut() } - ) { + composable(route = SettingsScreen.TIMER_SETTINGS) { TimerScreen(onNavigate = { navController.navigationEvent(event = it) }) } } } -object MinuteRoute { - const val HOME = "home" - const val GESTURE_SETTINGS = "gesture_settings" - const val GESTURE_SETTINGS_LIST = "gestures_list" - const val TIMER_SETTINGS = "timer_settings" -} fun NavController.navigationEvent(event: UiEvent.Navigate) { navigate(event.route) { diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/data/ApplicationRepository.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/data/ApplicationRepository.kt index 18481fa..b32b157 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/data/ApplicationRepository.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/data/ApplicationRepository.kt @@ -69,33 +69,32 @@ class ApplicationRepository @Inject constructor( } suspend fun startUsageUpdater() { + Timber.d("Starting usage updater.") while (currentCoroutineContext().isActive) { - val usageStats = getDailyStatsForWeek() - - _usageStats.emit(usageStats) + Timber.d("Emitting usage stats.") + _usageStats.emit(getDailyStatsForWeek()) delay(TimeUnit.MINUTES.toMillis(1)) } } private fun getDailyStatsForWeek(): List { val today = LocalDate.now() - val dates = listOf( - today, - today.minusDays(1), - today.minusDays(2), - today.minusDays(3), - today.minusDays(4), - today.minusDays(5), - today.minusDays(6) - ) + val dates = mutableListOf() + repeat(7) { dates += today.minusDays(it.toLong()) } val launcherApps = getLauncherApps() return dates.flatMap { date -> - Timber.d("Getting stats for date: $date") getDailyStats(date) .filter { !launcherApps.contains(it.packageName) } .filter { context.packageName != it.packageName } - }.also { Timber.d("--- ${it.size} packages, ${it.sumOf { it.usageDuration }.toTimeUsed()} ") } + .also { + Timber.d( + "$date: ${it.size} packages, ${ + it.sumOf { it.usageDuration }.toTimeUsed() + }" + ) + } + } } /** diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/AppCard.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/AppCard.kt index ef07444..4ecc18e 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/AppCard.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/AppCard.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -17,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -34,11 +36,14 @@ fun AppCard( onClick: () -> Unit ) { val appTitle = appInfo.app.appTitle - val appUsage by remember { derivedStateOf { appInfo.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration } } + val appUsage by remember(appInfo) { + derivedStateOf { + appInfo.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration + } + } val interactionSource = remember { MutableInteractionSource() } Surface( - tonalElevation = 2.dp, shape = MaterialTheme.shapes.large, color = Color.Transparent, modifier = Modifier @@ -61,6 +66,12 @@ fun AppCard( fontSize = 25.sp, textAlign = TextAlign.Center, overflow = TextOverflow.Clip, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f), + blurRadius = 12f + ) + ), modifier = Modifier .fillMaxWidth() ) @@ -68,6 +79,12 @@ fun AppCard( text = appUsage.toTimeUsed(), fontFamily = archivoFamily, color = MaterialTheme.colorScheme.primary, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f), + blurRadius = 12f + ) + ) ) } } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteCard.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteCard.kt index f238388..7e64a90 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteCard.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteCard.kt @@ -3,11 +3,11 @@ package com.alveteg.simon.minutelauncher.home import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -37,7 +38,9 @@ fun FavoriteCard( ) { val interactionSource = remember { MutableInteractionSource() } val appTitle = appInfo.app.appTitle - val appUsage by remember { derivedStateOf { appInfo.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration } } + val appUsage by remember(appInfo) { + derivedStateOf { appInfo.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration } + } Surface( shape = MaterialTheme.shapes.large, @@ -60,6 +63,12 @@ fun FavoriteCard( fontFamily = archivoBlackFamily, fontSize = 25.sp, textAlign = TextAlign.Center, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f), + blurRadius = 12f + ) + ), overflow = TextOverflow.Clip, modifier = Modifier .fillMaxWidth() @@ -68,6 +77,12 @@ fun FavoriteCard( text = appUsage.toTimeUsed(), fontFamily = archivoFamily, color = MaterialTheme.colorScheme.primary, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f), + blurRadius = 12f + ) + ) ) } } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteList.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteList.kt index 866414a..b532d6d 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteList.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/FavoriteList.kt @@ -19,10 +19,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,10 +31,11 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.IntOffset import com.alveteg.simon.minutelauncher.Event import com.alveteg.simon.minutelauncher.data.AppInfo import com.alveteg.simon.minutelauncher.data.FavoriteAppInfo @@ -46,7 +47,6 @@ import com.alveteg.simon.minutelauncher.utilities.GestureZone import com.alveteg.simon.minutelauncher.utilities.toTimeUsed import kotlinx.coroutines.launch import timber.log.Timber -import java.time.LocalDate import kotlin.math.abs @Suppress("NAME_SHADOWING") @@ -76,20 +76,13 @@ fun FavoriteList( label = "", animationSpec = if (screenState.isFavorites()) slowFloatSpec else tween(300) ) - val usageAlpha by animateFloatAsState( - targetValue = if (screenState.isFavorites()) 1f else 0f, - label = "", - animationSpec = if (!screenState.isFavorites()) tween(500) else tween( - durationMillis = 500, delayMillis = 600 - ) - ) Column(modifier = Modifier .fillMaxSize() .graphicsLayer { alpha = favoritesAlpha } - .offset(y = offsetY.value.dp) + .offset { IntOffset(x = 0, y = offsetY.value.toInt()) } .pointerInput(Unit) { detectHorizontalDragGestures( onDragCancel = { @@ -118,7 +111,7 @@ fun FavoriteList( } currentZone = GestureZone.NONE currentDirection = GestureDirection.NONE - onEvent(Event.HandleGesture(gesture)) + onEvent(HomeEvent.HandleGesture(gesture)) }, ) { change, dragAmount -> currentDirection = if (dragAmount > 0) { @@ -156,7 +149,7 @@ fun FavoriteList( onDragCancel = { onDragEnd() }, onDragEnd = { onDragEnd() - onEvent(Event.HandleGesture(gesture)) + onEvent(HomeEvent.HandleGesture(gesture)) }, ) { _, dragAmount -> val originalY = offsetY.value @@ -167,7 +160,6 @@ fun FavoriteList( coroutineScope.launch { offsetY.snapTo(originalY + easedDragAmount) } - Timber.d("EasingFactor: $easingFactor") gesture = if (easingFactor < 0.14) { if (offsetY.value > 0) Gesture.DOWN else Gesture.UP } else Gesture.NONE @@ -178,15 +170,16 @@ fun FavoriteList( ) { Text( text = totalUsage.toTimeUsed(), - color = LocalContentColor.current.copy(alpha = usageAlpha), - fontFamily = archivoFamily + color = LocalContentColor.current, + fontFamily = archivoFamily, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f), + blurRadius = 12f + ) + ) ) - val data = remember { mutableStateOf(favorites) } - LaunchedEffect(favorites) { - if (favorites.size != data.value.size) { - data.value = favorites - } - } + val listState = rememberLazyListState() LazyColumn( state = listState, @@ -194,7 +187,7 @@ fun FavoriteList( userScrollEnabled = false, modifier = Modifier.fillMaxWidth() ) { - items(data.value, { it.favoriteApp.app.packageName }) { favoriteAppInfo -> + items(favorites) { favoriteAppInfo -> FavoriteCard(favoriteAppInfo.toAppInfo()) { onAppClick(favoriteAppInfo.toAppInfo()) } } } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/MainActivity.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeActivity.kt similarity index 68% rename from app/src/main/java/com/alveteg/simon/minutelauncher/MainActivity.kt rename to app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeActivity.kt index a1cad34..a1cf527 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/MainActivity.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeActivity.kt @@ -1,4 +1,4 @@ -package com.alveteg.simon.minutelauncher +package com.alveteg.simon.minutelauncher.home import android.app.AppOpsManager import android.content.Context @@ -11,25 +11,31 @@ import androidx.activity.compose.setContent import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowCompat -import androidx.navigation.compose.rememberNavController +import com.alveteg.simon.minutelauncher.settings.SettingsActivity import com.alveteg.simon.minutelauncher.theme.MinuteLauncherTheme import dagger.hilt.android.AndroidEntryPoint -@ExperimentalMaterial3Api @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class HomeActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MinuteLauncherTheme { - val navController = rememberNavController() if (!isAccessGranted(LocalContext.current)) { // TODO: open dialog informing user about permission before opening settings - startActivity(Intent().apply { action = Settings.ACTION_USAGE_ACCESS_SETTINGS }) + startActivity(Intent().apply { + action = Settings.ACTION_USAGE_ACCESS_SETTINGS + flags += Intent.FLAG_ACTIVITY_NEW_TASK + }) } WindowCompat.setDecorFitsSystemWindows(window, false) - LauncherNavHost(navController) + HomeScreen(onNavigate = { + val intent = Intent(this, SettingsActivity::class.java) + intent.putExtra("screen", it.route) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + }) } } } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeEvent.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeEvent.kt new file mode 100644 index 0000000..0d9f5c3 --- /dev/null +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeEvent.kt @@ -0,0 +1,20 @@ +package com.alveteg.simon.minutelauncher.home + +import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.data.App +import com.alveteg.simon.minutelauncher.data.AppInfo +import com.alveteg.simon.minutelauncher.data.FavoriteAppInfo +import com.alveteg.simon.minutelauncher.utilities.Gesture + + +sealed class HomeEvent : Event { + data class OpenApplication(val appInfo: AppInfo) : HomeEvent() + data class LaunchActivity(val appInfo: AppInfo) : HomeEvent() + data class UpdateSearch(val searchTerm: String) : HomeEvent() + data class ToggleFavorite(val app: App) : HomeEvent() + data class HandleGesture(val gesture: Gesture) : HomeEvent() + data class UpdateFavoriteOrder(val favorites: List) : HomeEvent() + data class UpdateApp(val app: App) : HomeEvent() + data object OpenGestureSettings : HomeEvent() + data object OpenTimerSettings : HomeEvent() +} diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeScreen.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeScreen.kt index ceb77f4..522c7f1 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeScreen.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeScreen.kt @@ -29,12 +29,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.util.fastSumBy import androidx.hilt.navigation.compose.hiltViewModel -import com.alveteg.simon.minutelauncher.Event import com.alveteg.simon.minutelauncher.UiEvent import com.alveteg.simon.minutelauncher.data.AppInfo -import com.alveteg.simon.minutelauncher.data.LauncherViewModel import com.alveteg.simon.minutelauncher.home.dashboard.Dashboard import com.alveteg.simon.minutelauncher.home.modal.AppModalBottomSheet import timber.log.Timber @@ -44,17 +41,21 @@ import java.time.LocalDate @Composable fun HomeScreen( onNavigate: (UiEvent.Navigate) -> Unit, - viewModel: LauncherViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel() ) { var screenState by rememberSaveable { mutableStateOf(ScreenState.FAVORITES) } val searchText by viewModel.searchTerm.collectAsState() - val apps by viewModel.filteredApps.collectAsState(initial = emptyList()) - val installedApps by viewModel.installedApps.collectAsState(initial = emptyList()) - val timerMappings by viewModel.accessTimerMappings.collectAsState(initial = emptyList()) - val totalUsage by derivedStateOf { - installedApps.sumOf { it.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration ?: 0L } + val apps by viewModel.filteredApps.collectAsState() + val installedApps by viewModel.installedApps.collectAsState() + val timerMappings by viewModel.accessTimerMappings.collectAsState() + val totalUsage by remember { + derivedStateOf { + installedApps.sumOf { + it.usage.firstOrNull { it.usageDate == LocalDate.now() }?.usageDuration ?: 0L + } + } } - val favorites by viewModel.favoriteApps.collectAsState(initial = emptyList()) + val favorites by viewModel.favoriteApps.collectAsState() val mContext = LocalContext.current val hapticFeedback = LocalHapticFeedback.current @@ -68,7 +69,6 @@ fun HomeScreen( ScreenState.FAVORITES -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) ScreenState.DASHBOARD -> MaterialTheme.colorScheme.surface.copy(alpha = 0.85f) ScreenState.APPS -> MaterialTheme.colorScheme.surface.copy(alpha = 0.85f) - ScreenState.SELECTOR -> MaterialTheme.colorScheme.surface.copy(alpha = 0.97f) }, label = "" ) @@ -114,7 +114,7 @@ fun HomeScreen( val offsetY = remember { Animatable(0f) } val keyboardController = LocalSoftwareKeyboardController.current val appListSelectionAction: (AppInfo) -> Unit = { - viewModel.onEvent(Event.OpenApplication(it)) + viewModel.onEvent(HomeEvent.OpenApplication(it)) keyboardController?.hide() } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/data/LauncherViewModel.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeViewModel.kt similarity index 77% rename from app/src/main/java/com/alveteg/simon/minutelauncher/data/LauncherViewModel.kt rename to app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeViewModel.kt index 74c729c..a01ffc9 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/data/LauncherViewModel.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/HomeViewModel.kt @@ -1,12 +1,17 @@ -package com.alveteg.simon.minutelauncher.data +package com.alveteg.simon.minutelauncher.home import android.content.pm.LauncherApps import android.os.UserHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alveteg.simon.minutelauncher.Event -import com.alveteg.simon.minutelauncher.MinuteRoute import com.alveteg.simon.minutelauncher.UiEvent +import com.alveteg.simon.minutelauncher.data.App +import com.alveteg.simon.minutelauncher.data.AppInfo +import com.alveteg.simon.minutelauncher.data.ApplicationRepository +import com.alveteg.simon.minutelauncher.data.FavoriteAppInfo +import com.alveteg.simon.minutelauncher.data.LauncherRepository +import com.alveteg.simon.minutelauncher.settings.SettingsScreen import com.alveteg.simon.minutelauncher.utilities.Gesture import com.alveteg.simon.minutelauncher.utilities.filterBySearchTerm import com.alveteg.simon.minutelauncher.utilities.toTimeUsed @@ -15,19 +20,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @HiltViewModel -class LauncherViewModel @Inject constructor( +class HomeViewModel @Inject constructor( private val roomRepository: LauncherRepository, private val applicationRepository: ApplicationRepository ) : ViewModel() { @@ -38,7 +44,6 @@ class LauncherViewModel @Inject constructor( private val _searchTerm = MutableStateFlow("") val searchTerm = _searchTerm.asStateFlow() - val gestureApps = roomRepository.gestureApps() val favoriteApps = combine( roomRepository.favoriteApps(), applicationRepository.usageStats ) { favorites, usageStats -> @@ -46,7 +51,8 @@ class LauncherViewModel @Inject constructor( val usage = usageStats.filter { favorite.app.packageName == it.packageName } FavoriteAppInfo(favorite, usage) } - } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + val installedApps = combine( roomRepository.appList(), roomRepository.favoriteApps(), applicationRepository.usageStats ) { apps, favorites, usageStats -> @@ -55,22 +61,16 @@ class LauncherViewModel @Inject constructor( val usage = usageStats.filter { app.packageName == it.packageName } AppInfo(app, favorite, usage) } - } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + val filteredApps = combine( installedApps, searchTerm ) { apps, searchTerm -> apps.filterBySearchTerm(searchTerm) - } - val defaultTimerApps = installedApps.transform { appList -> - emit(appList.filter { it.app.timer == AccessTimer.DEFAULT } - .sortedBy { it.app.appTitle.lowercase() }) - } - val nonDefaultTimerApps = installedApps.transform { appList -> - emit(appList.filter { it.app.timer != AccessTimer.DEFAULT } - .sortedBy { it.app.appTitle.lowercase() }) - } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) val accessTimerMappings = roomRepository.getAccessTimerMappings() + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) private val packageCallback = object : LauncherApps.Callback() { override fun onPackageRemoved(packageName: String?, user: UserHandle?) { @@ -100,6 +100,7 @@ class LauncherViewModel @Inject constructor( } init { + Timber.d("ViewModel initialized!") applicationRepository.registerCallback(packageCallback) updateDatabase() viewModelScope.launch { @@ -131,12 +132,12 @@ class LauncherViewModel @Inject constructor( fun onEvent(event: Event) { Timber.d(event.toString()) when (event) { - is Event.OpenApplication -> { + is HomeEvent.OpenApplication -> { sendUiEvent(UiEvent.ShowModal(event.appInfo)) sendUiEvent(UiEvent.VibrateLongPress) } - is Event.LaunchActivity -> { + is HomeEvent.LaunchActivity -> { val appInfo = event.appInfo Timber.d("Launch Activity ${appInfo.app.appTitle}") applicationRepository.getLaunchIntentForPackage(appInfo.app.packageName)?.let { intent -> @@ -155,12 +156,12 @@ class LauncherViewModel @Inject constructor( } } - is Event.UpdateSearch -> { + is HomeEvent.UpdateSearch -> { Timber.d("Update search with ${event.searchTerm}") _searchTerm.value = event.searchTerm } - is Event.ToggleFavorite -> { + is HomeEvent.ToggleFavorite -> { viewModelScope.launch { withContext(Dispatchers.IO) { Timber.d("Toggle favorite app ${event.app.appTitle}") @@ -169,7 +170,7 @@ class LauncherViewModel @Inject constructor( } } - is Event.HandleGesture -> { + is HomeEvent.HandleGesture -> { val gesture = event.gesture Timber.d("Gesture handled, $gesture") when (gesture) { @@ -198,35 +199,10 @@ class LauncherViewModel @Inject constructor( } } - is Event.SetAppGesture -> { - viewModelScope.launch { - withContext(Dispatchers.IO) { - roomRepository.insertGestureApp(SwipeApp(event.gesture, event.app)) - } - } - sendUiEvent(UiEvent.Navigate(route = MinuteRoute.GESTURE_SETTINGS, popBackStack = true)) - } - - is Event.OpenGestureList -> sendUiEvent(UiEvent.Navigate(MinuteRoute.GESTURE_SETTINGS_LIST + "/${event.gesture}")) - is Event.OpenGestureSettings -> sendUiEvent(UiEvent.Navigate(MinuteRoute.GESTURE_SETTINGS)) - is Event.OpenTimerSettings -> sendUiEvent(UiEvent.Navigate(MinuteRoute.TIMER_SETTINGS)) - is Event.SetDefaultTimer -> { - viewModelScope.launch { - withContext(Dispatchers.IO) { - roomRepository.setAccessTimerMapping(event.accessTimerMapping) - } - } - } - - is Event.ClearAppGesture -> { - viewModelScope.launch { - withContext(Dispatchers.IO) { - roomRepository.removeAppForGesture(event.gesture) - } - } - } - is Event.UpdateFavoriteOrder -> { + is HomeEvent.OpenGestureSettings -> sendUiEvent(UiEvent.Navigate(SettingsScreen.GESTURE_SETTINGS)) + is HomeEvent.OpenTimerSettings -> sendUiEvent(UiEvent.Navigate(SettingsScreen.TIMER_SETTINGS)) + is HomeEvent.UpdateFavoriteOrder -> { viewModelScope.launch { withContext(Dispatchers.IO) { roomRepository.updateFavoritesOrder(event.favorites) @@ -234,7 +210,7 @@ class LauncherViewModel @Inject constructor( } } - is Event.UpdateApp -> { + is HomeEvent.UpdateApp -> { viewModelScope.launch { withContext(Dispatchers.IO) { roomRepository.updateApp(event.app) @@ -255,6 +231,7 @@ class LauncherViewModel @Inject constructor( } override fun onCleared() { + Timber.d("HomeViewModel Cleared.") applicationRepository.unregisterCallback() super.onCleared() } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/ScreenState.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/ScreenState.kt index 410c20d..4f92648 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/ScreenState.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/ScreenState.kt @@ -1,9 +1,8 @@ package com.alveteg.simon.minutelauncher.home enum class ScreenState { - FAVORITES, DASHBOARD, APPS, SELECTOR; + FAVORITES, DASHBOARD, APPS; fun isFavorites() = this == FAVORITES - fun isSelector() = this == SELECTOR fun isDashboard() = this == DASHBOARD } \ No newline at end of file diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/Dashboard.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/Dashboard.kt index ef5f003..deb70e2 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/Dashboard.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/Dashboard.kt @@ -6,16 +6,6 @@ import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api @@ -28,17 +18,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import com.alveteg.simon.minutelauncher.Event -import com.alveteg.simon.minutelauncher.data.App +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.data.AppInfo import com.alveteg.simon.minutelauncher.data.UsageStatistics -import com.alveteg.simon.minutelauncher.home.AppCard import com.alveteg.simon.minutelauncher.home.ScreenState -import com.alveteg.simon.minutelauncher.utilities.Gesture import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -65,7 +52,7 @@ fun Dashboard( } DisposableEffect(Unit) { onDispose { - onEvent(Event.UpdateSearch("")) + onEvent(HomeEvent.UpdateSearch("")) } } var searchHeight by remember { mutableStateOf(0.dp) } diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardActionBar.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardActionBar.kt index 6efdd82..2e853ea 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardActionBar.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardActionBar.kt @@ -8,7 +8,6 @@ import androidx.compose.material.icons.filled.Feedback import androidx.compose.material.icons.filled.Gesture import androidx.compose.material.icons.filled.RestartAlt import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Timer import androidx.compose.material.icons.filled.Wallpaper import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Timer @@ -18,9 +17,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.core.content.ContextCompat import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.R import com.alveteg.simon.minutelauncher.home.ActionBar import com.alveteg.simon.minutelauncher.home.ActionBarAction +import com.alveteg.simon.minutelauncher.home.HomeActivity @Composable @@ -40,17 +41,19 @@ fun DashboardActionBar( ActionBarAction( imageVector = Icons.Default.Gesture, description = "Change gesture shortcuts", - action = { onEvent(Event.OpenGestureSettings) } + action = { onEvent(HomeEvent.OpenGestureSettings) } ), ActionBarAction( imageVector = ImageVector.vectorResource(id = R.drawable.digital_wellbeing), description = "Open Digital Wellbeing", action = { - val intent = Intent() - intent.setClassName( - "com.google.android.apps.wellbeing", - "com.google.android.apps.wellbeing.settings.TopLevelSettingsActivity" - ) + val intent = Intent().apply { + setClassName( + "com.google.android.apps.wellbeing", + "com.google.android.apps.wellbeing.settings.TopLevelSettingsActivity" + ) + flags += Intent.FLAG_ACTIVITY_NEW_TASK + } ContextCompat.startActivity(mContext, intent, null) } ), @@ -58,7 +61,10 @@ fun DashboardActionBar( imageVector = Icons.Default.Wallpaper, description = "Change Wallpaper", action = { - val intent = Intent(Intent.ACTION_SET_WALLPAPER) + val intent = Intent(Intent.ACTION_SET_WALLPAPER).apply { + setPackage("com.google.android.apps.wallpaper") + flags += Intent.FLAG_ACTIVITY_NEW_TASK + } ContextCompat.startActivity( mContext, Intent.createChooser(intent, "Select Wallpaper"), @@ -69,24 +75,20 @@ fun DashboardActionBar( ActionBarAction( imageVector = Icons.Outlined.Timer, description = "Change Timer Settings", - action = { onEvent(Event.OpenTimerSettings) } + action = { onEvent(HomeEvent.OpenTimerSettings) } ), ActionBarAction( imageVector = Icons.Outlined.Info, description = "Open App Info", action = { val intent = Intent().apply { + flags += Intent.FLAG_ACTIVITY_NEW_TASK action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS data = Uri.fromParts("package", mContext.packageName, null) } ContextCompat.startActivity(mContext, intent, null) } ), - ActionBarAction( - imageVector = Icons.Default.RestartAlt, - description = "Restart Minute Launcher", - action = {} - ), ActionBarAction( imageVector = Icons.Default.Feedback, description = "Send Feedback", diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardBottomSheet.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardBottomSheet.kt index e981816..5b2471d 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardBottomSheet.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/DashboardBottomSheet.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.data.UsageStatistics import com.alveteg.simon.minutelauncher.home.stats.UsageBarGraph import com.alveteg.simon.minutelauncher.home.stats.UsageCard diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/SearchBar.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/SearchBar.kt index 9fc9af3..7f63c3a 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/SearchBar.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/dashboard/SearchBar.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.utilities.clearFocusOnKeyboardDismiss @Composable @@ -57,7 +58,7 @@ fun SearchBar( ) }) { TextField(value = searchText, - onValueChange = { onEvent(Event.UpdateSearch(it)) }, + onValueChange = { onEvent(HomeEvent.UpdateSearch(it)) }, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) @@ -90,7 +91,7 @@ fun SearchBar( }, trailingIcon = { IconButton(onClick = { - onEvent(Event.UpdateSearch("")) + onEvent(HomeEvent.UpdateSearch("")) } ) { val tint = if (searchText.isNotBlank()) LocalContentColor.current else Color.Transparent diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModal.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModal.kt index 0026119..022d0c1 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModal.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModal.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextMotion import androidx.compose.ui.unit.dp import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.data.AccessTimer import com.alveteg.simon.minutelauncher.data.AccessTimerMapping import com.alveteg.simon.minutelauncher.data.AppInfo diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalActionBar.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalActionBar.kt index 310c1e8..393dbc9 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalActionBar.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalActionBar.kt @@ -8,7 +8,6 @@ import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.StarBorder import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material.icons.outlined.HourglassEmpty import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Timer import androidx.compose.runtime.Composable @@ -17,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.core.content.ContextCompat import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.R import com.alveteg.simon.minutelauncher.data.AppInfo import com.alveteg.simon.minutelauncher.home.ActionBar @@ -40,6 +40,7 @@ fun AppModalActionBar( action = { val intent = Intent().apply { action = Intent.ACTION_DELETE + flags += Intent.FLAG_ACTIVITY_NEW_TASK data = Uri.fromParts("package", appInfo.app.packageName, null) } ContextCompat.startActivity(mContext, intent, null) @@ -57,6 +58,7 @@ fun AppModalActionBar( action = { val intent = Intent().apply { action = Settings.ACTION_APP_USAGE_SETTINGS + flags += Intent.FLAG_ACTIVITY_NEW_TASK putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.app.packageName) } ContextCompat.startActivity(mContext, intent, null) @@ -65,7 +67,7 @@ fun AppModalActionBar( ActionBarAction( imageVector = favoriteIcon, description = "Toggle app favorite", - action = { onEvent(Event.ToggleFavorite(appInfo.app)) }, + action = { onEvent(HomeEvent.ToggleFavorite(appInfo.app)) }, ), ActionBarAction( imageVector = Icons.Outlined.Info, @@ -73,6 +75,7 @@ fun AppModalActionBar( action = { val intent = Intent().apply { action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + flags += Intent.FLAG_ACTIVITY_NEW_TASK data = Uri.fromParts("package", appInfo.app.packageName, null) } ContextCompat.startActivity(mContext, intent, null) diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalBottomSheet.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalBottomSheet.kt index 8c67ff4..cceb3a4 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalBottomSheet.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/AppModalBottomSheet.kt @@ -3,17 +3,11 @@ package com.alveteg.simon.minutelauncher.home.modal import android.content.Intent import android.provider.Settings import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -21,22 +15,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.MinuteAccessibilityService -import com.alveteg.simon.minutelauncher.data.AccessTimer import com.alveteg.simon.minutelauncher.data.AccessTimerMapping import com.alveteg.simon.minutelauncher.data.AppInfo -import com.alveteg.simon.minutelauncher.home.MinuteBottomSheet -import com.alveteg.simon.minutelauncher.home.SegmentedControl import com.alveteg.simon.minutelauncher.isAccessibilityServiceEnabled -import com.alveteg.simon.minutelauncher.theme.archivoBlackFamily -import com.alveteg.simon.minutelauncher.theme.archivoFamily import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -69,7 +57,7 @@ fun AppModalBottomSheet( timerMapping = timerMappings, onEvent = onEvent, onConfirmation = { - onEvent(Event.LaunchActivity(appInfo)) + onEvent(HomeEvent.LaunchActivity(appInfo)) onDismiss() }, onCancel = { diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/TimerBottomSheet.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/TimerBottomSheet.kt index 6e3e32b..a6286c3 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/TimerBottomSheet.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/modal/TimerBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.data.AccessTimer import com.alveteg.simon.minutelauncher.data.AccessTimerMapping import com.alveteg.simon.minutelauncher.data.AppInfo @@ -74,7 +75,7 @@ fun TimerBottomSheet( selectedItem = selectedTimer, onItemSelection = { selected -> onEvent( - Event.UpdateApp( + HomeEvent.UpdateApp( appInfo.app.copy(timer = timerMappings.first { it.integerValue == selected }.enum) ) ) @@ -83,7 +84,7 @@ fun TimerBottomSheet( TextButton( enabled = !hasDefaultTimer, onClick = { - onEvent(Event.UpdateApp(appInfo.app.copy(timer = AccessTimer.DEFAULT))) + onEvent(HomeEvent.UpdateApp(appInfo.app.copy(timer = AccessTimer.DEFAULT))) } ) { val resetText = "Reset to default" diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/home/stats/UsageBarGraph.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/home/stats/UsageBarGraph.kt index 0581bf5..da55648 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/home/stats/UsageBarGraph.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/home/stats/UsageBarGraph.kt @@ -39,19 +39,14 @@ import com.patrykandpatrick.vico.core.cartesian.axis.AxisItemPlacer import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer -import com.patrykandpatrick.vico.core.cartesian.data.ColumnCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer -import com.patrykandpatrick.vico.core.common.Defaults -import com.patrykandpatrick.vico.core.common.half import com.patrykandpatrick.vico.core.common.shape.Shape import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import timber.log.Timber import java.time.LocalDate import java.time.format.DateTimeFormatter import kotlin.math.max @@ -72,7 +67,7 @@ fun UsageBarGraph( } else if (tenners > 0) { tenners.plus(2).coerceAtMost(5).times(millisInHour.div(6)) } else { - twos.plus(2).coerceIn(2, 4).times(millisInHour.div(30)) + twos.plus(1).coerceIn(1, 4).times(millisInHour.div(30)) } val style = MaterialTheme.typography.bodySmall @@ -95,7 +90,6 @@ fun UsageBarGraph( val dates = sortedStats.map { it.usageDate.toEpochDay() - LocalDate.now().toEpochDay() + 7 } val durations = sortedStats.map { it.usageDuration } - Timber.d("$dates, ${durations.map { it.toTimeUsed(false) }}") series(y = durations, x = dates) } } @@ -199,9 +193,9 @@ class VerticalPlacer( ): Float = when (verticalLabelPosition) { VerticalAxis.VerticalLabelPosition.Top -> maxLineThickness VerticalAxis.VerticalLabelPosition.Center -> - (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).half + (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).div(2) - else -> maxLabelHeight + maxLineThickness.half + else -> maxLabelHeight + maxLineThickness.div(2) } override fun getHeightMeasurementLabelValues( @@ -223,12 +217,12 @@ class VerticalPlacer( maxLineThickness: Float ): Float = when (verticalLabelPosition) { VerticalAxis.VerticalLabelPosition.Top -> - maxLabelHeight + (if (shiftTopLines) maxLineThickness else -maxLineThickness).half + maxLabelHeight + (if (shiftTopLines) maxLineThickness else -maxLineThickness).div(2) VerticalAxis.VerticalLabelPosition.Center -> (max(maxLabelHeight, maxLineThickness) + if (shiftTopLines) maxLineThickness else -maxLineThickness) - .half + .div(2) else -> if (shiftTopLines) maxLineThickness else 0f } @@ -257,7 +251,6 @@ class VerticalPlacer( val twos = maxY.div(TWO_MINUTES).toInt() + 1 repeat(twos) { values += 0f.plus(TWO_MINUTES).times(it) } } - Timber.d("Y Axis: $values") return values } } \ No newline at end of file diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureList.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureList.kt index cb6c8bc..6d57474 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureList.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureList.kt @@ -17,9 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.alveteg.simon.minutelauncher.Event import com.alveteg.simon.minutelauncher.UiEvent -import com.alveteg.simon.minutelauncher.data.LauncherViewModel import com.alveteg.simon.minutelauncher.home.dashboard.AppList import com.alveteg.simon.minutelauncher.home.dashboard.SearchBar import com.alveteg.simon.minutelauncher.utilities.Gesture @@ -28,7 +26,7 @@ import timber.log.Timber @Composable fun GestureList( onNavigate: (UiEvent.Navigate) -> Unit, - viewModel: LauncherViewModel = hiltViewModel(), + viewModel: SettingsViewModel = hiltViewModel(), gesture: Gesture ) { @@ -59,14 +57,14 @@ fun GestureList( ) { AppList( apps = apps, - onAppClick = { viewModel.onEvent(Event.SetAppGesture(it.app, gesture)) }, + onAppClick = { viewModel.onEvent(SettingsEvent.SetAppGesture(it.app, gesture)) }, searchHeight = searchHeight ) SearchBar( searchText = searchText, onSearch = { apps.firstOrNull()?.let { - viewModel.onEvent(Event.SetAppGesture(it.app, gesture)) + viewModel.onEvent(SettingsEvent.SetAppGesture(it.app, gesture)) } }, topPadding = 0.dp, diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureScreen.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureScreen.kt index 66c2f64..9731675 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureScreen.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/GestureScreen.kt @@ -31,11 +31,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.alveteg.simon.minutelauncher.Event -import com.alveteg.simon.minutelauncher.MinuteRoute import com.alveteg.simon.minutelauncher.UiEvent import com.alveteg.simon.minutelauncher.data.App -import com.alveteg.simon.minutelauncher.data.LauncherViewModel import com.alveteg.simon.minutelauncher.theme.archivoBlackFamily import com.alveteg.simon.minutelauncher.theme.archivoFamily import com.alveteg.simon.minutelauncher.utilities.Gesture @@ -45,7 +42,7 @@ import timber.log.Timber @Composable fun GestureScreen( onNavigate: (UiEvent.Navigate) -> Unit, - viewModel: LauncherViewModel = hiltViewModel() + viewModel: SettingsViewModel = hiltViewModel() ) { LaunchedEffect(key1 = true) { Timber.d("launched effect") @@ -75,7 +72,7 @@ fun GestureScreen( ) }, navigationIcon = { - IconButton(onClick = { onNavigate(UiEvent.Navigate(MinuteRoute.HOME, true)) }) { + IconButton(onClick = { onNavigate(UiEvent.Navigate(SettingsScreen.HOME, true)) }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Navigate Back" @@ -147,7 +144,7 @@ fun GestureEntry( name: String, gesture: Gesture, app: App?, - onEvent: (Event) -> Unit + onEvent: (SettingsEvent) -> Unit ) { val appTitle = app?.appTitle ?: "No app selected" Surface( @@ -171,7 +168,7 @@ fun GestureEntry( ) { FilledTonalButton( onClick = { - onEvent(Event.OpenGestureList(gesture)) + onEvent(SettingsEvent.OpenGestureList(gesture)) }, modifier = Modifier.weight(1f), shape = MaterialTheme.shapes.medium @@ -180,7 +177,7 @@ fun GestureEntry( } IconButton( onClick = { - onEvent(Event.ClearAppGesture(gesture)) + onEvent(SettingsEvent.ClearAppGesture(gesture)) }, enabled = app != null, modifier = Modifier.wrapContentWidth() diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsActivity.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsActivity.kt new file mode 100644 index 0000000..146e957 --- /dev/null +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsActivity.kt @@ -0,0 +1,37 @@ +package com.alveteg.simon.minutelauncher.settings + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat +import androidx.navigation.compose.rememberNavController +import com.alveteg.simon.minutelauncher.LauncherNavHost +import com.alveteg.simon.minutelauncher.theme.MinuteLauncherTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MinuteLauncherTheme { + WindowCompat.setDecorFitsSystemWindows(window, false) + val destination = intent.getStringExtra("screen") + if (destination == null) { + Toast.makeText(this, "Destination missing.", Toast.LENGTH_SHORT).show() + return@MinuteLauncherTheme + } + LauncherNavHost(navController = rememberNavController(), startDestination = destination) + } + } + } +} + +object SettingsScreen { + const val HOME = "home" + const val GESTURE_SETTINGS = "gesture_settings" + const val GESTURE_SETTINGS_LIST = "gestures_list" + const val TIMER_SETTINGS = "timer_settings" +} diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsEvent.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsEvent.kt new file mode 100644 index 0000000..ae73f52 --- /dev/null +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsEvent.kt @@ -0,0 +1,14 @@ +package com.alveteg.simon.minutelauncher.settings + +import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.data.AccessTimerMapping +import com.alveteg.simon.minutelauncher.data.App +import com.alveteg.simon.minutelauncher.utilities.Gesture + +sealed class SettingsEvent : Event { + + data class SetDefaultTimer(val accessTimerMapping: AccessTimerMapping) : SettingsEvent() + data class ClearAppGesture(val gesture: Gesture) : SettingsEvent() + data class SetAppGesture(val app: App, val gesture: Gesture) : SettingsEvent() + data class OpenGestureList(val gesture: Gesture) : SettingsEvent() +} diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsViewModel.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsViewModel.kt new file mode 100644 index 0000000..6a5c5f0 --- /dev/null +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/SettingsViewModel.kt @@ -0,0 +1,117 @@ +package com.alveteg.simon.minutelauncher.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alveteg.simon.minutelauncher.Event +import com.alveteg.simon.minutelauncher.UiEvent +import com.alveteg.simon.minutelauncher.data.AccessTimer +import com.alveteg.simon.minutelauncher.data.AppInfo +import com.alveteg.simon.minutelauncher.data.ApplicationRepository +import com.alveteg.simon.minutelauncher.data.LauncherRepository +import com.alveteg.simon.minutelauncher.data.SwipeApp +import com.alveteg.simon.minutelauncher.home.HomeEvent +import com.alveteg.simon.minutelauncher.utilities.filterBySearchTerm +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val roomRepository: LauncherRepository, + private val applicationRepository: ApplicationRepository +) : ViewModel() { + + private val _searchTerm = MutableStateFlow("") + val searchTerm = _searchTerm.asStateFlow() + + val gestureApps = roomRepository.gestureApps() + + val accessTimerMappings = roomRepository.getAccessTimerMappings() + + val installedApps = combine( + roomRepository.appList(), roomRepository.favoriteApps(), applicationRepository.usageStats + ) { apps, favorites, usageStats -> + apps.map { app -> + val favorite = favorites.map { it.app.packageName }.contains(app.packageName) + val usage = usageStats.filter { app.packageName == it.packageName } + AppInfo(app, favorite, usage) + } + } + val filteredApps = combine( + installedApps, searchTerm + ) { apps, searchTerm -> + apps.filterBySearchTerm(searchTerm) + } + val defaultTimerApps = installedApps.transform { appList -> + emit(appList.filter { it.app.timer == AccessTimer.DEFAULT } + .sortedBy { it.app.appTitle.lowercase() }) + } + val nonDefaultTimerApps = installedApps.transform { appList -> + emit(appList.filter { it.app.timer != AccessTimer.DEFAULT } + .sortedBy { it.app.appTitle.lowercase() }) + } + private val _uiEvent = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow().onEach { Timber.d(it.toString()) } + + private fun sendUiEvent(event: UiEvent) { + viewModelScope.launch { + _uiEvent.emit(event) + } + } + + fun onEvent(event: Event) { + Timber.d(event.toString()) + when (event) { + is HomeEvent.UpdateApp -> { + viewModelScope.launch { + withContext(Dispatchers.IO) { + roomRepository.updateApp(event.app) + } + } + } + + is HomeEvent.UpdateSearch -> { + Timber.d("Update search with ${event.searchTerm}") + _searchTerm.value = event.searchTerm + } + + is SettingsEvent.ClearAppGesture -> { + viewModelScope.launch { + withContext(Dispatchers.IO) { + roomRepository.removeAppForGesture(event.gesture) + } + } + } + + is SettingsEvent.SetDefaultTimer -> { + viewModelScope.launch { + withContext(Dispatchers.IO) { + roomRepository.setAccessTimerMapping(event.accessTimerMapping) + } + } + } + + is SettingsEvent.SetAppGesture -> { + viewModelScope.launch { + withContext(Dispatchers.IO) { + roomRepository.insertGestureApp(SwipeApp(event.gesture, event.app)) + } + } + sendUiEvent(UiEvent.Navigate(route = SettingsScreen.GESTURE_SETTINGS, popBackStack = true)) + } + + is SettingsEvent.OpenGestureList -> sendUiEvent(UiEvent.Navigate(SettingsScreen.GESTURE_SETTINGS_LIST + "/${event.gesture}")) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/TimerScreen.kt b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/TimerScreen.kt index 63aebd9..2a14b35 100644 --- a/app/src/main/java/com/alveteg/simon/minutelauncher/settings/TimerScreen.kt +++ b/app/src/main/java/com/alveteg/simon/minutelauncher/settings/TimerScreen.kt @@ -33,18 +33,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.alveteg.simon.minutelauncher.Event -import com.alveteg.simon.minutelauncher.MinuteRoute import com.alveteg.simon.minutelauncher.UiEvent import com.alveteg.simon.minutelauncher.data.AccessTimer import com.alveteg.simon.minutelauncher.data.AccessTimerMapping import com.alveteg.simon.minutelauncher.data.AppInfo -import com.alveteg.simon.minutelauncher.data.LauncherViewModel +import com.alveteg.simon.minutelauncher.home.HomeEvent import com.alveteg.simon.minutelauncher.theme.archivoBlackFamily import com.alveteg.simon.minutelauncher.theme.archivoFamily import timber.log.Timber @@ -53,7 +51,7 @@ import timber.log.Timber @Composable fun TimerScreen( onNavigate: (UiEvent.Navigate) -> Unit, - viewModel: LauncherViewModel = hiltViewModel() + viewModel: SettingsViewModel = hiltViewModel() ) { LaunchedEffect(key1 = true) { Timber.d("launched effect") @@ -86,7 +84,7 @@ fun TimerScreen( ) }, navigationIcon = { - IconButton(onClick = { onNavigate(UiEvent.Navigate(MinuteRoute.HOME, true)) }) { + IconButton(onClick = { onNavigate(UiEvent.Navigate(SettingsScreen.HOME, true)) }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Navigate Back" @@ -137,7 +135,7 @@ fun TimerScreen( text = { Text(text = "${it.enum.name} (${it.integerValue}s)") }, onClick = { viewModel.onEvent( - Event.SetDefaultTimer( + SettingsEvent.SetDefaultTimer( AccessTimerMapping( enum = AccessTimer.DEFAULT, integerValue = it.integerValue @@ -175,7 +173,7 @@ fun AppTimerCard( timerMappings: List, onEvent: (Event) -> Unit ) { - var expanded by mutableStateOf(false) + var expanded by remember { mutableStateOf(false) } Surface( modifier = Modifier.padding(vertical = 1.dp) ) { @@ -215,7 +213,7 @@ fun AppTimerCard( text = { Text(text = "${it.enum.name} (${it.integerValue}s)") }, onClick = { onEvent( - Event.UpdateApp( + HomeEvent.UpdateApp( appInfo.app.copy(timer = it.enum) ) ) diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index fd6ae73..a7c68d7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,4 +5,8 @@ true @android:color/transparent + + \ No newline at end of file