From afcfac1b3cd3d127326005fb368a9bbb1e7ccfd2 Mon Sep 17 00:00:00 2001 From: James Barr Date: Wed, 16 Jun 2021 19:52:33 -0700 Subject: [PATCH] Merge activities, add underlining to query, & add Info screen --- app/src/main/AndroidManifest.xml | 16 +- .../github/jbarr21/appdialer/app/AppModule.kt | 20 +-- .../io/github/jbarr21/appdialer/data/App.kt | 29 ++++ .../github/jbarr21/appdialer/data/AppRepo.kt | 8 +- .../jbarr21/appdialer/data/DialerButton.kt | 2 - .../appdialer/service/KeepAliveService.kt | 4 +- .../jbarr21/appdialer/ui/AppDialerApp.kt | 20 +++ .../jbarr21/appdialer/ui/RootActivity.kt | 24 +++ .../io/github/jbarr21/appdialer/ui/Screen.kt | 15 ++ .../appdialer/ui/common/AppDialerTopBar.kt | 55 +++++++ .../jbarr21/appdialer/ui/info/InfoScreen.kt | 62 +++++++ .../appdialer/ui/info/InfoViewModel.kt | 39 +++++ .../jbarr21/appdialer/ui/main/MainActivity.kt | 93 ----------- .../appdialer/ui/main/MainAppBottomSheet.kt | 8 +- .../jbarr21/appdialer/ui/main/MainScreen.kt | 94 +++++------ .../appdialer/ui/main/MainViewModel.kt | 93 ++++++++--- .../{MainPreviewData.kt => PreviewData.kt} | 24 +-- .../jbarr21/appdialer/ui/main/apps/AppGrid.kt | 48 ++++-- .../appdialer/ui/main/apps/AppGridModule.kt | 30 ++++ .../jbarr21/appdialer/ui/main/apps/AppItem.kt | 52 +++--- .../appdialer/ui/main/dialer/DialerGrid.kt | 15 +- .../appdialer/ui/main/dialer/DialerItem.kt | 32 +++- .../{MainModule.kt => dialer/DialerModule.kt} | 10 +- .../appdialer/ui/settings/SettingsActivity.kt | 154 ------------------ .../appdialer/ui/settings/SettingsModule.kt | 10 +- .../appdialer/ui/settings/SettingsScreen.kt | 117 +++++++++++++ .../ui/settings/SettingsViewModel.kt | 30 ++-- .../appdialer/util/ActivityLauncher.kt | 28 ++-- .../io/github/jbarr21/appdialer/util/Trie.kt | 4 +- 29 files changed, 690 insertions(+), 446 deletions(-) create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/AppDialerApp.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/RootActivity.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/Screen.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/common/AppDialerTopBar.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoScreen.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoViewModel.kt delete mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainActivity.kt rename app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/{MainPreviewData.kt => PreviewData.kt} (55%) create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGridModule.kt rename app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/{MainModule.kt => dialer/DialerModule.kt} (72%) delete mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsActivity.kt create mode 100644 app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsScreen.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39d77b4..7a0e4c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,27 +20,15 @@ android:name=".app.AppDialerApplication" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme" + android:theme="@style/Theme.AppDialer" tools:ignore="GoogleAppIndexingWarning"> - + - - - - - - - - diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/app/AppModule.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/app/AppModule.kt index d919782..6ddcdc9 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/app/AppModule.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/app/AppModule.kt @@ -13,27 +13,29 @@ import coil.ImageLoader import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ApplicationComponent +import dagger.hilt.components.SingletonComponent import io.github.jbarr21.appdialer.data.db.AppDatabase +import io.github.jbarr21.appdialer.util.ActivityLauncher import io.github.jbarr21.appdialer.util.AppIconFetcher -import javax.inject.Singleton - -@InstallIn(ApplicationComponent::class) +@InstallIn(SingletonComponent::class) @Module object AppModule { - @Singleton + @Provides + fun activityLauncher( + application: Application, + launcherApps: LauncherApps + ): ActivityLauncher = ActivityLauncher(application, launcherApps) + @Provides fun activityManager(application: Application) = application.getSystemService()!! - @Singleton @Provides fun appDatabase(application: Application): AppDatabase { return Room.databaseBuilder(application, AppDatabase::class.java, "apps").build() } - @Singleton @Provides fun imageCache(am: ActivityManager, application: Application): LruCache { val largeHeap = application.applicationInfo.flags and FLAG_LARGE_HEAP !== 0 @@ -43,7 +45,6 @@ object AppModule { return LruCache(maxSize) } - @Singleton @Provides fun imageLoader(application: Application, appIconFetcher: AppIconFetcher): ImageLoader { return ImageLoader.Builder(application).componentRegistry { @@ -51,15 +52,12 @@ object AppModule { }.build() } - @Singleton @Provides fun launcherApps(application: Application) = application.getSystemService()!! - @Singleton @Provides fun packageManager(application: Application) = application.packageManager - @Singleton @Provides fun userManager(application: Application) = application.getSystemService()!! } \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/App.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/App.kt index 96189b3..82adc3a 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/App.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/App.kt @@ -6,6 +6,11 @@ import android.content.Intent.ACTION_MAIN import android.graphics.Color.TRANSPARENT import android.net.Uri import android.os.UserHandle +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import io.github.jbarr21.appdialer.ui.main.dialer.DialerModule import io.github.jbarr21.appdialer.util.AppIconFetcher.Companion.PARAM_ACTIVITY_NAME import io.github.jbarr21.appdialer.util.AppIconFetcher.Companion.PARAM_USER_ID import io.github.jbarr21.appdialer.util.AppIconFetcher.Companion.SCHEME_PNAME @@ -30,6 +35,30 @@ data class App( return user == other.user && packageName == other.packageName && activityName == other.activityName } + fun annotatedLabel(query: String): AnnotatedString { + val keyMappings = DialerModule.keyMappings() + var matching = 0 + while (matching < query.length && matching < label.length + && keyMappings.keyForValueContainingChar(label[matching]) == keyMappings.keyForValueContainingChar(query[matching])) { + matching++ + } + + return AnnotatedString.Builder().apply { + label.substring(0, matching).let { + if (it.isNotEmpty()) { + pushStyle(SpanStyle(textDecoration = TextDecoration.Underline, fontWeight = FontWeight.Bold)) + append(it) + pop() + } + } + append(label.substring(minOf(matching, label.length))) + }.toAnnotatedString() + } + + private fun Map.keyForValueContainingChar(ch: Char): Int { + return entries.firstOrNull { (_, letters) -> ch.toLowerCase() in letters.toLowerCase() }?.key ?: -1 + } + companion object { fun iconUri(packageName: String, activityName: String, user: UserHandle): Uri { return Uri.parse("$SCHEME_PNAME://$packageName?$PARAM_ACTIVITY_NAME=$activityName&$PARAM_USER_ID=${user.id}") diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/AppRepo.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/AppRepo.kt index 7accdf5..3f3c308 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/AppRepo.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/AppRepo.kt @@ -33,9 +33,15 @@ class AppRepo @Inject constructor( return withContext(Dispatchers.IO) { if (useCache) { val cachedApps = loadAppsFromCache() - if (cachedApps.isNotEmpty()) return@withContext cachedApps + if (cachedApps.isNotEmpty()) { + this@AppRepo.cachedApps.clear() + this@AppRepo.cachedApps.addAll(cachedApps) + return@withContext cachedApps + } } val pmApps = loadAppsFromPackageManager() + this@AppRepo.cachedApps.clear() + this@AppRepo.cachedApps.addAll(pmApps) return@withContext pmApps } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/DialerButton.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/DialerButton.kt index 14efe34..0136b32 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/data/DialerButton.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/data/DialerButton.kt @@ -7,8 +7,6 @@ data class DialerButton( ) { val isClearButton = "clear" in label.toString().toLowerCase() val isInfoButton = "i" in label.toString().toLowerCase() - val isRefreshButton = "r" in label.toString().toLowerCase() - } fun List.asText() = map { it.letters.first().toString() }.joinToString(separator = "") diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/service/KeepAliveService.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/service/KeepAliveService.kt index c6c2e5a..655f591 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/service/KeepAliveService.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/service/KeepAliveService.kt @@ -10,7 +10,7 @@ import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import io.github.jbarr21.appdialer.R import io.github.jbarr21.appdialer.data.UserPreferencesRepo -import io.github.jbarr21.appdialer.ui.main.MainActivity +import io.github.jbarr21.appdialer.ui.RootActivity import io.github.jbarr21.appdialer.util.Channels import timber.log.Timber import javax.inject.Inject @@ -51,7 +51,7 @@ class KeepAliveService : Service() { } private fun createNotification(): Notification { - val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { intent -> + val pendingIntent: PendingIntent = Intent(this, RootActivity::class.java).let { intent -> PendingIntent.getActivity(this, 0, intent, 0) } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/AppDialerApp.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/AppDialerApp.kt new file mode 100644 index 0000000..b93fb24 --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/AppDialerApp.kt @@ -0,0 +1,20 @@ +package io.github.jbarr21.appdialer.ui + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import io.github.jbarr21.appdialer.ui.info.InfoScreen +import io.github.jbarr21.appdialer.ui.main.MainScreen +import io.github.jbarr21.appdialer.ui.settings.SettingsScreen + +@Composable +fun AppDialerApp() { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = Screen.Main.toString()) { + composable(Screen.Main.toString()) { MainScreen(hiltViewModel(), navController) } + composable(Screen.Settings.toString()) { SettingsScreen(hiltViewModel(), navController) } + composable(Screen.Info.toString()) { InfoScreen(hiltViewModel(), navController) } + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/RootActivity.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/RootActivity.kt new file mode 100644 index 0000000..a0a02ec --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/RootActivity.kt @@ -0,0 +1,24 @@ +package io.github.jbarr21.appdialer.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat +import com.google.accompanist.insets.ProvideWindowInsets +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class RootActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + ProvideWindowInsets { + AppTheme { + AppDialerApp() + } + } + } + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/Screen.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/Screen.kt new file mode 100644 index 0000000..cf752c6 --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/Screen.kt @@ -0,0 +1,15 @@ +package io.github.jbarr21.appdialer.ui + +sealed class Screen(val route: String) { + object Main : Screen("main") + object Settings : Screen("settings") + object Info : Screen("info") + + companion object { + val items = listOf( + Main, + Settings, + Info + ) + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/common/AppDialerTopBar.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/common/AppDialerTopBar.kt new file mode 100644 index 0000000..7a4c9d4 --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/common/AppDialerTopBar.kt @@ -0,0 +1,55 @@ +package io.github.jbarr21.appdialer.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.google.accompanist.insets.statusBarsPadding +import io.github.jbarr21.appdialer.ui.AppTheme + +@Composable +fun AppDialerTopBar( + title: String, + navController: NavController, + showStatusBar: Boolean = true, + actions: @Composable RowScope.() -> Unit = {} +) { + val appBarColor = Color(0xFF2D2D2D) + TopAppBar( + title = { Text(title) }, + backgroundColor = appBarColor, + contentColor = MaterialTheme.colors.onSurface, + modifier = if (showStatusBar) Modifier + .background(appBarColor) + .statusBarsPadding() else Modifier.padding(0.dp), + actions = actions, + navigationIcon = navController.previousBackStackEntry?.let { + { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.Default.ArrowBack, contentDescription = null) + } + } + } + ) +} + +@Preview +@Composable +fun AppDialerTopBarPreview() { + AppTheme(darkTheme = true) { + AppDialerTopBar(title = "AppDialer", NavController(LocalContext.current)) + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoScreen.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoScreen.kt new file mode 100644 index 0000000..ed94aa2 --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoScreen.kt @@ -0,0 +1,62 @@ +package io.github.jbarr21.appdialer.ui.info + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import io.github.jbarr21.appdialer.ui.AppTheme +import io.github.jbarr21.appdialer.ui.common.AppDialerTopBar + +@Composable +fun InfoScreen(viewModel: InfoViewModel = viewModel(), navController: NavController) { + Scaffold(topBar = { AppDialerTopBar(title = "AppDialer Info", navController = navController) }) { + InfoContent(viewModel.appStats) + } +} + +@Composable +fun InfoContent(appStats: AppStats) { + Surface(modifier = Modifier.fillMaxSize()) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(32.dp)) { + if (appStats.isLoaded) { + with (appStats) { + listOf("Total = $totalCount", + "Unique = $uniqueAppCount", + "Main = $mainAppCount", + "Work = $workAppCount" + ).let { + Text(it.joinToString("\n")) + } + } + } else { + CircularProgressIndicator() + } + } + } +} + +@Preview +@Composable +fun InfoScreenPreview() { + AppTheme(darkTheme = true) { + InfoContent(AppStats(142, 121, 100, 42)) + } +} + +@Preview +@Composable +fun InfoScreenLoadingPreview() { + AppTheme(darkTheme = true) { + InfoContent(AppStats(-1, -1, -1, -1)) + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoViewModel.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoViewModel.kt new file mode 100644 index 0000000..f23a0cc --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/info/InfoViewModel.kt @@ -0,0 +1,39 @@ +package io.github.jbarr21.appdialer.ui.info + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.jbarr21.appdialer.data.AppRepo +import io.github.jbarr21.appdialer.data.isMain +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class InfoViewModel @Inject constructor(private val appRepo: AppRepo) : ViewModel() { + + var appStats by mutableStateOf(AppStats(-1, -1, -1, -1)).also { loadApps() } + + private fun loadApps() { + viewModelScope.launch { + val apps = appRepo.loadApps(true) + appStats = AppStats( + totalCount = apps.size, + uniqueAppCount = apps.groupBy { it.packageName + it.activityName }.size, + mainAppCount = apps.filter { it.user.isMain }.size, + workAppCount = apps.filter { !it.user.isMain }.size + ) + } + } +} + +data class AppStats( + val totalCount: Int, + val uniqueAppCount: Int, + val mainAppCount: Int, + val workAppCount: Int +) { + val isLoaded = totalCount >= 0 +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainActivity.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainActivity.kt deleted file mode 100644 index 3fe411e..0000000 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainActivity.kt +++ /dev/null @@ -1,93 +0,0 @@ -package io.github.jbarr21.appdialer.ui.main - -import android.content.Intent -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.livedata.observeAsState -import dagger.hilt.android.AndroidEntryPoint -import io.github.jbarr21.appdialer.R -import io.github.jbarr21.appdialer.data.App -import io.github.jbarr21.appdialer.data.DialerButton -import io.github.jbarr21.appdialer.data.SimpleListItem -import io.github.jbarr21.appdialer.ui.AppTheme -import io.github.jbarr21.appdialer.ui.settings.SettingsActivity -import io.github.jbarr21.appdialer.util.ActivityLauncher -import io.github.jbarr21.appdialer.util.Vibrator -import javax.inject.Inject - -@AndroidEntryPoint -class MainActivity : AppCompatActivity() { - - @Inject lateinit var activityLauncher: ActivityLauncher - @Inject lateinit var dialerLabels: List - @Inject lateinit var mainViewModelFactory: MainViewModel.Factory - @Inject lateinit var vibrator: Vibrator - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val mainViewModel by lazy { mainViewModelFactory.create(MainViewModel::class.java) } - - val onAppClicked: (App) -> Unit = { - vibrator.vibrate() - activityLauncher.startMainActivity(it) - finishAndRemoveTask() - } - - val onAppLongClicked: (App?) -> Unit = { - vibrator.vibrate() - mainViewModel.selectedApp.value = it - } - - val onDialerClicked: (DialerButton) -> Unit = { - vibrator.vibrate() - if (it.isClearButton) { - mainViewModel.clearQuery() - } else { - mainViewModel.addToQuery(it) - } - } - - val onDialerLongClicked: (DialerButton) -> Unit = { - vibrator.vibrate() - when { - it.isClearButton -> activityLauncher.startActivity(Intent(this, SettingsActivity::class.java)) - it.isRefreshButton -> mainViewModel.loadApps(useCache = false) - } - } - - val itemAction: (() -> Unit) -> Unit = { - it() - mainViewModel.selectedApp.value = null - } - val appLongClickActions = listOf>( - SimpleListItem("Uninstall", iconRes = R.drawable.ic_delete_black_24dp, action = { - itemAction { activityLauncher.uninstallApp(it) } - }), - SimpleListItem("App Info", iconRes = R.drawable.ic_info_black_24dp, action = { - itemAction { activityLauncher.startAppDetails(it) } - }), - SimpleListItem("Play Store", iconRes = R.drawable.ic_local_grocery_store_black_24dp, action = { - itemAction { activityLauncher.startPlayStore(it) } - }) - ) - - setContent { - AppTheme { - MainScreen( - apps = mainViewModel.filteredApps.observeAsState(emptyList()), - buttons = dialerLabels, - buttonColors = mainViewModel.buttonColors, - query = mainViewModel.queryText, - selectedApp = mainViewModel.selectedApp, - appLongClickActions = appLongClickActions, - onAppClicked = onAppClicked, - onAppLongClicked = onAppLongClicked, - onDialerClicked = onDialerClicked, - onDialerLongClicked = onDialerLongClicked - ) - } - } - } -} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainAppBottomSheet.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainAppBottomSheet.kt index d6400ee..a1ff115 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainAppBottomSheet.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainAppBottomSheet.kt @@ -23,12 +23,13 @@ import androidx.compose.ui.unit.dp import io.github.jbarr21.appdialer.R import io.github.jbarr21.appdialer.data.App import io.github.jbarr21.appdialer.data.SimpleListItem -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.previewApp +import io.github.jbarr21.appdialer.ui.main.PreviewData.previewApp @Composable fun MainAppBottomSheet( app: App, actions: List>, + onActionClick: () -> Unit = {}, onDismiss: () -> Unit = {} ) { Surface( @@ -56,7 +57,10 @@ fun MainAppBottomSheet( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable(onClick = { it.action(app) }) + .clickable(onClick = { + it.action(app) + onActionClick() + }) .padding(16.dp) ) { Image( diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainScreen.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainScreen.kt index acaf2d6..d0db915 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainScreen.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainScreen.kt @@ -5,79 +5,64 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme -import androidx.compose.material.SnackbarDuration import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.github.jbarr21.appdialer.data.App -import io.github.jbarr21.appdialer.data.DialerButton -import io.github.jbarr21.appdialer.data.SimpleListItem +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import io.github.jbarr21.appdialer.ui.AppTheme -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.buttonColors -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.buttons -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.previewApps import io.github.jbarr21.appdialer.ui.main.apps.AppGrid import io.github.jbarr21.appdialer.ui.main.dialer.DialerGrid -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun MainScreen( - apps: State>, - buttons: List, - buttonColors: List = emptyList(), - query: State = mutableStateOf(""), - selectedApp: State = mutableStateOf(null), - appLongClickActions: List> = emptyList(), - onAppClicked: (App) -> Unit = {}, - onAppLongClicked: (App?) -> Unit = {}, - onDialerClicked: (DialerButton) -> Unit = {}, - onDialerLongClicked: (DialerButton) -> Unit = {} + viewModel: MainViewModel = viewModel(), + navController: NavController ) { val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() - - val onDialerLongClickedDecorated: (DialerButton) -> Unit = { - onDialerLongClicked(it) - if (it.isInfoButton) { - scope.launch { - snackbarHostState.showSnackbar( - message = "Found ${apps.value.size} apps", - duration = SnackbarDuration.Short - ) - } - } else if (it.isRefreshButton) { - scope.launch { - snackbarHostState.showSnackbar( - message = "Refreshing...", - duration = SnackbarDuration.Short - ) - } - } - } + val context = LocalContext.current Surface(color = MaterialTheme.colors.background, modifier = Modifier.fillMaxSize()) { Box { - AppGrid(apps = apps, query = query, onClick = onAppClicked, onLongClick = onAppLongClicked) + AppGrid( + apps = viewModel.filteredApps, + query = viewModel.queryText, + isRefreshing = viewModel.isRefreshing, + onClick = { viewModel.onAppClicked(context, it) }, + onLongClick = viewModel.onAppLongClicked, + onRefresh = { viewModel.loadApps(false) } + ) Box(modifier = Modifier.align(alignment = Alignment.BottomCenter)) { - DialerGrid(buttons = buttons, buttonColors = buttonColors, onClick = onDialerClicked, onLongClick = onDialerLongClickedDecorated) + DialerGrid( + buttons = viewModel.dialerLabels, + buttonColors = viewModel.buttonColors, + onClick = viewModel.onDialerClicked, + onLongClick = { viewModel.onDialerLongClickedDecorated(it, navController, snackbarHostState) } + ) } SnackbarHost( hostState = snackbarHostState, - modifier = Modifier.fillMaxSize().padding(8.dp).align(alignment = Alignment.TopCenter) + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + .align(alignment = Alignment.TopCenter) ) - selectedApp.value?.let { - MainAppBottomSheet(it, appLongClickActions, onDismiss = { onAppLongClicked(null) }) + viewModel.selectedApp.value?.let { + MainAppBottomSheet( + it, + viewModel.appLongClickActions, + onActionClick = { viewModel.selectedApp.value = null }, + onDismiss = { viewModel.onAppLongClicked(null) } + ) } } } @@ -87,7 +72,10 @@ fun MainScreen( @Composable fun MainPreview() { AppTheme(darkTheme = true) { - MainScreen(apps = mutableStateOf(previewApps), buttons = buttons, buttonColors = buttonColors) + MainScreen( + viewModel = hiltViewModel(), + navController = NavController(LocalContext.current) + ) } } @@ -95,7 +83,10 @@ fun MainPreview() { @Composable fun MainPreviewLight() { AppTheme(darkTheme = false) { - MainScreen(apps = mutableStateOf(previewApps), buttons = buttons, buttonColors = buttonColors) + MainScreen( + viewModel = hiltViewModel(), + navController = NavController(LocalContext.current) + ) } } @@ -103,6 +94,9 @@ fun MainPreviewLight() { @Composable fun MainPreviewModal() { AppTheme(darkTheme = false) { - MainScreen(apps = mutableStateOf(previewApps), buttons = buttons, buttonColors = buttonColors) + MainScreen( + viewModel = hiltViewModel(), + navController = NavController(LocalContext.current) + ) } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainViewModel.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainViewModel.kt index 87ce8c9..f973208 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainViewModel.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainViewModel.kt @@ -1,63 +1,112 @@ package io.github.jbarr21.appdialer.ui.main +import android.app.Activity +import android.content.Context +import androidx.compose.material.SnackbarHostState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.jbarr21.appdialer.data.App import io.github.jbarr21.appdialer.data.AppRepo import io.github.jbarr21.appdialer.data.DialerButton +import io.github.jbarr21.appdialer.data.SimpleListItem import io.github.jbarr21.appdialer.data.asText +import io.github.jbarr21.appdialer.ui.Screen +import io.github.jbarr21.appdialer.util.ActivityLauncher import io.github.jbarr21.appdialer.util.Trie +import io.github.jbarr21.appdialer.util.Vibrator +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject -class MainViewModel( - private val appRepo: AppRepo +@HiltViewModel +class MainViewModel @Inject constructor( + private val activityLauncher: ActivityLauncher, + internal val appLongClickActions: List>, + private val appRepo: AppRepo, + internal val dialerLabels: List, + private val vibrator: Vibrator ) : ViewModel() { - private val allApps by lazy { MutableLiveData>() } - private val query = mutableStateOf(listOf()) - private val trie = Trie() + private var allApps by mutableStateOf(emptyList()) + private var query by mutableStateOf(listOf()) + private var trie = Trie() val buttonColors = listOf(0xff2196f3, 0xfff44336, 0xffffeb3b, 0xff4caf50, 0xff888888).map { Color(it) } - val selectedApp = mutableStateOf(null) - val filteredApps by lazy { MutableLiveData>() } - val queryText = mutableStateOf(query.value.asText()) + var selectedApp = mutableStateOf(null) + var filteredApps by mutableStateOf(emptyList()) + var queryText by mutableStateOf(query.asText()) + var isRefreshing by mutableStateOf(false) init { loadApps(useCache = true) } fun addToQuery(dialerButton: DialerButton) { - query.value = query.value.plus(dialerButton) - queryText.value = query.value.asText() - trie.predictWord(queryText.value) + query = query.plus(dialerButton) + queryText = query.asText() + trie.predictWord(queryText) .sortedBy { it.label.toLowerCase() } - .also { apps -> filteredApps.value = apps } + .also { apps -> filteredApps = apps } } fun clearQuery() { - query.value = emptyList() - filteredApps.value = allApps.value + query = emptyList() + filteredApps = allApps } fun loadApps(useCache: Boolean = true) { + isRefreshing = true viewModelScope.launch { appRepo.loadApps(useCache).let { apps -> - allApps.value = apps - if (query.value.isEmpty()) { - filteredApps.value = apps + allApps = apps + if (query.isEmpty()) { + filteredApps = apps } trie.clear() apps.forEach { trie.add(it.label, it) } + delay(3000) + isRefreshing = false } } } - class Factory @Inject constructor(private val appRepo: AppRepo) : ViewModelProvider.Factory { - override fun create(modelClass: Class) = MainViewModel(appRepo) as T + fun onAppClicked(context: Context, app: App) { + vibrator.vibrate() + activityLauncher.startMainActivity(app) + (context as Activity).finishAndRemoveTask() } -} \ No newline at end of file + + val onAppLongClicked: (App?) -> Unit = { + vibrator.vibrate() + selectedApp.value = it + } + + val onDialerClicked: (DialerButton) -> Unit = { + vibrator.vibrate() + if (it.isClearButton) { + clearQuery() + } else { + addToQuery(it) + } + } + + fun onDialerLongClicked(button: DialerButton, navController: NavController) { + vibrator.vibrate() + if (button.isClearButton) { + navController.navigate(Screen.Settings.toString()) + } + } + + fun onDialerLongClickedDecorated(button: DialerButton, navController: NavController, snackbarHostState: SnackbarHostState) { + onDialerLongClicked(button, navController) + if (button.isInfoButton) { + navController.navigate(Screen.Info.toString()) + } + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainPreviewData.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/PreviewData.kt similarity index 55% rename from app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainPreviewData.kt rename to app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/PreviewData.kt index 49bd700..b080904 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainPreviewData.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/PreviewData.kt @@ -4,21 +4,20 @@ import android.os.Process import androidx.compose.ui.graphics.Color import io.github.jbarr21.appdialer.data.App import io.github.jbarr21.appdialer.data.DialerButton +import kotlin.random.Random -object MainPreviewData { +object PreviewData { val previewApp = App( "Name", "com.foo", "BarActivity", Process.myUserHandle(), - iconColor = randomColor() + iconColor = randomColor("Name") ) val previewApps = (0 until 10).map { - previewApp.copy( - name = "${previewApp.name} $it", - iconColor = randomColor() - ) + val name = "${previewApp.name} $it" + previewApp.copy(name = name, iconColor = randomColor(name)) } val buttons = listOf(DialerButton(label = "CLEAR*")) + (0 until 8).map { digit -> @@ -29,9 +28,14 @@ object MainPreviewData { }.joinToString(separator = "")) } - val buttonColors = (0 until 5).map { Color(randomColor()) } + val buttonColors = (0 until 5).map { Color(randomColor(('A'.toInt() + it).toString())) } - fun randomColor(): Int { - return android.graphics.Color.rgb((0..256).random(), (0..256).random(), (0..256).random()) + private fun randomColor(str: String): Int { + val random = Random(str.hashCode()) + return android.graphics.Color.rgb( + (0..256).random(random), + (0..256).random(random), + (0..256).random(random) + ) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGrid.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGrid.kt index bc7943b..b69edd3 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGrid.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGrid.kt @@ -11,27 +11,29 @@ import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import io.github.jbarr21.appdialer.data.App -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.previewApps +import io.github.jbarr21.appdialer.ui.main.PreviewData.previewApps @OptIn(ExperimentalFoundationApi::class) @Composable fun AppGrid( - apps: State>, - query: State = mutableStateOf(""), + apps: List, + query: String = "", numColumns: Int = 4, + isRefreshing: Boolean = false, onClick: (App) -> Unit = {}, - onLongClick: (App) -> Unit = {} + onLongClick: (App) -> Unit = {}, + onRefresh: () -> Unit = {} ) { - val isEmpty = apps.value.isEmpty() && query.value.isNotEmpty() - val isLoading = apps.value.isEmpty() && query.value.isEmpty() + val isEmpty = apps.isEmpty() && query.isNotEmpty() + val isLoading = apps.isEmpty() && query.isEmpty() when { isLoading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { @@ -41,19 +43,24 @@ fun AppGrid( isEmpty -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { Text( - text = "No matches for \"${query.value}\"", + text = "No matches for \"${query}\"", textAlign = TextAlign.Center, style = MaterialTheme.typography.h5, modifier = Modifier.padding(32.dp) ) } } - apps.value.isNotEmpty() -> { - LazyVerticalGrid(cells = GridCells.Fixed(numColumns), content = { - itemsIndexed(apps.value) { _, app -> - AppItem(app, onClick, onLongClick) - } - }) + apps.isNotEmpty() -> { + SwipeRefresh( + state = rememberSwipeRefreshState(isRefreshing), + onRefresh = onRefresh + ) { + LazyVerticalGrid(cells = GridCells.Fixed(numColumns), content = { + itemsIndexed(apps) { _, app -> + AppItem(app, query, onClick, onLongClick) + } + }) + } } } } @@ -61,12 +68,17 @@ fun AppGrid( @Preview @Composable fun AppGridPreview() { - AppGrid(mutableStateOf(previewApps)) + AppGrid(previewApps) } @Preview(widthDp = 300, heightDp = 300) -@Preview @Composable fun EmptyAppGridPreview() { - AppGrid(mutableStateOf(emptyList()), query = mutableStateOf("xyz")) {} + AppGrid(emptyList(), query = "xyz") {} +} + +@Preview(widthDp = 300, heightDp = 300) +@Composable +fun LoadingAppGridPreview() { + AppGrid(emptyList(), query = "") {} } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGridModule.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGridModule.kt new file mode 100644 index 0000000..f05b051 --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppGridModule.kt @@ -0,0 +1,30 @@ +package io.github.jbarr21.appdialer.ui.main.apps + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import io.github.jbarr21.appdialer.R +import io.github.jbarr21.appdialer.data.App +import io.github.jbarr21.appdialer.data.SimpleListItem +import io.github.jbarr21.appdialer.util.ActivityLauncher + +@InstallIn(ViewModelComponent::class) +@Module +object AppGridModule { + + @Provides + fun appLongClickActions(activityLauncher: ActivityLauncher): List> { + return listOf( + SimpleListItem("Uninstall", iconRes = R.drawable.ic_delete_black_24dp, action = { + activityLauncher.uninstallApp(it) + }), + SimpleListItem("App Info", iconRes = R.drawable.ic_info_black_24dp, action = { + activityLauncher.startAppDetails(it) + }), + SimpleListItem("Play Store", iconRes = R.drawable.ic_local_grocery_store_black_24dp, action = { + activityLauncher.startPlayStore(it) + }) + ) + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppItem.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppItem.kt index 737b4a0..605cadf 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppItem.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/apps/AppItem.kt @@ -6,9 +6,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme @@ -22,20 +24,19 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.coil.rememberCoilPainter +import com.google.accompanist.imageloading.ImageLoadState import io.github.jbarr21.appdialer.data.App -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.previewApp - +import io.github.jbarr21.appdialer.ui.AppTheme +import io.github.jbarr21.appdialer.ui.main.PreviewData.previewApp @OptIn(ExperimentalFoundationApi::class) @Composable fun AppItem( app: App, + query: String = "", onClick: (App) -> Unit = {}, onLongClick: (App) -> Unit = {} ) { - val placeholderImage = Box(modifier = Modifier - .background(Color.DarkGray) - .clip(CircleShape)) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -48,18 +49,26 @@ fun AppItem( .padding(8.dp) .fillMaxWidth() .aspectRatio(1f)) { - Image( - painter = rememberCoilPainter( - request = app.iconUri.toString(), - fadeIn = true), - contentDescription = null, -// loading = { placeholderImage }, -// error = { placeholderImage }, - modifier = Modifier.fillMaxSize() - ) + + val painter = rememberCoilPainter(request = app.iconUri.toString()) + when (painter.loadState) { + is ImageLoadState.Success -> { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + else -> { + Box(modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + .background(Color.DarkGray)) + } + } } Text( - text = app.label, + text = app.annotatedLabel(query), color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.body2, overflow = TextOverflow.Ellipsis, @@ -71,9 +80,14 @@ fun AppItem( @Preview(widthDp = 150) @Composable fun AppItemPreview() { - Column { - AppItem(previewApp) {} - AppItem(previewApp.copy(name = "Application Name")) - AppItem(previewApp.copy(name = "Really Long Application Name")) + AppTheme(darkTheme = true) { + Column { + listOf("Name", "Application Name", "Really Long Application Name") + .forEach { + AppItem(previewApp.copy(name = it)) + Spacer(modifier = Modifier.height(16.dp)) + } + AppItem(previewApp.copy(name = "Calm"), query = "AA") + } } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerGrid.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerGrid.kt index 8a49334..459b701 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerGrid.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerGrid.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.google.accompanist.insets.navigationBarsPadding import io.github.jbarr21.appdialer.data.DialerButton -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.buttonColors -import io.github.jbarr21.appdialer.ui.main.MainPreviewData.buttons +import io.github.jbarr21.appdialer.ui.AppTheme +import io.github.jbarr21.appdialer.ui.main.PreviewData.buttonColors +import io.github.jbarr21.appdialer.ui.main.PreviewData.buttons @OptIn(ExperimentalFoundationApi::class) @Composable @@ -52,14 +54,15 @@ fun DialerGrid( } }) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().navigationBarsPadding(), horizontalArrangement = Arrangement.SpaceEvenly ) { buttonColors.forEach { color -> FloatingActionButton( backgroundColor = color, onClick = { onColorClicked(color) }, - modifier = Modifier.size(48.dp) + modifier = Modifier + .size(48.dp) .clickable( onClick = { onColorClicked(color) } // , indication = rememberRipple(bounded = false), @@ -76,5 +79,7 @@ fun DialerGrid( @Preview @Composable fun DialerGridPreview() { - DialerGrid(buttons = buttons, buttonColors = buttonColors) + AppTheme(darkTheme = true) { + DialerGrid(buttons = buttons, buttonColors = buttonColors) + } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerItem.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerItem.kt index 704335a..7ec2489 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerItem.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerItem.kt @@ -3,7 +3,9 @@ package io.github.jbarr21.appdialer.ui.main.dialer import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.MaterialTheme @@ -11,10 +13,14 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.jbarr21.appdialer.data.DialerButton +import io.github.jbarr21.appdialer.ui.AppTheme @OptIn(ExperimentalFoundationApi::class) @Composable @@ -23,8 +29,18 @@ fun DialerItem( onClick: (DialerButton) -> Unit = {}, onLongClick: (DialerButton) -> Unit = {} ) { + val text = button.label.toString() + val annotatedText = AnnotatedString.Builder().apply { + append(text.substringBeforeLast("I")) + if (text.endsWith("I")) { + pushStyle(SpanStyle(textDecoration = TextDecoration.Underline)) + append("I") + pop() + } + }.toAnnotatedString() + Text( - text = button.label.toString(), + text = annotatedText, textAlign = TextAlign.Center, style = MaterialTheme.typography.h6, modifier = Modifier @@ -41,8 +57,16 @@ fun DialerItem( @Preview @Composable fun DialerItemPreview() { - Column { - DialerItem(DialerButton(digit = 5, letters = "JKL")) - DialerItem(DialerButton(digit = -1, label = "CLEAR*")) + AppTheme(darkTheme = true) { + Column { + listOf( + 4 to "GHI", + 5 to "JKL", + -1 to "CLEAR*" + ).forEach { (digit, letters) -> + DialerItem(DialerButton(digit = digit, letters = letters)) + Spacer(modifier = Modifier.height(16.dp)) + } + } } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainModule.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerModule.kt similarity index 72% rename from app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainModule.kt rename to app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerModule.kt index 13a4f6e..46d21b2 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/MainModule.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/main/dialer/DialerModule.kt @@ -1,14 +1,14 @@ -package io.github.jbarr21.appdialer.ui.main +package io.github.jbarr21.appdialer.ui.main.dialer import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.ViewModelComponent import io.github.jbarr21.appdialer.data.DialerButton -@InstallIn(ActivityComponent::class) +@InstallIn(ViewModelComponent::class) @Module -object MainModule { +object DialerModule { @Provides fun dialerLabels(): List { @@ -17,7 +17,7 @@ object MainModule { }.toList() } - fun keyMappings() = (2 until 10).mapIndexed { index, digit -> + fun keyMappings(): Map = (2 until 10).mapIndexed { index, digit -> val fourSet = setOf(7, 9) val numLetters = if (digit in fourSet) 4 else 3 val letters = (0 until numLetters).map { diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsActivity.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsActivity.kt deleted file mode 100644 index fd94e80..0000000 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsActivity.kt +++ /dev/null @@ -1,154 +0,0 @@ -package io.github.jbarr21.appdialer.ui.settings - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Switch -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import dagger.hilt.android.AndroidEntryPoint -import io.github.jbarr21.appdialer.R -import io.github.jbarr21.appdialer.data.SimpleListItem -import io.github.jbarr21.appdialer.data.UserPreferences -import io.github.jbarr21.appdialer.ui.AppTheme -import javax.inject.Inject - -@AndroidEntryPoint -class SettingsActivity : AppCompatActivity() { - - @Inject lateinit var viewModelFactory: SettingsViewModel.Factory - @Inject lateinit var settingsData: List> - - private val viewModel: SettingsViewModel by viewModels { viewModelFactory } - private val onNavIconPressed = { finish() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - viewModel.userPreferences.observe(this) { - setContent { - SettingsScreen(onNavIconPressed = onNavIconPressed, userPreferences = it) - } - } - } - - @Composable - fun SettingsScreen( - onNavIconPressed: () -> Unit, - userPreferences: UserPreferences - ) { - AppTheme { - Scaffold( - topBar = { TopBar(onNavIconPressed = onNavIconPressed) }) { - Surface(modifier = Modifier.fillMaxSize()) { - Column { - SettingsGroup("General") - SettingsItem( - listItem = settingsData[0], - checked = userPreferences.useHapticFeedback, - onCheckedChange = { viewModel.updateUseHaptipFeedback(it) } - ) - SettingsItem( - listItem = settingsData[1], - checked = userPreferences.usePersistentService, - onCheckedChange = { viewModel.updateUsePersistentService(it) } - ) - } - } - } - } - } - - @Composable - fun TopBar(onNavIconPressed: () -> Unit) { - TopAppBar( - title = { Text("AppDialer Settings") }, - navigationIcon = { - Image( - painter = painterResource(id = R.drawable.ic_back), - contentDescription = null, - modifier = Modifier - .padding(horizontal = 16.dp) - .clickable(onClick = onNavIconPressed) - ) - } - ) - } - - @Composable - fun SettingsGroup(title: String) { - Text( - title, - style = MaterialTheme.typography.button, - color = MaterialTheme.colors.secondaryVariant, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 72.dp) - .padding(top = 16.dp) - ) - } - - @Composable - fun SettingsItem(listItem: SimpleListItem, checked: Boolean, onCheckedChange: (Boolean) -> Unit = { }) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable(onClick = { onCheckedChange(!checked) }).padding(vertical = 16.dp) - ) { - Image( - painter = painterResource(id = listItem.iconRes), - colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), - contentDescription = null, - modifier = Modifier.padding(horizontal = 24.dp) - ) - Column(modifier = Modifier.weight(1f, fill = true)) { - Text(listItem.label, style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium)) - Text(listItem.description.orEmpty(), style = MaterialTheme.typography.body2) - } - Switch( - checked = checked, - onCheckedChange = onCheckedChange, - modifier = Modifier.padding(horizontal = 16.dp) - ) - } - } - - @Preview - @Composable - fun DefaultPreview() { - Column { - TopBar(onNavIconPressed = {}) - - SettingsGroup(title = "General") - - repeat(3) { - SettingsItem( - listItem = SimpleListItem( - label = "Setting title $it", - description = "A description for the use of the setting $it", - iconRes = R.drawable.ic_vibration - ), - checked = it % 2 == 0 - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsModule.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsModule.kt index f177348..defd2be 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsModule.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsModule.kt @@ -3,22 +3,22 @@ package io.github.jbarr21.appdialer.ui.settings import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.ViewModelComponent import io.github.jbarr21.appdialer.R import io.github.jbarr21.appdialer.data.SimpleListItem -@InstallIn(ActivityComponent::class) +@InstallIn(ViewModelComponent::class) @Module object SettingsModule { @Provides - fun settingsData() = listOf>( - SimpleListItem( + fun settingsData(): List> = listOf( + SimpleListItem( label = "Vibrate", description = "Use haptic feedback on dialer key taps", iconRes = R.drawable.ic_vibration ), - SimpleListItem( + SimpleListItem( label = "Persistent service", description = "Use a persistent service to keep the app alive", iconRes = R.drawable.ic_notifications diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..298e69a --- /dev/null +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsScreen.kt @@ -0,0 +1,117 @@ +package io.github.jbarr21.appdialer.ui.settings + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import io.github.jbarr21.appdialer.R +import io.github.jbarr21.appdialer.data.SimpleListItem +import io.github.jbarr21.appdialer.ui.AppTheme +import io.github.jbarr21.appdialer.ui.common.AppDialerTopBar + +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = viewModel(), + navController: NavController +) { + Scaffold(topBar = { AppDialerTopBar(title = "AppDialer Settings", navController = navController) }) { + Surface(modifier = Modifier.fillMaxSize()) { + Column { + SettingsGroup("General") + SettingsItem( + listItem = viewModel.settingsData[0], + checked = viewModel.userPreferences.useHapticFeedback, + onCheckedChange = { viewModel.updateUseHaptipFeedback(it) } + ) + SettingsItem( + listItem = viewModel.settingsData[1], + checked = viewModel.userPreferences.usePersistentService, + onCheckedChange = { viewModel.updateUsePersistentService(it) } + ) + } + } + } +} + +@Composable +fun SettingsGroup(title: String) { + Text( + title, + style = MaterialTheme.typography.button, + color = MaterialTheme.colors.secondaryVariant, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 72.dp) + .padding(top = 16.dp) + ) +} + +@Composable +fun SettingsItem(listItem: SimpleListItem, checked: Boolean, onCheckedChange: (Boolean) -> Unit = { }) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = { onCheckedChange(!checked) }) + .padding(vertical = 16.dp) + ) { + Image( + painter = painterResource(id = listItem.iconRes), + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = null, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Column(modifier = Modifier.weight(1f, fill = true)) { + Text(listItem.label, style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium)) + Text(listItem.description.orEmpty(), style = MaterialTheme.typography.body2) + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } +} + +@Preview +@Composable +fun SettingsScreenPreview() { + AppTheme(darkTheme = true) { + Surface { + Column { + AppDialerTopBar(title = "AppDialer Settings", navController = NavController(LocalContext.current)) + + SettingsGroup(title = "General") + + repeat(3) { + SettingsItem( + listItem = SimpleListItem( + label = "Setting title $it", + description = "A description for the use of the setting $it", + iconRes = R.drawable.ic_vibration + ), + checked = it % 2 == 0 + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsViewModel.kt index b5ee866..cf5ddb2 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/ui/settings/SettingsViewModel.kt @@ -1,27 +1,34 @@ package io.github.jbarr21.appdialer.ui.settings -import androidx.lifecycle.MutableLiveData +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.jbarr21.appdialer.data.SimpleListItem import io.github.jbarr21.appdialer.data.UserPreferences import io.github.jbarr21.appdialer.data.UserPreferencesRepo import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject -class SettingsViewModel(private val userPreferencesRepo: UserPreferencesRepo) : ViewModel() { +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val userPreferencesRepo: UserPreferencesRepo, + internal val settingsData: List> +) : ViewModel() { - val userPreferences by lazy { - MutableLiveData().also { - loadUserPreferences() - } + var userPreferences by mutableStateOf(UserPreferences()) + + init { + loadUserPreferences() } private fun loadUserPreferences() { viewModelScope.launch { - userPreferencesRepo.userPreferencesFlow - .collect { userPreferences.value = it } + userPreferencesRepo.userPreferencesFlow.collect { userPreferences = it } } } @@ -33,7 +40,10 @@ class SettingsViewModel(private val userPreferencesRepo: UserPreferencesRepo) : userPreferencesRepo.updateUsePersistentService(enable) } - class Factory @Inject constructor(private val userPreferencesRepo: UserPreferencesRepo) : ViewModelProvider.Factory { - override fun create(modelClass: Class) = SettingsViewModel(userPreferencesRepo) as T + class Factory @Inject constructor( + private val userPreferencesRepo: UserPreferencesRepo, + private val settingsData: List> + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class) = SettingsViewModel(userPreferencesRepo, settingsData) as T } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/util/ActivityLauncher.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/util/ActivityLauncher.kt index f16acc4..76ef807 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/util/ActivityLauncher.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/util/ActivityLauncher.kt @@ -1,28 +1,19 @@ package io.github.jbarr21.appdialer.util +import android.app.Application import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.LauncherApps import android.net.Uri import android.os.Bundle import android.provider.Settings -import androidx.fragment.app.FragmentActivity -import dagger.hilt.android.scopes.ActivityScoped import io.github.jbarr21.appdialer.data.App -import javax.inject.Inject -@ActivityScoped -class ActivityLauncher @Inject constructor( - private val activity: FragmentActivity, +class ActivityLauncher constructor( + private val application: Application, private val launcherApps: LauncherApps ) { - fun startActivity(intent: Intent) = activity.startActivity(intent) - - fun startActivityInNewTask(intent: Intent) { - activity.startActivity(intent.run { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) - } - fun startMainActivity(app: App) { launcherApps.startMainActivity(app.launchIntent.component, app.user, null, Bundle.EMPTY) } @@ -30,15 +21,13 @@ class ActivityLauncher @Inject constructor( fun uninstallApp(app: App) { val packageURI = Uri.parse("package:${app.packageName}") val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageURI) - intent.putExtra(Intent.EXTRA_USER, app.user) - startActivity(intent) + startActivity(intent, app) } fun startAppDetails(app: App) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.data = Uri.parse("package:${app.packageName}") - intent.putExtra(Intent.EXTRA_USER, app.user) - startActivity(intent) + startActivity(intent, app) } fun startPlayStore(app: App) { @@ -47,7 +36,12 @@ class ActivityLauncher @Inject constructor( } catch (anfe: ActivityNotFoundException) { Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=${app.packageName}")) } + startActivity(intent, app) + } + + private fun startActivity(intent: Intent, app: App) { intent.putExtra(Intent.EXTRA_USER, app.user) - startActivity(intent) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) } } diff --git a/app/src/main/kotlin/io/github/jbarr21/appdialer/util/Trie.kt b/app/src/main/kotlin/io/github/jbarr21/appdialer/util/Trie.kt index f2ee7ca..f65ae06 100644 --- a/app/src/main/kotlin/io/github/jbarr21/appdialer/util/Trie.kt +++ b/app/src/main/kotlin/io/github/jbarr21/appdialer/util/Trie.kt @@ -1,6 +1,6 @@ package io.github.jbarr21.appdialer.util -import io.github.jbarr21.appdialer.ui.main.MainModule +import io.github.jbarr21.appdialer.ui.main.dialer.DialerModule import java.util.* // TODO: support matching at more than just the first letter of the label @@ -10,7 +10,7 @@ class Trie( ) ) { - private val keyMappings = MainModule.keyMappings() + private val keyMappings = DialerModule.keyMappings() fun add(word: String, value: T? = null) { var node = root