diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02e27361c..088f73e6d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.sameerasw.essentials" minSdk = 26 targetSdk = 36 - versionCode = 31 - versionName = "12.0" + versionCode = 32 + versionName = "12.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -139,4 +139,7 @@ dependencies { // Watermark dependencies implementation("androidx.exifinterface:exifinterface:1.3.7") implementation("androidx.compose.material:material-icons-extended:1.7.0") // Compatible with Compose BOM + + // GSMArena Parsing + implementation("org.jsoup:jsoup:1.15.3") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 859eff45a..0aa6aacbc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,6 +86,13 @@ android:theme="@style/Theme.Essentials"> + + + = Build.VERSION_CODES.Q) { window.isNavigationBarContrastEnforced = false } + + val isDarkMode = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == + android.content.res.Configuration.UI_MODE_NIGHT_YES + window.setBackgroundDrawableResource(if (isDarkMode) android.R.color.black else R.color.app_window_background) val featureId = intent.getStringExtra("feature") ?: "" val featureObj = FeatureRegistry.ALL_FEATURES.find { it.id == featureId } val highlightSetting = intent.getStringExtra("highlight_setting") @@ -149,9 +160,8 @@ class FeatureSettingsActivity : FragmentActivity() { } } - LaunchedEffect(Unit) { - viewModel.check(context) - } + // Initialize synchronously so settingsRepository is ready before first composition + remember(context) { viewModel.check(context) } val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled val pinnedFeatureKeys by viewModel.pinnedFeatureKeys @@ -296,99 +306,38 @@ class FeatureSettingsActivity : FragmentActivity() { ) } - val scrollBehavior = - TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - Scaffold( - contentWindowInsets = androidx.compose.foundation.layout.WindowInsets( - 0, - 0, - 0, - 0 - ), - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surfaceContainer, - topBar = { - ReusableTopAppBar( - title = if (featureObj != null) stringResource(featureObj.title) else featureId, - hasBack = true, - hasSearch = false, - onBackClick = { finish() }, - scrollBehavior = scrollBehavior, - subtitle = if (featureObj != null) stringResource(featureObj.description) else "", - isBeta = featureObj?.isBeta ?: false, - actions = { - if (featureObj != null && featureObj.aboutDescription != null) { - var showMenu by remember { mutableStateOf(false) } - androidx.compose.material3.IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showMenu = true - }, - colors = androidx.compose.material3.IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ), - modifier = Modifier.size(40.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_more_vert_24), - contentDescription = stringResource(R.string.content_desc_more_options), - modifier = Modifier.size(24.dp) - ) + val pageTitle = if (featureObj != null) stringResource(featureObj.title) else featureId + val hasMenu = featureObj != null && featureObj.aboutDescription != null - com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( - text = { - Text(stringResource(R.string.action_what_is_this)) - }, - onClick = { - showMenu = false - selectedHelpFeature = featureObj - showHelpSheet = true - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_help_24), - contentDescription = null - ) - } - ) - } - } - } - } + val statusBarHeightPx = with(LocalDensity.current) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + } + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainer) + .progressiveBlur( + blurRadius = 40f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP ) - }, - floatingActionButton = { - if (featureId == "Notification lighting") { - ExtendedFloatingActionButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.triggerNotificationLighting(context) - }, - expanded = fabExpanded, - icon = { - Icon( - painter = painterResource(id = R.drawable.rounded_play_arrow_24), - contentDescription = null - ) - }, - text = { Text(stringResource(R.string.action_preview)) }, - modifier = Modifier.height(64.dp) - ) - } - } - ) { innerPadding -> - val hasScroll = - featureId != "Sound mode tile" && featureId != "Quick settings tiles" + ) { + val hasScroll = featureId != "Sound mode tile" && featureId != "Quick settings tiles" Column( modifier = Modifier - .padding(innerPadding) .fillMaxSize() + .progressiveBlur( + blurRadius = 40f, + height = with(LocalDensity.current) { 150.dp.toPx() }, + direction = BlurDirection.BOTTOM + ) .then(if (hasScroll) Modifier.verticalScroll(rememberScrollState()) else Modifier) ) { + // Top padding for status bar + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(statusBarHeight)) + if (featureId == "Watch") { WatchSettingsUI( viewModel = watchViewModel, @@ -670,7 +619,34 @@ class FeatureSettingsActivity : FragmentActivity() { } } + // Bottom padding for toolbar + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(150.dp)) } + + SettingsFloatingToolbar( + title = pageTitle, + onBackClick = { finish() }, + modifier = Modifier + .align(Alignment.BottomCenter) + .zIndex(1f), + menuContent = if (hasMenu) { + { + MenuItem( + text = { Text(stringResource(R.string.action_what_is_this)) }, + onClick = { + selectedHelpFeature = featureObj + showHelpSheet = true + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_help_24), + contentDescription = null + ) + } + ) + } + } else null + ) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index 0ddab28a8..0f4139a01 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -12,7 +12,6 @@ import androidx.activity.viewModels import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -43,14 +42,10 @@ import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -79,14 +74,23 @@ import androidx.fragment.app.FragmentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import com.sameerasw.essentials.domain.DIYTabs import com.sameerasw.essentials.domain.registry.initPermissionRegistry import com.sameerasw.essentials.ui.components.DIYFloatingToolbar -import com.sameerasw.essentials.ui.components.ReusableTopAppBar import com.sameerasw.essentials.ui.components.cards.TrackedRepoCard +import androidx.compose.foundation.layout.statusBarsPadding import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.sheets.AddRepoBottomSheet import com.sameerasw.essentials.ui.components.sheets.GitHubAuthSheet @@ -97,6 +101,8 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.composables.DIYScreen import com.sameerasw.essentials.ui.composables.FreezeGridUI import com.sameerasw.essentials.ui.composables.SetupFeatures +import com.sameerasw.essentials.ui.modifiers.BlurDirection +import com.sameerasw.essentials.ui.modifiers.progressiveBlur import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.AppUpdatesViewModel @@ -247,9 +253,6 @@ class MainActivity : FragmentActivity() { stringResource(R.string.label_unknown) } - var searchRequested by remember { mutableStateOf(false) } - val scrollBehavior = - TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) var showUpdateSheet by remember { mutableStateOf(false) } var showInstructionsSheet by remember { mutableStateOf(false) } val isUpdateAvailable by viewModel.isUpdateAvailable @@ -257,6 +260,7 @@ class MainActivity : FragmentActivity() { var showGitHubAuthSheet by remember { mutableStateOf(false) } var showNewAutomationSheet by remember { mutableStateOf(false) } + var showFabProfileMenu by remember { mutableStateOf(false) } val gitHubToken by viewModel.gitHubToken val gitHubUser by gitHubAuthViewModel.currentUser @@ -436,199 +440,154 @@ class MainActivity : FragmentActivity() { 0, 0 ), - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .nestedScroll(exitAlwaysScrollBehavior), + modifier = Modifier, containerColor = MaterialTheme.colorScheme.surfaceContainer, - topBar = { + topBar = {} + ) { innerPadding -> + val statusBarHeightPx = with(androidx.compose.ui.platform.LocalDensity.current) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + } + + Box( + modifier = Modifier + .fillMaxSize() + .progressiveBlur( + blurRadius = 40f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP + ) + ) { val currentTab = remember(tabs, currentPage) { - tabs.getOrNull(currentPage) ?: tabs.firstOrNull() - ?: DIYTabs.ESSENTIALS + tabs.getOrNull(currentPage) ?: tabs.firstOrNull() ?: DIYTabs.ESSENTIALS } - ReusableTopAppBar( - title = currentTab.title, - subtitle = currentTab.subtitle, - hasBack = false, - hasSearch = true, - hasSettings = currentTab == DIYTabs.ESSENTIALS || currentTab == DIYTabs.FREEZE, - hasHelp = currentTab == DIYTabs.ESSENTIALS, - helpIconRes = R.drawable.rounded_help_24, - helpContentDescription = R.string.action_help_guide, - onSearchClick = { searchRequested = true }, - onSettingsClick = { - if (currentTab == DIYTabs.FREEZE) { - startActivity( - Intent( - this, - FeatureSettingsActivity::class.java - ).apply { - putExtra("feature", "Freeze") - }) - } else { - startActivity(Intent(this, SettingsActivity::class.java)) - } - }, - onUpdateClick = { showUpdateSheet = true }, - onGitHubClick = { showGitHubAuthSheet = true }, - hasGitHub = currentTab == DIYTabs.APPS, - gitHubUser = gitHubUser, - onSignOutClick = { gitHubAuthViewModel.signOut(context) }, - onHelpClick = { - showInstructionsSheet = true + + + DIYFloatingToolbar( + modifier = Modifier + .align(Alignment.BottomCenter) + // .offset(y = -ScreenOffset) + .zIndex(1f), + currentPage = currentPage, + tabs = tabs, + onTabSelected = { index -> + HapticUtil.performUIHaptic(view) + currentPage = index }, - actions = { - when (currentTab) { - DIYTabs.FREEZE -> { - var showFreezeMenu by remember { mutableStateOf(false) } - Box { - IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showFreezeMenu = true - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright + scrollBehavior = exitAlwaysScrollBehavior, + badges = mapOf(DIYTabs.APPS to viewModel.hasPendingUpdates.value), + floatingActionButton = { + Box { // Menu anchor + FloatingActionButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + when (currentTab) { + DIYTabs.ESSENTIALS -> { + startActivity(Intent(context, SettingsActivity::class.java)) + } + DIYTabs.FREEZE -> { + startActivity( + Intent( + context, + FeatureSettingsActivity::class.java + ).apply { + putExtra("feature", "Freeze") + }) + } + DIYTabs.DIY -> { + showNewAutomationSheet = true + } + DIYTabs.APPS -> { + val user = gitHubUser + if (user != null) { + showFabProfileMenu = true + } else { + showGitHubAuthSheet = true + } + } + } + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + shape = MaterialTheme.shapes.large, + elevation = androidx.compose.material3.FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp) + ) { + when (currentTab) { + DIYTabs.ESSENTIALS -> { + Icon( + painter = painterResource(id = R.drawable.rounded_settings_heart_24), + contentDescription = stringResource(R.string.content_desc_settings) ) - ) { + } + DIYTabs.FREEZE -> { + Icon( + painter = painterResource(id = R.drawable.rounded_settings_heart_24), + contentDescription = stringResource(R.string.content_desc_settings) + ) + } + DIYTabs.DIY -> { Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = stringResource(R.string.tab_freeze) + painter = painterResource(id = R.drawable.rounded_add_24), + contentDescription = stringResource(R.string.diy_editor_new_title) ) } + DIYTabs.APPS -> { + val user = gitHubUser + if (user != null) { + AsyncImage( + model = user.avatarUrl, + contentDescription = stringResource(R.string.action_profile), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(24.dp) + .clip(CircleShape), + placeholder = painterResource(id = R.drawable.brand_github), + error = painterResource(id = R.drawable.brand_github) + ) + } else { + Icon( + painter = painterResource(id = R.drawable.brand_github), + contentDescription = stringResource(R.string.action_sign_in_github) + ) + } + } + } + } + if (currentTab == DIYTabs.APPS) { + val user = gitHubUser + if (user != null) { SegmentedDropdownMenu( - expanded = showFreezeMenu, - onDismissRequest = { showFreezeMenu = false } + expanded = showFabProfileMenu, + onDismissRequest = { showFabProfileMenu = false } ) { SegmentedDropdownMenuItem( - text = { Text(stringResource(R.string.action_freeze_all)) }, - onClick = { - showFreezeMenu = false - viewModel.freezeAllApps(context) - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = null - ) - } - ) - SegmentedDropdownMenuItem( - text = { Text(stringResource(R.string.action_unfreeze_all)) }, - onClick = { - showFreezeMenu = false - viewModel.unfreezeAllApps(context) - }, + text = { Text(user.name ?: user.login) }, + onClick = { showFabProfileMenu = false }, leadingIcon = { Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), + painter = painterResource(id = R.drawable.brand_github), contentDescription = null ) } ) SegmentedDropdownMenuItem( - text = { Text("Freeze Automatic") }, + text = { Text(stringResource(R.string.action_sign_out)) }, onClick = { - showFreezeMenu = false - viewModel.freezeAutomaticApps(context) + gitHubAuthViewModel.signOut(context) + showFabProfileMenu = false }, leadingIcon = { Icon( - painter = painterResource(id = R.drawable.rounded_nest_farsight_cool_24), + painter = painterResource(id = R.drawable.rounded_logout_24), contentDescription = null ) } ) } } - Spacer(modifier = Modifier.width(8.dp)) - } - - DIYTabs.DIY -> { - IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showNewAutomationSheet = true - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ) - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_add_24), - contentDescription = stringResource(R.string.diy_editor_new_title) - ) - } - Spacer(modifier = Modifier.width(8.dp)) - } - - DIYTabs.APPS -> { - IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showAddRepoSheet = true - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ) - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_add_24), - contentDescription = stringResource(R.string.action_add_repo) - ) - } - Spacer(modifier = Modifier.width(8.dp)) - - IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - updatesViewModel.checkForUpdates(context) - }, - enabled = refreshingRepoIds.isEmpty(), - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ), - modifier = Modifier.size(40.dp) - ) { - if (refreshingRepoIds.isNotEmpty()) { - CircularWavyProgressIndicator( - progress = { animatedProgress }, - modifier = Modifier.size(24.dp) - ) - } else { - Icon( - painter = painterResource(id = R.drawable.rounded_refresh_24), - contentDescription = stringResource(R.string.action_refresh), - modifier = Modifier.size(24.dp) - ) - } - } - Spacer(modifier = Modifier.width(8.dp)) } - - else -> {} } - }, - hasUpdateAvailable = isUpdateAvailable, - hasHelpBadge = false, - scrollBehavior = scrollBehavior - ) - } - ) { innerPadding -> - Box(modifier = Modifier.fillMaxSize()) { - DIYFloatingToolbar( - modifier = Modifier - .align(Alignment.BottomCenter) - .offset(y = -ScreenOffset) - .zIndex(1f), - currentPage = currentPage, - tabs = tabs, - onTabSelected = { index -> - HapticUtil.performUIHaptic(view) - currentPage = index - }, - scrollBehavior = exitAlwaysScrollBehavior, - badges = mapOf(DIYTabs.APPS to viewModel.hasPendingUpdates.value) + } ) AnimatedContent( @@ -649,58 +608,73 @@ class MainActivity : FragmentActivity() { }, modifier = Modifier .scale(1f - (backProgress.value * 0.05f)) - .alpha(1f - (backProgress.value * 0.3f)), + .alpha(1f - (backProgress.value * 0.3f)) + .progressiveBlur( + blurRadius = 40f, + height = with(androidx.compose.ui.platform.LocalDensity.current) { 130.dp.toPx() }, + direction = BlurDirection.BOTTOM + ), label = "Tab Transition" ) { targetPage -> - when (tabs[targetPage]) { + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val topContentPadding = statusBarHeight + val bottomToolbarPadding = 150.dp + val contentPadding = PaddingValues( + top = topContentPadding, + bottom = bottomToolbarPadding, + start = 16.dp, + end = 16.dp + ) + + when (tabs[targetPage]) { DIYTabs.ESSENTIALS -> { SetupFeatures( viewModel = viewModel, - modifier = Modifier.padding(innerPadding), - searchRequested = searchRequested, - onSearchHandled = { searchRequested = false }, + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, onHelpClick = { showInstructionsSheet = true } ) } DIYTabs.FREEZE -> { - FreezeGridUI( - viewModel = viewModel, - modifier = Modifier.padding(innerPadding), - contentPadding = innerPadding - ) - } + FreezeGridUI( + viewModel = viewModel, + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding + ) + } - DIYTabs.DIY -> { - DIYScreen( - modifier = Modifier.padding(innerPadding), - showNewAutomationSheet = showNewAutomationSheet, - onDismissNewAutomationSheet = { showNewAutomationSheet = false } - ) - } + DIYTabs.DIY -> { + DIYScreen( + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding, + showNewAutomationSheet = showNewAutomationSheet, + onDismissNewAutomationSheet = { showNewAutomationSheet = false } + ) + } - DIYTabs.APPS -> { - Box(modifier = Modifier.fillMaxSize()) { - val isLoading by updatesViewModel.isLoading + DIYTabs.APPS -> { + Box(modifier = Modifier.fillMaxSize()) { + val isLoading by updatesViewModel.isLoading - if (isLoading && trackedRepos.isEmpty()) { - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - androidx.compose.material3.LoadingIndicator() - } - } else if (trackedRepos.isEmpty()) { - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { + if (isLoading && trackedRepos.isEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = bottomToolbarPadding, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + androidx.compose.material3.LoadingIndicator() + } + } else if (trackedRepos.isEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = bottomToolbarPadding, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { Text( text = stringResource(R.string.msg_no_repos_tracked), style = MaterialTheme.typography.bodyLarge, @@ -734,14 +708,34 @@ class MainActivity : FragmentActivity() { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = androidx.compose.foundation.layout.PaddingValues( - top = innerPadding.calculateTopPadding() + 16.dp, - bottom = 150.dp, + contentPadding = PaddingValues( + bottom = bottomToolbarPadding, start = 16.dp, end = 16.dp ), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + item { + Spacer(modifier = Modifier.height(topContentPadding)) + } + item { + RoundedCardContainer { + AppsActionButtons( + view = view, + onAddClick = { + HapticUtil.performUIHaptic(view) + showAddRepoSheet = true + }, + onRefreshAllClick = { + HapticUtil.performUIHaptic(view) + updatesViewModel.checkForUpdates(context) + }, + isRefreshing = refreshingRepoIds.isNotEmpty(), + progress = { animatedProgress } + ) + } + } + // Pending Section if (pending.isNotEmpty()) { item { @@ -953,8 +947,9 @@ class MainActivity : FragmentActivity() { } } - // Mark app as ready after composing (happens very quickly) + // Mark app as ready after a short delay to ensure first frame is painted LaunchedEffect(Unit) { + kotlinx.coroutines.delay(100) isAppReady = true } } @@ -986,6 +981,58 @@ class MainActivity : FragmentActivity() { } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun AppsActionButtons( + view: android.view.View, + onAddClick: () -> Unit, + onRefreshAllClick: () -> Unit, + isRefreshing: Boolean, + progress: () -> Float +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = onAddClick, + modifier = Modifier.weight(1f), + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_add_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_add)) + } + + Button( + onClick = onRefreshAllClick, + modifier = Modifier.weight(1f), + enabled = !isRefreshing + ) { + if (isRefreshing) { + CircularWavyProgressIndicator( + progress = progress, + modifier = Modifier.size(18.dp) + ) + } else { + Icon( + painter = painterResource(id = R.drawable.rounded_refresh_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_refresh)) + } + } +} + @Composable private fun ImportExportButtons( view: android.view.View, diff --git a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt index c44307609..280f6fac0 100644 --- a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt @@ -23,14 +23,18 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues 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.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -41,8 +45,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -51,16 +53,17 @@ import androidx.compose.runtime.remember 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.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.core.app.ActivityCompat import com.sameerasw.essentials.domain.DIYTabs import com.sameerasw.essentials.domain.registry.PermissionRegistry -import com.sameerasw.essentials.ui.components.ReusableTopAppBar +import com.sameerasw.essentials.ui.components.SettingsFloatingToolbar import com.sameerasw.essentials.ui.components.cards.IconToggleItem import com.sameerasw.essentials.ui.components.cards.PermissionCard import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer @@ -68,6 +71,8 @@ import com.sameerasw.essentials.ui.components.dialogs.AboutSection import com.sameerasw.essentials.ui.components.pickers.DefaultTabPicker import com.sameerasw.essentials.ui.components.sheets.InstructionsBottomSheet import com.sameerasw.essentials.ui.components.sheets.UpdateBottomSheet +import com.sameerasw.essentials.ui.modifiers.BlurDirection +import com.sameerasw.essentials.ui.modifiers.progressiveBlur import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.PermissionUtils @@ -93,6 +98,10 @@ class SettingsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + val isDarkMode = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == + android.content.res.Configuration.UI_MODE_NIGHT_YES + window.setBackgroundDrawableResource(if (isDarkMode) android.R.color.black else R.color.app_window_background) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { window.isNavigationBarContrastEnforced = false } @@ -103,8 +112,6 @@ class SettingsActivity : ComponentActivity() { EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { val context = LocalContext.current val view = LocalView.current - val scrollBehavior = - TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) var showBugReportSheet by remember { mutableStateOf(false) } @@ -119,46 +126,57 @@ class SettingsActivity : ComponentActivity() { ) } - Scaffold( - contentWindowInsets = androidx.compose.foundation.layout.WindowInsets( - 0, - 0, - 0, - 0 - ), - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surfaceContainer, - topBar = { - ReusableTopAppBar( - title = "Settings", - hasBack = true, - hasSearch = false, - onBackClick = { finish() }, - scrollBehavior = scrollBehavior, - actions = { - androidx.compose.material3.IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showBugReportSheet = true - }, - colors = androidx.compose.material3.IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ), - modifier = Modifier.size(40.dp) - ) { + val statusBarHeightPx = with(LocalDensity.current) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + } + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainer) + .progressiveBlur( + blurRadius = 40f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP + ) + ) { + val contentPadding = androidx.compose.foundation.layout.PaddingValues( + top = statusBarHeight, + bottom = 150.dp, + start = 16.dp, + end = 16.dp + ) + + SettingsContent( + viewModel = viewModel, + contentPadding = contentPadding, + modifier = Modifier + .progressiveBlur( + blurRadius = 40f, + height = with(LocalDensity.current) { 150.dp.toPx() }, + direction = BlurDirection.BOTTOM + ) + ) + + SettingsFloatingToolbar( + title = stringResource(R.string.label_settings), + onBackClick = { finish() }, + modifier = Modifier + .align(Alignment.BottomCenter) + .zIndex(1f), + menuContent = { + MenuItem( + text = { Text(stringResource(R.string.action_report_bug)) }, + onClick = { showBugReportSheet = true }, + leadingIcon = { Icon( painter = painterResource(id = R.drawable.rounded_bug_report_24), - contentDescription = "Report Bug", - modifier = Modifier.size(24.dp) + contentDescription = null ) } - } - ) - } - ) { innerPadding -> - SettingsContent( - viewModel = viewModel, - modifier = Modifier.padding(innerPadding) + ) + } ) } } @@ -189,7 +207,11 @@ class SettingsActivity : ComponentActivity() { } @Composable -fun SettingsContent(viewModel: MainViewModel, modifier: Modifier = Modifier) { +fun SettingsContent( + viewModel: MainViewModel, + contentPadding: androidx.compose.foundation.layout.PaddingValues, + modifier: Modifier = Modifier +) { val isAccessibilityEnabled by viewModel.isAccessibilityEnabled val isWriteSecureSettingsEnabled by viewModel.isWriteSecureSettingsEnabled val isPostNotificationsEnabled by viewModel.isPostNotificationsEnabled @@ -275,10 +297,26 @@ fun SettingsContent(viewModel: MainViewModel, modifier: Modifier = Modifier) { modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), + .padding(contentPadding), verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.Start ) { + // Help Section + RoundedCardContainer { + IconToggleItem( + iconRes = R.drawable.rounded_help_24, + title = stringResource(R.string.label_help_guide), + isChecked = false, + onCheckedChange = { + showInstructionsSheet = true + }, + showToggle = false, + modifier = Modifier.fillMaxWidth().height(72.dp) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + // App Settings Section Text( text = "App Settings", diff --git a/app/src/main/java/com/sameerasw/essentials/data/model/DeviceSpecs.kt b/app/src/main/java/com/sameerasw/essentials/data/model/DeviceSpecs.kt new file mode 100644 index 000000000..afeb5c5e2 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/data/model/DeviceSpecs.kt @@ -0,0 +1,17 @@ +package com.sameerasw.essentials.data.model + +data class DeviceSpecItem( + val name: String, + val value: String +) + +data class DeviceSpecCategory( + val category: String, + val specifications: List +) + +data class DeviceSpecs( + val deviceName: String, + val detailSpec: List, + val imageUrls: List = emptyList() +) diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/BaseTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/BaseTileService.kt index 74af11225..1caacbfba 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/BaseTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/BaseTileService.kt @@ -2,11 +2,15 @@ package com.sameerasw.essentials.services.tiles import android.graphics.drawable.Icon import android.os.Build +import android.provider.Settings import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi import androidx.core.content.edit +import com.sameerasw.essentials.R import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.ShellUtils +import com.sameerasw.essentials.utils.PermissionUtils @RequiresApi(Build.VERSION_CODES.N) abstract class BaseTileService : TileService() { @@ -65,8 +69,9 @@ abstract class BaseTileService : TileService() { } tile.label = getTileLabel() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - tile.subtitle = if (!hasPerm) "Missing permissions" else getTileSubtitle() + tile.subtitle = if (!hasPerm) getString(R.string.permission_missing) else getTileSubtitle() } + val icon = getTileIcon() if (icon != null) { tile.icon = icon @@ -75,6 +80,33 @@ abstract class BaseTileService : TileService() { } protected abstract fun getTileState(): Int + + protected fun getSecureInt(key: String, def: Int): Int { + try { + val value = Settings.Secure.getInt(contentResolver, key, -1) + if (value != -1) return value + } catch (_: SecurityException) { + // Only fallback to shell on SecurityException + return try { + val output = ShellUtils.runCommandWithOutput(this, "settings get secure $key") + output?.toIntOrNull() ?: def + } catch (_: Exception) { + def + } + } catch (_: Exception) { + return def + } + return def + } + + protected fun putSecureInt(key: String, value: Int) { + try { + Settings.Secure.putInt(contentResolver, key, value) + } catch (_: Exception) { + // Fallback to shell if standard API fails + ShellUtils.runCommand(this, "settings put secure $key $value") + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ChargeQuickTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ChargeQuickTileService.kt index f9615e937..f89889309 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ChargeQuickTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ChargeQuickTileService.kt @@ -43,23 +43,23 @@ class ChargeQuickTileService : BaseTileService() { } override fun onTileClick() { - val adaptiveChargingEnabled = Settings.Secure.getInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) == 1 - val chargeOptimizationEnabled = Settings.Secure.getInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) == 1 + val adaptiveChargingEnabled = getSecureInt(ADAPTIVE_CHARGING_SETTING, 0) == 1 + val chargeOptimizationEnabled = getSecureInt(CHARGE_OPTIMIZATION_MODE, 0) == 1 when { adaptiveChargingEnabled -> { - Settings.Secure.putInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 1) - Settings.Secure.putInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) + putSecureInt(CHARGE_OPTIMIZATION_MODE, 1) + putSecureInt(ADAPTIVE_CHARGING_SETTING, 0) } chargeOptimizationEnabled -> { - Settings.Secure.putInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) - Settings.Secure.putInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) + putSecureInt(CHARGE_OPTIMIZATION_MODE, 0) + putSecureInt(ADAPTIVE_CHARGING_SETTING, 0) } else -> { - Settings.Secure.putInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) - Settings.Secure.putInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 1) + putSecureInt(CHARGE_OPTIMIZATION_MODE, 0) + putSecureInt(ADAPTIVE_CHARGING_SETTING, 1) } } } @@ -67,8 +67,8 @@ class ChargeQuickTileService : BaseTileService() { override fun getTileLabel(): String = getString(R.string.tile_charge_optimization) override fun getTileSubtitle(): String { - val adaptiveChargingEnabled = Settings.Secure.getInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) == 1 - val chargeOptimizationEnabled = Settings.Secure.getInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) == 1 + val adaptiveChargingEnabled = getSecureInt(ADAPTIVE_CHARGING_SETTING, 0) == 1 + val chargeOptimizationEnabled = getSecureInt(CHARGE_OPTIMIZATION_MODE, 0) == 1 return when { chargeOptimizationEnabled -> getString(R.string.limit_to_80) adaptiveChargingEnabled -> getString(R.string.adaptive_charging) @@ -77,12 +77,15 @@ class ChargeQuickTileService : BaseTileService() { } override fun hasFeaturePermission(): Boolean { - return PermissionUtils.canWriteSecureSettings(this) + return PermissionUtils.canWriteSecureSettings(this) && + com.sameerasw.essentials.utils.ShellUtils.hasPermission(this) && + com.sameerasw.essentials.utils.ShellUtils.isAvailable(this) } + override fun getTileIcon(): Icon { - val adaptiveChargingEnabled = Settings.Secure.getInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) == 1 - val chargeOptimizationEnabled = Settings.Secure.getInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) == 1 + val adaptiveChargingEnabled = getSecureInt(ADAPTIVE_CHARGING_SETTING, 0) == 1 + val chargeOptimizationEnabled = getSecureInt(CHARGE_OPTIMIZATION_MODE, 0) == 1 val resId = when { chargeOptimizationEnabled -> R.drawable.rounded_battery_android_frame_shield_24 adaptiveChargingEnabled -> R.drawable.rounded_battery_android_frame_plus_24 @@ -92,8 +95,8 @@ class ChargeQuickTileService : BaseTileService() { } override fun getTileState(): Int { - val adaptiveChargingEnabled = Settings.Secure.getInt(contentResolver, ADAPTIVE_CHARGING_SETTING, 0) == 1 - val chargeOptimizationEnabled = Settings.Secure.getInt(contentResolver, CHARGE_OPTIMIZATION_MODE, 0) == 1 + val adaptiveChargingEnabled = getSecureInt(ADAPTIVE_CHARGING_SETTING, 0) == 1 + val chargeOptimizationEnabled = getSecureInt(CHARGE_OPTIMIZATION_MODE, 0) == 1 return if (chargeOptimizationEnabled || adaptiveChargingEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt index 58ac2a406..bc90c7e32 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt @@ -173,6 +173,7 @@ class AppFreezingActivity : ComponentActivity() { }, floatingActionButton = { ExpandableFreezeFab( + modifier = Modifier.padding(bottom = 16.dp, end = 16.dp), onUnfreezeAll = { viewModel.unfreezeAllApps(context) }, onFreezeAll = { viewModel.freezeAllApps(context) }, onFreezeAutomatic = { viewModel.freezeAutomaticApps(context) } @@ -346,6 +347,7 @@ fun AppGridItem( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ExpandableFreezeFab( + modifier: Modifier = Modifier, onUnfreezeAll: () -> Unit, onFreezeAll: () -> Unit, onFreezeAutomatic: () -> Unit @@ -355,6 +357,7 @@ fun ExpandableFreezeFab( BackHandler(fabMenuExpanded) { fabMenuExpanded = false } FloatingActionButtonMenu( + modifier = modifier, expanded = fabMenuExpanded, button = { ToggleFloatingActionButton( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt new file mode 100644 index 000000000..924ae7a84 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt @@ -0,0 +1,254 @@ +package com.sameerasw.essentials.ui.activities + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +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.layout.statusBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.background +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.model.DeviceSpecs +import com.sameerasw.essentials.ui.components.DeviceHeroCard +import com.sameerasw.essentials.ui.components.DeviceSpecsCard +import com.sameerasw.essentials.ui.components.SettingsFloatingToolbar +import com.sameerasw.essentials.ui.modifiers.BlurDirection +import com.sameerasw.essentials.ui.modifiers.progressiveBlur +import com.sameerasw.essentials.ui.theme.EssentialsTheme +import com.sameerasw.essentials.utils.DeviceInfo +import com.sameerasw.essentials.utils.DeviceUtils +import com.sameerasw.essentials.utils.GSMArenaService +import com.sameerasw.essentials.utils.HapticUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class YourAndroidViewModel : ViewModel() { + private val _deviceSpecs = MutableStateFlow(null) + val deviceSpecs = _deviceSpecs.asStateFlow() + + private val _isSpecsLoading = MutableStateFlow(true) + val isSpecsLoading = _isSpecsLoading.asStateFlow() + + var hasRunStartupAnimation = false + + fun loadDeviceSpecs(deviceInfo: DeviceInfo) { + if (_deviceSpecs.value != null) { + _isSpecsLoading.value = false + return + } + + viewModelScope.launch { + _isSpecsLoading.value = true + val specs = withContext(Dispatchers.IO) { + // Remove generic terms from the hardware name + val brand = deviceInfo.manufacturer.replace("Google", "") + .replace("samsung", "Samsung") + .trim() + val model = deviceInfo.model + .replace("Pixel", "") + .replace("Galaxy", "") + .trim() + + // Fallback to simpler search + val searchBrand = if(brand.isEmpty()) deviceInfo.manufacturer else brand + + GSMArenaService.fetchSpecs(brand = searchBrand, model = model) + } + _deviceSpecs.value = specs + _isSpecsLoading.value = false + } + } +} + +class YourAndroidActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val isDarkMode = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES + window.setBackgroundDrawableResource(if (isDarkMode) android.R.color.black else R.color.app_window_background) + + setContent { + val mainViewModel: com.sameerasw.essentials.viewmodels.MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel() + val isPitchBlackThemeEnabled by mainViewModel.isPitchBlackThemeEnabled + + val viewModel: YourAndroidViewModel = androidx.lifecycle.viewmodel.compose.viewModel() + val deviceSpecs by viewModel.deviceSpecs.collectAsState() + val isSpecsLoading by viewModel.isSpecsLoading.collectAsState() + val context = androidx.compose.ui.platform.LocalContext.current + val deviceInfo = remember { DeviceUtils.getDeviceInfo(context) } + + LaunchedEffect(Unit) { + mainViewModel.check(context) + viewModel.loadDeviceSpecs(deviceInfo) + } + + EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { + val statusBarHeightPx = with(LocalDensity.current) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainer) + .progressiveBlur( + blurRadius = 40f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP + ) + ) { + YourAndroidContent( + deviceInfo = deviceInfo, + deviceSpecs = deviceSpecs, + isSpecsLoading = isSpecsLoading, + hasRunStartupAnimation = viewModel.hasRunStartupAnimation, + onAnimationRun = { viewModel.hasRunStartupAnimation = true }, + modifier = Modifier.fillMaxSize() + ) + + SettingsFloatingToolbar( + title = stringResource(R.string.tab_your_android), + onBackClick = { finish() }, + modifier = Modifier + .align(Alignment.BottomCenter) + .zIndex(1f) + ) + } + } + } + } +} + +@Composable +fun YourAndroidContent( + deviceInfo: DeviceInfo, + deviceSpecs: DeviceSpecs?, + isSpecsLoading: Boolean, + hasRunStartupAnimation: Boolean, + onAnimationRun: () -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp) +) { + var isStartupAnimationRunning by remember { mutableStateOf(hasRunStartupAnimation) } + + LaunchedEffect(hasRunStartupAnimation) { + if (!hasRunStartupAnimation) { + delay(100) + isStartupAnimationRunning = true + onAnimationRun() + } + } + + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + val initialImageOffset = (screenHeight / 2) - 240.dp - 64.dp + + val imageOffsetState = animateDpAsState( + targetValue = if (isStartupAnimationRunning) 0.dp else initialImageOffset, + animationSpec = tween(durationMillis = 850, easing = FastOutSlowInEasing), + label = "imageOffset" + ) + + val contentAlphaState = animateFloatAsState( + targetValue = if (isStartupAnimationRunning) 1f else 0f, + animationSpec = tween(durationMillis = 750, delayMillis = 350, easing = LinearEasing), + label = "contentAlpha" + ) + + val contentOffsetState = animateDpAsState( + targetValue = if (isStartupAnimationRunning) 0.dp else 40.dp, + animationSpec = tween( + durationMillis = 750, + delayMillis = 350, + easing = FastOutSlowInEasing + ), + label = "contentOffset" + ) + + Column( + modifier = modifier + .fillMaxSize() + .progressiveBlur( + blurRadius = 40f, + height = with(LocalDensity.current) { 150.dp.toPx() }, + direction = BlurDirection.BOTTOM + ) + .verticalScroll(rememberScrollState()) + .padding( + top = contentPadding.calculateTopPadding() + WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + bottom = 150.dp, + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + DeviceHeroCard( + deviceInfo = deviceInfo, + deviceSpecs = deviceSpecs, + imageOffset = { imageOffsetState.value }, + contentAlpha = { contentAlphaState.value }, + contentOffset = { contentOffsetState.value } + ) + + DeviceSpecsCard( + deviceSpecs = deviceSpecs, + isLoading = isSpecsLoading, + modifier = Modifier.graphicsLayer { + alpha = contentAlphaState.value + translationY = contentOffsetState.value.toPx() + } + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/DIYFloatingToolbar.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/DIYFloatingToolbar.kt index 419cc3f4a..810794397 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/DIYFloatingToolbar.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/DIYFloatingToolbar.kt @@ -45,7 +45,8 @@ fun DIYFloatingToolbar( tabs: List, onTabSelected: (Int) -> Unit, scrollBehavior: FloatingToolbarScrollBehavior, - badges: Map = emptyMap() + badges: Map = emptyMap(), + floatingActionButton: @Composable () -> Unit = {} ) { // Persistent visibility var expanded by remember { mutableStateOf(true) } @@ -54,8 +55,10 @@ fun DIYFloatingToolbar( modifier = modifier .windowInsetsPadding( androidx.compose.foundation.layout.WindowInsets.navigationBars - ), + ) + .padding(start = 16.dp, end = 16.dp, bottom = 0.dp), expanded = expanded, + floatingActionButton = floatingActionButton, scrollBehavior = scrollBehavior, colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors( toolbarContentColor = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt new file mode 100644 index 000000000..587b1f50c --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt @@ -0,0 +1,323 @@ +package com.sameerasw.essentials.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.model.DeviceSpecs +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.theme.Shapes +import com.sameerasw.essentials.ui.components.modifiers.shimmer +import com.sameerasw.essentials.utils.DeviceInfo +import com.sameerasw.essentials.utils.DeviceUtils +import com.sameerasw.essentials.utils.DeviceImageMapper + +@Composable +fun DeviceHeroCard( + deviceInfo: DeviceInfo, + deviceSpecs: DeviceSpecs? = null, + imageOffset: () -> Dp = { 0.dp }, + contentAlpha: () -> Float = { 1f }, + contentOffset: () -> Dp = { 0.dp }, + modifier: Modifier = Modifier +) { + val imageUrls = deviceSpecs?.imageUrls ?: emptyList() + val pageCount = 1 + imageUrls.size + val pagerState = rememberPagerState(pageCount = { pageCount }) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .graphicsLayer { + translationY = imageOffset().toPx() + } + .fillMaxWidth() + .height(480.dp) + .clip(MaterialTheme.shapes.medium), + contentAlignment = Alignment.Center + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { page -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (page == 0) { + // stylized vector + Icon( + painter = painterResource( + id = DeviceImageMapper.getDeviceDrawable( + deviceInfo.model + ) + ), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .fillMaxHeight(0.85f) + .fillMaxWidth(0.85f) + ) + } else { + // real image from gsmarena + AsyncImage( + model = imageUrls[page - 1], + contentDescription = "Device Image", + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxHeight(0.85f) + .fillMaxWidth(0.85f) + .clip(RoundedCornerShape(24.dp)) + .shimmer() + ) + } + } + } + + // Page Indicator dots + if (pageCount > 1) { + Row( + Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + repeat(pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) + Box( + modifier = Modifier + .size(8.dp) + .clip(MaterialTheme.shapes.small) + .background(color) + ) + } + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // User-set Device Name + Text( + text = deviceInfo.deviceName, + modifier = Modifier.graphicsLayer { + alpha = contentAlpha() + translationY = contentOffset().toPx() + }, + style = MaterialTheme.typography.headlineMedium.copy( + fontFamily = null + ), + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + // Manufacturer Model + Text( + text = "${deviceInfo.manufacturer.replaceFirstChar { it.uppercase() }} ${deviceInfo.model} (${deviceInfo.hardware})", + modifier = Modifier.graphicsLayer { + alpha = contentAlpha() + translationY = contentOffset().toPx() + }, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + +// Spacer(modifier = Modifier.height(24.dp)) + + } + + RoundedCardContainer( + modifier = modifier + .fillMaxWidth() + .graphicsLayer { + alpha = contentAlpha() + translationY = contentOffset().toPx() + }, + ) { + + + Row( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + painter = painterResource(id = DeviceImageMapper.getAndroidLogo(deviceInfo)), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.size(56.dp) + ) + Column(horizontalAlignment = Alignment.Start) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Android ${deviceInfo.androidVersion} (${ + DeviceUtils.getOSName( + deviceInfo.sdkInt, + deviceInfo.osCodename + ) + })", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + val isBeta = deviceInfo.buildTag.lowercase().contains("beta") + val isCanary = deviceInfo.buildTag.lowercase().contains("canary") + + if (isBeta || isCanary) { + Spacer(modifier = Modifier.size(8.dp)) + Box( + modifier = Modifier + .background( + MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.large + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = if (isCanary) "CANARY" else "BETA", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + Text( + text = "API ${deviceInfo.sdkInt} • Patch: ${ + DeviceUtils.formatSecurityPatch( + deviceInfo.securityPatch + ) + }", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Build: ${deviceInfo.display}", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) + ) + } + } + } + + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Storage and Memory Info + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + // Storage Section + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_dns_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Column(horizontalAlignment = Alignment.Start) { + Text( + text = stringResource(R.string.label_device_storage), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = DeviceUtils.formatHardwareSize(deviceInfo.totalStorage), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + + // Memory Section + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_memory_alt_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Column(horizontalAlignment = Alignment.Start) { + Text( + text = stringResource(R.string.label_device_ram), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = DeviceUtils.formatHardwareSize(deviceInfo.totalRam), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceSpecsCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceSpecsCard.kt new file mode 100644 index 000000000..914b50760 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceSpecsCard.kt @@ -0,0 +1,222 @@ +package com.sameerasw.essentials.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.data.model.DeviceSpecs +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.modifiers.shimmer +import com.sameerasw.essentials.ui.theme.Shapes + +@Composable +fun DeviceSpecsCard( + deviceSpecs: DeviceSpecs?, + isLoading: Boolean, + modifier: Modifier = Modifier +) { + RoundedCardContainer( + modifier = modifier.fillMaxWidth(), + ) { + // Section Header + SpecHeader("Device Specifications") + + if (isLoading) { + repeat(5) { + LoadingSpecSection() + } + } else if (deviceSpecs != null) { + deviceSpecs.detailSpec.forEach { category -> + SpecSection( + title = category.category, + specs = category.specifications.map { it.name to it.value } + ) + } + + if (deviceSpecs.imageUrls.isNotEmpty()) { + SpecFooter(deviceSpecs) + } + + } else { + // Fallback or empty state + Box( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .padding(32.dp), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + Text( + text = "Specifications unavailable", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun LoadingSpecSection() { + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp) + ) { + Box( + modifier = Modifier + .width(100.dp) + .height(16.dp) + .clip(Shapes.extraSmall) + .shimmer() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + repeat(3) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Box( + modifier = Modifier + .width(80.dp) + .height(12.dp) + .clip(Shapes.extraSmall) + .shimmer() + ) + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .weight(1f) + .height(12.dp) + .clip(Shapes.extraSmall) + .shimmer() + ) + } + } + } +} + +@Composable +private fun SpecHeader(title: String) { + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } +} + + +@Composable +private fun SpecFooter(deviceSpecs: DeviceSpecs?) { + Row( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + text = "Powered by", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + + val uriHandler = androidx.compose.ui.platform.LocalUriHandler.current + Text( + text = "GSMArena.com", + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.primary, + textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline + ), + modifier = Modifier + .clickable { + uriHandler.openUri("https://www.gsmarena.com") + } + .padding(start = 8.dp) + ) + } +} + +@Composable +private fun SpecSection( + title: String, + specs: List> +) { + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + shape = Shapes.extraSmall + ) + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.secondary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + specs.forEach { (label, value) -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Text( + text = label, + modifier = Modifier.width(100.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium + ) + Text( + text = value, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/SettingsFloatingToolbar.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/SettingsFloatingToolbar.kt new file mode 100644 index 000000000..037998417 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/SettingsFloatingToolbar.kt @@ -0,0 +1,151 @@ +package com.sameerasw.essentials.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingToolbarDefaults +import androidx.compose.material3.HorizontalFloatingToolbar +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import androidx.compose.foundation.layout.RowScope +import androidx.compose.ui.Alignment + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SettingsFloatingToolbar( + title: String, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + menuContent: (@Composable SettingsMenuScope.() -> Unit)? = null +) { + var menuExpanded by remember { mutableStateOf(false) } + + if (menuContent != null) { + HorizontalFloatingToolbar( + modifier = modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(start = 16.dp, end = 16.dp, bottom = 0.dp), + expanded = true, + floatingActionButton = { + Box { + FloatingActionButton( + onClick = { menuExpanded = true }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + shape = MaterialTheme.shapes.large, + elevation = androidx.compose.material3.FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_more_vert_24), + contentDescription = stringResource(R.string.content_desc_more_options) + ) + } + + SegmentedDropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + val scope = SettingsMenuScope(dismissMenu = { menuExpanded = false }) + scope.menuContent() + } + } + }, + colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors( + toolbarContentColor = MaterialTheme.colorScheme.onSurface, + toolbarContainerColor = MaterialTheme.colorScheme.primary, + ), + content = { + ToolbarContent(title, onBackClick) + } + ) + } else { + // Use the variant without the FAB to avoid reserving space + HorizontalFloatingToolbar( + modifier = modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + expanded = true, + colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors( + toolbarContentColor = MaterialTheme.colorScheme.onSurface, + toolbarContainerColor = MaterialTheme.colorScheme.primary, + ), + content = { + ToolbarContent(title, onBackClick) + } + ) + } +} + +@Composable +private fun RowScope.ToolbarContent( + title: String, + onBackClick: () -> Unit +) { + IconButton( + onClick = onBackClick, + modifier = Modifier.align(Alignment.CenterVertically), + colors = IconButtonDefaults.filledIconButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.background + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_back_24), + contentDescription = stringResource(R.string.content_desc_back), + modifier = Modifier.size(24.dp) + ) + } + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.background, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .widthIn(min = 100.dp, max = 300.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) +} + +class SettingsMenuScope(val dismissMenu: () -> Unit) { + @Composable + fun MenuItem( + text: @Composable () -> Unit, + onClick: () -> Unit, + leadingIcon: (@Composable () -> Unit)? = null + ) { + SegmentedDropdownMenuItem( + text = text, + onClick = { + onClick() + dismissMenu() + }, + leadingIcon = leadingIcon + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt index d09b3d118..d8d54672e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt @@ -146,7 +146,10 @@ fun FeatureCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(text = resolvedTitle) + Text( + text = resolvedTitle, + color = MaterialTheme.colorScheme.onSurface + ) if (isBeta) { Card( colors = CardDefaults.cardColors( @@ -195,7 +198,8 @@ fun FeatureCard( .padding(end = 12.dp) .size(24.dp), painter = painterResource(id = R.drawable.rounded_chevron_right_24), - contentDescription = "More settings" + contentDescription = "More settings", + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt index 67aa29049..2e80a78b5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt @@ -70,7 +70,8 @@ fun IconToggleItem( Column(modifier = Modifier.weight(1f)) { Text( text = title, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface ) Text( text = description, @@ -82,7 +83,8 @@ fun IconToggleItem( Text( text = title, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/containers/RoundedCardContainer.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/containers/RoundedCardContainer.kt index 2de3c94e2..6f294df16 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/containers/RoundedCardContainer.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/containers/RoundedCardContainer.kt @@ -9,13 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun RoundedCardContainer( modifier: Modifier = Modifier, - spacing: androidx.compose.ui.unit.Dp = 2.dp, - cornerRadius: androidx.compose.ui.unit.Dp = 24.dp, + spacing: Dp = 2.dp, + cornerRadius: Dp = 24.dp, containerColor: Color = Color.Transparent, content: @Composable ColumnScope.() -> Unit ) { @@ -27,4 +28,3 @@ fun RoundedCardContainer( content = content ) } - diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/modifiers/ShimmerModifier.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/modifiers/ShimmerModifier.kt new file mode 100644 index 000000000..d47c76f69 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/modifiers/ShimmerModifier.kt @@ -0,0 +1,55 @@ +package com.sameerasw.essentials.ui.components.modifiers + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +fun Modifier.shimmer(): Modifier = composed { + val transition = rememberInfiniteTransition(label = "shimmer") + val translateAnim = transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + easing = FastOutSlowInEasing + ), + repeatMode = RepeatMode.Restart + ), + label = "shimmer_translate" + ) + + val shimmerColors = remember { + listOf( + Color.Unspecified, + Color.Unspecified, + Color.Unspecified, + ) + }.let { + listOf( + MaterialTheme.colorScheme.surfaceContainerHighest, + MaterialTheme.colorScheme.surfaceContainerHigh, + MaterialTheme.colorScheme.surfaceContainerHighest, + ) + } + + this.drawBehind { + val brush = Brush.linearGradient( + colors = shimmerColors, + start = Offset(translateAnim.value - 500f, translateAnim.value - 500f), + end = Offset(translateAnim.value, translateAnim.value) + ) + drawRect(brush) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt index 6f8b43cc6..6d01bbf19 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/DIYScreen.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme @@ -33,6 +35,7 @@ import com.sameerasw.essentials.viewmodels.DIYViewModel fun DIYScreen( modifier: Modifier = Modifier, viewModel: DIYViewModel = viewModel(), + contentPadding: PaddingValues = PaddingValues(0.dp), showNewAutomationSheet: Boolean = false, onDismissNewAutomationSheet: () -> Unit = {} ) { @@ -46,8 +49,7 @@ fun DIYScreen( .fillMaxSize() .pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) - } - .padding(16.dp), + }, verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start ) { @@ -68,8 +70,15 @@ fun DIYScreen( LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(bottom = 150.dp) + contentPadding = PaddingValues( + bottom = contentPadding.calculateBottomPadding(), + start = 16.dp, + end = 16.dp + ) ) { + item { + Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding())) + } if (enabledAutomations.isNotEmpty()) { item { Text( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt index 6dcba1fa5..9452c0b7a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt @@ -2,6 +2,8 @@ package com.sameerasw.essentials.ui.composables import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -16,14 +18,17 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonGroupDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -33,7 +38,9 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,6 +50,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -71,7 +79,6 @@ fun FreezeGridUI( val pickedApps by viewModel.freezePickedApps val isPickedAppsLoading by viewModel.isFreezePickedAppsLoading - val gridState = rememberLazyGridState() val frozenStates = remember { mutableStateMapOf() } val lifecycleOwner = LocalLifecycleOwner.current @@ -129,41 +136,226 @@ fun FreezeGridUI( ) } } else { - RoundedCardContainer( + val isShizukuAvailable by viewModel.isShizukuAvailable + val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted + var isMenuExpanded by remember { mutableStateOf(false) } + val scrollState = androidx.compose.foundation.rememberScrollState() + + val exportLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { + try { + context.contentResolver.openOutputStream(it)?.use { stream -> + viewModel.exportFreezeApps(stream) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + val importLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { + try { + context.contentResolver.openInputStream(it)?.use { stream -> + viewModel.importFreezeApps(context, stream) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + Column( modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 16.dp), + .fillMaxSize() + .verticalScroll(scrollState) ) { - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 88.dp), - state = gridState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues( - bottom = 150.dp, - top = 0.dp - ), - horizontalArrangement = Arrangement.spacedBy(2.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding())) + Spacer(modifier = Modifier.height(16.dp)) + + RoundedCardContainer( + modifier = Modifier + .padding(horizontal = 16.dp), ) { - items(pickedApps, key = { it.packageName }) { app -> - AppGridItem( - app = app, - isFrozen = frozenStates[app.packageName] ?: false, - isAutoFreezeEnabled = app.isEnabled, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.launchAndUnfreezeApp( - context, - app.packageName + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = MaterialTheme.shapes.extraSmall ) - // We don't finish() here since this is a tab - }, - onLongClick = { - ShortcutUtil.pinAppShortcut(context, app) + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + verticalAlignment = Alignment.CenterVertically + ) { + // Freeze Button + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.freezeAllAuto(context) + }, + modifier = Modifier.weight(1f), + enabled = isShizukuAvailable && isShizukuPermissionGranted, + shape = ButtonDefaults.shape + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.action_freeze)) + } + + // Unfreeze Button + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.unfreezeAllAuto(context) + }, + modifier = Modifier.weight(1f), + enabled = isShizukuAvailable && isShizukuPermissionGranted, + shape = ButtonDefaults.shape + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.action_unfreeze)) + } + + // More Menu Button + IconButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + isMenuExpanded = true + }, + enabled = isShizukuAvailable && isShizukuPermissionGranted + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_more_vert_24), + contentDescription = stringResource(R.string.content_desc_more_options) + ) + + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.action_freeze_all)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.freezeAllManual(context) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.action_unfreeze_all)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.unfreezeAllManual(context) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.action_export_freeze)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + exportLauncher.launch("freeze_apps_backup.json") + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_warm_up_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.action_import_freeze)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + importLauncher.launch(arrayOf("application/json")) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_cool_down_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + } + } + } + + // App Grid Items + val chunkedApps = pickedApps.chunked(4) + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + chunkedApps.forEach { rowApps -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + rowApps.forEach { app -> + Box(modifier = Modifier.weight(1f)) { + AppGridItem( + app = app, + isFrozen = frozenStates[app.packageName] ?: false, + isAutoFreezeEnabled = app.isEnabled, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.launchAndUnfreezeApp( + context, + app.packageName + ) + }, + onLongClick = { + ShortcutUtil.pinAppShortcut(context, app) + } + ) + } + } + repeat(4 - rowApps.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } } - ) + } } } + + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) } } } @@ -183,7 +375,7 @@ fun AppGridItem( Surface( shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, + color = MaterialTheme.colorScheme.surfaceBright, modifier = Modifier .fillMaxWidth() .combinedClickable( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt index d88bfb4e0..6750f9777 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt @@ -9,8 +9,11 @@ import android.provider.Settings import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer 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.layout.size import androidx.compose.foundation.rememberScrollState @@ -23,6 +26,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -34,16 +39,27 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.OutlinedCard +import androidx.compose.ui.graphics.Color import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.sameerasw.essentials.FeatureSettingsActivity import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.activities.YourAndroidActivity import com.sameerasw.essentials.domain.registry.FeatureRegistry import com.sameerasw.essentials.domain.registry.PermissionRegistry import com.sameerasw.essentials.ui.components.FavoriteCarousel @@ -51,17 +67,20 @@ import com.sameerasw.essentials.ui.components.cards.FeatureCard import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.sheets.PermissionItem import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.BiometricSecurityHelper +import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel import kotlinx.coroutines.delay private const val FEATURE_MAPS_POWER_SAVING = R.string.feat_maps_power_saving_title -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SetupFeatures( viewModel: MainViewModel, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), searchRequested: Boolean = false, onSearchHandled: () -> Unit = {}, onHelpClick: () -> Unit = {} @@ -758,9 +777,13 @@ fun SetupFeatures( } val scrollState = rememberScrollState() + val view = LocalView.current val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current var isFocused by remember { mutableStateOf(false) } + + val pullRefreshState = rememberPullToRefreshState() + var isRefreshing by remember { mutableStateOf(false) } val allFeatures = FeatureRegistry.ALL_FEATURES @@ -772,20 +795,132 @@ fun SetupFeatures( onSearchHandled() } } + + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + HapticUtil.performUIHaptic(view) + context.startActivity(Intent(context, YourAndroidActivity::class.java)) + delay(500) + isRefreshing = false + } + } - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(scrollState) - .pointerInput(Unit) { - detectTapGestures(onTap = { focusManager.clearFocus() }) + var lastHapticBucket by remember { mutableStateOf(0) } + LaunchedEffect(pullRefreshState.distanceFraction) { + val fraction = pullRefreshState.distanceFraction + val currentBucket = (fraction * 10).toInt() + + if (fraction >= 1f && lastHapticBucket < 10) { + HapticUtil.performUIHaptic(view) + lastHapticBucket = 10 + } else if (fraction < 1f && currentBucket != lastHapticBucket) { + if (currentBucket > lastHapticBucket) { + HapticUtil.performSliderHaptic(view) } - .padding(top = 16.dp, bottom = 150.dp), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start + lastHapticBucket = currentBucket + } + + if (fraction == 0f) { + lastHapticBucket = 0 + } + } + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { isRefreshing = true }, + state = pullRefreshState, + indicator = { }, + modifier = modifier.fillMaxSize() ) { - OutlinedTextField( - value = viewModel.searchQuery.value, + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start + ) { + Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding())) + + val deviceInfo = remember { DeviceUtils.getDeviceInfo(context) } + val fraction = pullRefreshState.distanceFraction + val thresholdPassed = fraction >= 1f + + val cardExpansion by androidx.compose.animation.core.animateDpAsState( + targetValue = 120.dp * fraction.coerceIn(0f, 1f), + animationSpec = androidx.compose.animation.core.spring(stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow), + label = "cardExpansion" + ) + + val containerColor by androidx.compose.animation.animateColorAsState( + targetValue = if (thresholdPassed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLow, + animationSpec = androidx.compose.animation.core.tween(durationMillis = 300), + label = "containerColor" + ) + + val contentColor by androidx.compose.animation.animateColorAsState( + targetValue = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + animationSpec = androidx.compose.animation.core.tween(durationMillis = 300), + label = "contentColor" + ) + + val arrowColor by androidx.compose.animation.animateColorAsState( + targetValue = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, + animationSpec = androidx.compose.animation.core.tween(durationMillis = 300), + label = "arrowColor" + ) + + val borderColor by androidx.compose.animation.animateColorAsState( + targetValue = if (thresholdPassed) Color.Transparent else MaterialTheme.colorScheme.outlineVariant, + animationSpec = androidx.compose.animation.core.tween(durationMillis = 300), + label = "borderColor" + ) + + // My Android + OutlinedCard( + onClick = { + HapticUtil.performUIHaptic(view) + context.startActivity(Intent(context, YourAndroidActivity::class.java)) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 0.dp) + .height(64.dp + cardExpansion), + shape = MaterialTheme.shapes.extraLarge, + colors = CardDefaults.outlinedCardColors( + containerColor = containerColor, + contentColor = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ), + border = BorderStroke(1.dp, borderColor) + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Rounded.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(24.dp).graphicsLayer { + rotationZ = (fraction * 180f).coerceIn(0f, 180f) + }, + tint = contentColor + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = deviceInfo.deviceName, + style = MaterialTheme.typography.titleMedium, + fontWeight = androidx.compose.ui.text.font.FontWeight.Medium, + color = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + + OutlinedTextField( + value = viewModel.searchQuery.value, onValueChange = { new -> viewModel.onSearchQueryChanged(new, context) }, @@ -997,9 +1132,69 @@ fun SetupFeatures( } } else null ) + } + } + + } else { + val topLevelFeatures = + allFeatures.filter { it.parentFeatureId == null && it.isVisibleInMain } + + if (topLevelFeatures.isNotEmpty()) { + RoundedCardContainer( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + topLevelFeatures.forEachIndexed { index, feature -> + FeatureCard( + title = feature.title, + isEnabled = feature.isEnabled(viewModel), + onToggle = { enabled -> + BiometricSecurityHelper.runWithAuth( + activity = context as FragmentActivity, + feature = feature, + isToggle = true, + action = { + feature.onToggle(viewModel, context, enabled) + } + ) + }, + onClick = { + BiometricSecurityHelper.runWithAuth( + activity = context as FragmentActivity, + feature = feature, + action = { + feature.onClick(context, viewModel) + } + ) + }, + iconRes = feature.iconRes, + modifier = Modifier.padding(horizontal = 0.dp, vertical = 0.dp), + isToggleEnabled = feature.isToggleEnabled(viewModel, context), + showToggle = feature.showToggle, + hasMoreSettings = feature.hasMoreSettings, + onDisabledToggleClick = { + currentFeature = feature.title + showSheet = true + }, + description = feature.description, + isBeta = feature.isBeta, + isPinned = pinnedFeatureKeys.contains(feature.id), + onPinToggle = { + viewModel.togglePinFeature(feature.id) + }, + onHelpClick = if (feature.aboutDescription != null) { + { + selectedHelpFeature = feature + showHelpSheet = true + } + } else null + ) + } + } } } + + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt index 6e6237398..91c925e06 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt @@ -275,9 +275,10 @@ fun QuickSettingsTilesSettingsUI( R.string.tile_charge_optimization, R.drawable.rounded_battery_android_frame_shield_24, ChargeQuickTileService::class.java, - listOf("WRITE_SECURE_SETTINGS"), + if (ShellUtils.isRootEnabled(context)) listOf("ROOT") else listOf("SHIZUKU", "WRITE_SECURE_SETTINGS"), R.string.about_desc_charge_optimization ) + ) if (showPermissionSheet && selectedTileForPermissions != null) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt index 44cdcef5b..2257ed43e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt @@ -130,8 +130,10 @@ fun WatermarkScreen( performUIHaptic(view) showEditSheet = true }, + modifier = Modifier.padding(bottom = 16.dp, end = 16.dp), containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + elevation = androidx.compose.material3.FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp) ) { Icon( painter = painterResource(R.drawable.rounded_edit_24), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt b/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt new file mode 100644 index 000000000..88e58a7e0 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt @@ -0,0 +1,91 @@ +package com.sameerasw.essentials.ui.modifiers + +import android.graphics.RenderEffect +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asComposeRenderEffect +import androidx.compose.ui.graphics.graphicsLayer +import org.intellij.lang.annotations.Language + +enum class BlurDirection { + TOP, BOTTOM +} + +@Language("AGSL") +private val PROGRESSIVE_BLUR_SKSL = """ + uniform shader content; + uniform float blurRadius; + uniform float height; + uniform float contentHeight; + uniform int isTop; + + half4 main(float2 fragCoord) { + float progress; + if (isTop == 1) { + progress = 1.0 - clamp(fragCoord.y / height, 0.0, 1.0); + } else { + progress = 1.0 - clamp((contentHeight - fragCoord.y) / height, 0.0, 1.0); + } + + // Easing curve for smoother transition (power curve) + progress = pow(progress, 1.5); + + float radius = progress * blurRadius; + + if (radius <= 0.0) { + return content.eval(fragCoord); + } + + half4 accum = half4(0.0); + float weightSum = 0.0; + + const int SAMPLES = 5; + float offsetScale = radius / float(SAMPLES); + + for (int x = -SAMPLES; x <= SAMPLES; x++) { + for (int y = -SAMPLES; y <= SAMPLES; y++) { + float2 offset = float2(float(x), float(y)) * offsetScale; + + float distSq = dot(offset, offset); + float radiusSq = radius * radius; + + if (distSq <= radiusSq) { + float weight = exp(-3.0 * distSq / radiusSq); + accum += content.eval(fragCoord + offset) * weight; + weightSum += weight; + } + } + } + + return accum / weightSum; + } +""".trimIndent() + +/** + * Applies a progressive blur to the specified edge of the element. + * Only works on Android 13+ (API 33). + */ +fun Modifier.progressiveBlur( + blurRadius: Float, + height: Float, + direction: BlurDirection = BlurDirection.TOP +): Modifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.then( + Modifier.graphicsLayer { + if (blurRadius <= 0f) return@graphicsLayer + + val shader = RuntimeShader(PROGRESSIVE_BLUR_SKSL) + shader.setFloatUniform("blurRadius", blurRadius) + shader.setFloatUniform("height", height) + shader.setFloatUniform("contentHeight", size.height) + shader.setIntUniform("isTop", if (direction == BlurDirection.TOP) 1 else 0) + + renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "content") + .asComposeRenderEffect() + } + ) +} else { + this +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DeviceImageMapper.kt b/app/src/main/java/com/sameerasw/essentials/utils/DeviceImageMapper.kt new file mode 100644 index 000000000..72edce3b6 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/DeviceImageMapper.kt @@ -0,0 +1,59 @@ +package com.sameerasw.essentials.utils + +import com.sameerasw.essentials.R + +object DeviceImageMapper { + /** + * Maps a device model string to a corresponding drawable resource. + * + * @param model The device model (e.g., from Build.MODEL) + * @return The resource ID of the matching drawable vector illustration. + */ + fun getDeviceDrawable(model: String): Int { + val m = model.lowercase() + return when { + // Pixel 6 series + m.contains("pixel 6a") -> R.drawable.pixel_6a + m.contains("pixel 6 pro") -> R.drawable.pixel_6pro + m.contains("pixel 6") -> R.drawable.pixel_6 + + // Pixel 7 series + m.contains("pixel 7a") -> R.drawable.pixel_7a + m.contains("pixel 7 pro") -> R.drawable.pixel_7pro + m.contains("pixel 7") -> R.drawable.pixel_7 + + // Pixel 8 series + m.contains("pixel 8a") -> R.drawable.pixel_8a + m.contains("pixel 8 pro") -> R.drawable.pixel_8pro + + // Pixel 9 & 10 series + m.contains("pixel 9a") || m.contains("pixel 10a") -> R.drawable.pixel_9a_10a + m.contains("pixel 9 pro") || m.contains("pixel 9 pro xl") || + m.contains("pixel 10") || m.contains("pixel 10 pro") || m.contains("pixel 10 pro xl") -> + R.drawable.pixel_9pro_9proxl_10_10pro_10proxl + + m.contains("pixel 9") -> R.drawable.pixel_9 + + // Default fallback + else -> R.drawable.rounded_android_24 + } + } + + /** + * Maps the Android version to a specific logo. + */ + fun getAndroidLogo(deviceInfo: DeviceInfo): Int { + val osName = deviceInfo.osCodename.lowercase() + val sdk = deviceInfo.sdkInt + + return when { + osName.contains("android 17") || osName.contains("cinnamonbun") || sdk >= 37 -> R.drawable.android17 + osName.contains("android 16") || osName.contains("baklava") || sdk >= 36 -> R.drawable.android16 + osName.contains("android 15") || osName.contains("vanilla") || sdk >= 35 -> R.drawable.android15 + osName.contains("android 14") || osName.contains("upside") || sdk >= 34 -> R.drawable.android14 + osName.contains("android 13") || osName.contains("tiramisu") || sdk >= 33 -> R.drawable.android13 + osName.contains("android 12") || osName.contains("snow cone") || sdk >= 31 -> R.drawable.android12 + else -> R.drawable.rounded_android_24 + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt new file mode 100644 index 000000000..fcdef2629 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt @@ -0,0 +1,215 @@ +package com.sameerasw.essentials.utils + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import android.os.Environment +import android.os.StatFs +import android.provider.Settings +import org.json.JSONArray + +data class DeviceInfo( + val deviceName: String, + val brand: String = Build.BRAND, + val model: String = Build.MODEL, + val device: String = Build.DEVICE, + val hardware: String = Build.HARDWARE, + val product: String = Build.PRODUCT, + val androidVersion: String = Build.VERSION.RELEASE, + val sdkInt: Int = Build.VERSION.SDK_INT, + val manufacturer: String = Build.MANUFACTURER, + val board: String = Build.BOARD, + val display: String = Build.DISPLAY, + val fingerprint: String = Build.FINGERPRINT, + val totalStorage: Long, + val availableStorage: Long, + val totalRam: Long, + val availableRam: Long, + val securityPatch: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Build.VERSION.SECURITY_PATCH else "Unknown", + val osCodename: String = Build.VERSION.CODENAME, + val buildTag: String = "", + val supportedDevices: String = "" +) + +object DeviceUtils { + fun getDeviceInfo(context: Context): DeviceInfo { + val deviceName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) + ?: Settings.Secure.getString(context.contentResolver, "bluetooth_name") + ?: Build.MODEL + } else { + Settings.Secure.getString(context.contentResolver, "bluetooth_name") ?: Build.MODEL + } + + val stat = StatFs(Environment.getDataDirectory().path) + val blockSize = stat.blockSizeLong + val totalStorage = stat.blockCountLong * blockSize + val availableStorage = stat.availableBlocksLong * blockSize + + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + + val buildId = Build.DISPLAY + val buildInfo = findBuildInfo(context, buildId) ?: getBetaDetailsFromPrefix(buildId) + + val deviceSecurityPatch = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Build.VERSION.SECURITY_PATCH else "Unknown" + val deviceOsCodename = Build.VERSION.CODENAME + val matchedVersion = buildInfo?.optString("version") + + val androidVersion = matchedVersion?.let { + if (it.startsWith("Android ")) { + val v = it.removePrefix("Android ").substringBefore(" ") + if (v.firstOrNull()?.isDigit() == true) v else null + } else null + } ?: Build.VERSION.RELEASE + + return DeviceInfo( + deviceName = deviceName, + totalStorage = totalStorage, + availableStorage = availableStorage, + totalRam = memoryInfo.totalMem, + availableRam = memoryInfo.availMem, + androidVersion = androidVersion, + securityPatch = buildInfo?.optString("patch")?.takeIf { it.isNotBlank() } + ?: deviceSecurityPatch, + osCodename = matchedVersion?.takeIf { it.isNotBlank() } ?: deviceOsCodename, + buildTag = buildInfo?.optString("tag") ?: "", + supportedDevices = buildInfo?.optString("devices") ?: "" + ) + } + + private fun getBetaDetailsFromPrefix(buildId: String): org.json.JSONObject? { + val build = buildId.uppercase() + val details = org.json.JSONObject() + return when { + build.startsWith("UPB") || build.startsWith("U1B") -> { + details.put("version", "Android 15 Beta") + details.put("tag", "Beta Prefix") + details + } + + build.startsWith("AP11") || build.startsWith("AP21") || build.startsWith("AP31") -> { + details.put("version", "Android 16 Beta") + details.put("tag", "Beta Prefix") + details + } + + build.startsWith("BP") -> { + // Check if it's likely a QPR beta based on the request (BPxx) + details.put("version", "Android 16 QPR Beta") + details.put("tag", "Platform/QPR Beta") + details + } + + build.startsWith("CP") -> { + details.put("version", "Android 17 Beta") + details.put("tag", "Developer Beta") + details + } + + build.startsWith("ZP") -> { + details.put("version", "Android Canary") + details.put("tag", "Canary Build") + details + } + + else -> null + } + } + + private fun findBuildInfo(context: Context, buildId: String): org.json.JSONObject? { + return try { + val jsonString = + context.assets.open("android_builds.json").bufferedReader().use { it.readText() } + val jsonArray = JSONArray(jsonString) + for (i in 0 until jsonArray.length()) { + val obj = jsonArray.getJSONObject(i) + if (obj.getString("build_id").equals(buildId, ignoreCase = true)) { + return obj + } + } + null + } catch (e: Exception) { + null + } + } + + fun formatSize(size: Long): String { + if (size <= 0) return "0 B" + val units = arrayOf("B", "KB", "MB", "GB", "TB") + val digitGroups = (Math.log10(size.toDouble()) / Math.log10(1024.0)).toInt() + return java.lang.String.format( + "%.1f %s", + size / Math.pow(1024.0, digitGroups.toDouble()), + units[digitGroups] + ) + } + + fun getOSName(sdkInt: Int, defaultCodename: String): String { + // Try to determine by version string first (most accurate for betas) + val name = defaultCodename.lowercase() + if (name.contains("17") || name.contains("cinnamon")) return "CinnamonBun" + if (name.contains("16") || name.contains("baklava")) return "Baklava" + + val dessert = when (sdkInt) { + 37 -> "CinnamonBun" + 36 -> "Baklava" + 35 -> "Vanilla Ice Cream" + 34 -> "Upside Down Cake" + 33 -> "Tiramisu" + 32 -> "Snow Cone v2" + 31 -> "Snow Cone" + 30 -> "R" + 29 -> "Q" + 28 -> "Pie" + 27 -> "Oreo" + 26 -> "Oreo" + else -> null + } + + if (dessert != null) return dessert + + // If SDK mapping fails, try to use the provided codename + if (defaultCodename.contains("Beta") || defaultCodename.contains("Canary") || defaultCodename.contains( + "QPR" + ) + ) { + return defaultCodename + } + + return defaultCodename.takeIf { it != "REL" } ?: "Android" + } + + fun formatHardwareSize(sizeBytes: Long): String { + if (sizeBytes <= 0) return "Unknown" + val rawGb = sizeBytes / (1024.0 * 1024.0 * 1024.0) + + // Known standard hardware sizes in GB + val standardSizes = + listOf(1, 2, 3, 4, 6, 8, 10, 12, 16, 18, 24, 32, 64, 128, 256, 512, 1024, 2048) + + // Find the closest standard size that is greater than or equal to our raw GB (allowing a 10% delta for OS reservations) + val roundedGb = standardSizes.firstOrNull { it >= rawGb * 0.9 } ?: Math.ceil(rawGb).toInt() + + return if (roundedGb >= 1024 && roundedGb % 1024 == 0) { + "${roundedGb / 1024} TB" + } else { + "$roundedGb GB" + } + } + + fun formatSecurityPatch(patchString: String): String { + if (patchString == "Unknown" || patchString.isBlank()) return patchString + return try { + val parser = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) + val formatter = + java.text.SimpleDateFormat("d MMMM, yyyy", java.util.Locale.getDefault()) + val date = parser.parse(patchString) + if (date != null) formatter.format(date) else patchString + } catch (e: Exception) { + patchString + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/GSMArenaService.kt b/app/src/main/java/com/sameerasw/essentials/utils/GSMArenaService.kt new file mode 100644 index 000000000..bc32610f9 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/GSMArenaService.kt @@ -0,0 +1,106 @@ +package com.sameerasw.essentials.utils + +import com.sameerasw.essentials.data.model.DeviceSpecCategory +import com.sameerasw.essentials.data.model.DeviceSpecItem +import com.sameerasw.essentials.data.model.DeviceSpecs +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +object GSMArenaService { + private const val BASE_URL = "https://www.gsmarena.com" + + fun fetchSpecs(brand: String, model: String): DeviceSpecs? { + return try { + val query = "$brand $model".replace(" ", "+") + val searchUrl = "$BASE_URL/results.php3?sQuickSearch=yes&sName=$query" + + val searchDoc: Document = Jsoup.connect(searchUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .timeout(30000) + .get() + + val firstDeviceElement = searchDoc.select(".makers li").firstOrNull() ?: return null + val firstDevicePath = firstDeviceElement.select("a").attr("href") + val searchThumbnail = firstDeviceElement.select("img").attr("src") + + val deviceUrl = + if (firstDevicePath.startsWith("/")) "$BASE_URL$firstDevicePath" else "$BASE_URL/$firstDevicePath" + + val deviceDoc: Document = Jsoup.connect(deviceUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .timeout(30000) + .get() + + val name = deviceDoc.select(".specs-phone-name-title").text() + val tables = deviceDoc.select("table") + val detailSpecs = mutableListOf() + + // Scrape images + val imageUrls = mutableListOf() + + // Fix protocol helper + fun String.fixUrl(): String { + return when { + startsWith("//") -> "https:$this" + startsWith("/") -> "$BASE_URL$this" + else -> this + } + } + + // Fallback to search thumbnail + if (searchThumbnail.isNotBlank()) { + imageUrls.add(searchThumbnail.fixUrl()) + } + + // Get main image on device page (often higher quality) + deviceDoc.select(".specs-photo-main a img").firstOrNull()?.attr("src")?.let { + val url = it.fixUrl() + if (!imageUrls.contains(url)) imageUrls.add(0, url) + } + + // Get more images from gallery if available + val picturesLink = + deviceDoc.select(".specs-links a:contains(Pictures)").firstOrNull()?.attr("href") + if (picturesLink != null) { + val picturesUrl = picturesLink.fixUrl() + val picturesDoc = Jsoup.connect(picturesUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .timeout(30000) + .get() + + picturesDoc.select("#pictures-list img").forEach { img -> + val src = img.attr("src").ifBlank { img.attr("data-src") } + if (src.isNotBlank()) { + val url = src.fixUrl() + if (!imageUrls.contains(url)) { + imageUrls.add(url) + } + } + } + } + + tables.forEach { table -> + val categoryName = table.select("th").firstOrNull()?.text() ?: "" + val rows = table.select("tr") + val specs = mutableListOf() + + rows.forEach { row -> + val label = row.select("td.ttl").text() + val value = row.select("td.nfo").text() + if (label.isNotBlank() && value.isNotBlank()) { + specs.add(DeviceSpecItem(label, value)) + } + } + + if (categoryName.isNotBlank() && specs.isNotEmpty()) { + detailSpecs.add(DeviceSpecCategory(categoryName, specs)) + } + } + + DeviceSpecs(name, detailSpecs, imageUrls) + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt index df114341c..8e3b35268 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt @@ -35,6 +35,16 @@ object ShellUtils { } } + fun runCommandWithOutput(context: Context, command: String): String? { + return try { + val process = newProcess(context, arrayOf("sh", "-c", command)) + process?.inputStream?.bufferedReader()?.use { it.readText() }?.trim() + } catch (e: Exception) { + null + } + } + + fun newProcess(context: Context, command: Array): Process? { return if (isRootEnabled(context)) { RootUtils.newProcess(command) diff --git a/app/src/main/res/drawable/android12.png b/app/src/main/res/drawable/android12.png new file mode 100644 index 000000000..ad1b6cfb7 Binary files /dev/null and b/app/src/main/res/drawable/android12.png differ diff --git a/app/src/main/res/drawable/android13.png b/app/src/main/res/drawable/android13.png new file mode 100644 index 000000000..9e8dbefbb Binary files /dev/null and b/app/src/main/res/drawable/android13.png differ diff --git a/app/src/main/res/drawable/android14.png b/app/src/main/res/drawable/android14.png new file mode 100644 index 000000000..5a21d7a61 Binary files /dev/null and b/app/src/main/res/drawable/android14.png differ diff --git a/app/src/main/res/drawable/android15.png b/app/src/main/res/drawable/android15.png new file mode 100644 index 000000000..18f4ec0ed Binary files /dev/null and b/app/src/main/res/drawable/android15.png differ diff --git a/app/src/main/res/drawable/android16.png b/app/src/main/res/drawable/android16.png new file mode 100644 index 000000000..5bc915231 Binary files /dev/null and b/app/src/main/res/drawable/android16.png differ diff --git a/app/src/main/res/drawable/android17.png b/app/src/main/res/drawable/android17.png new file mode 100644 index 000000000..d5d777188 Binary files /dev/null and b/app/src/main/res/drawable/android17.png differ diff --git a/app/src/main/res/drawable/pixel_6.xml b/app/src/main/res/drawable/pixel_6.xml new file mode 100644 index 000000000..b60f86932 --- /dev/null +++ b/app/src/main/res/drawable/pixel_6.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_6a.xml b/app/src/main/res/drawable/pixel_6a.xml new file mode 100644 index 000000000..c950d9662 --- /dev/null +++ b/app/src/main/res/drawable/pixel_6a.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_6pro.xml b/app/src/main/res/drawable/pixel_6pro.xml new file mode 100644 index 000000000..3f6f1f912 --- /dev/null +++ b/app/src/main/res/drawable/pixel_6pro.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_7.xml b/app/src/main/res/drawable/pixel_7.xml new file mode 100644 index 000000000..a89a87854 --- /dev/null +++ b/app/src/main/res/drawable/pixel_7.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_7a.xml b/app/src/main/res/drawable/pixel_7a.xml new file mode 100644 index 000000000..0c926ce8e --- /dev/null +++ b/app/src/main/res/drawable/pixel_7a.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_7pro.xml b/app/src/main/res/drawable/pixel_7pro.xml new file mode 100644 index 000000000..807030cce --- /dev/null +++ b/app/src/main/res/drawable/pixel_7pro.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_8a.xml b/app/src/main/res/drawable/pixel_8a.xml new file mode 100644 index 000000000..b632c98df --- /dev/null +++ b/app/src/main/res/drawable/pixel_8a.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_8pro.xml b/app/src/main/res/drawable/pixel_8pro.xml new file mode 100644 index 000000000..53cb7823d --- /dev/null +++ b/app/src/main/res/drawable/pixel_8pro.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_9.xml b/app/src/main/res/drawable/pixel_9.xml new file mode 100644 index 000000000..f57952b71 --- /dev/null +++ b/app/src/main/res/drawable/pixel_9.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_9a_10a.xml b/app/src/main/res/drawable/pixel_9a_10a.xml new file mode 100644 index 000000000..4b3ed464d --- /dev/null +++ b/app/src/main/res/drawable/pixel_9a_10a.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/pixel_9pro_9proxl_10_10pro_10proxl.xml b/app/src/main/res/drawable/pixel_9pro_9proxl_10_10pro_10proxl.xml new file mode 100644 index 000000000..13ce16f01 --- /dev/null +++ b/app/src/main/res/drawable/pixel_9pro_9proxl_10_10pro_10proxl.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/rounded_android_24.xml b/app/src/main/res/drawable/rounded_android_24.xml new file mode 100644 index 000000000..ea55a9981 --- /dev/null +++ b/app/src/main/res/drawable/rounded_android_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_memory_alt_24.xml b/app/src/main/res/drawable/rounded_memory_alt_24.xml new file mode 100644 index 000000000..c490af0b2 --- /dev/null +++ b/app/src/main/res/drawable/rounded_memory_alt_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-ach/strings.xml b/app/src/main/res/values-ach/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-ach/strings.xml +++ b/app/src/main/res/values-ach/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 11c2dc5ae..c14076023 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index beb28e9eb..080cbe1dd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -199,7 +199,7 @@ Aus USB-Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper An Aus diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20a740c8b..d5dbbf691 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 442427241..c880c837e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -199,7 +199,7 @@ Désactivé Débogage USB Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper Activer Désactiver diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0b0d37362..aad164a8d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -200,7 +200,7 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 61a53e2d8..4b2300ad9 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -3,6 +3,7 @@ diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 7b9f6b7df..ffdee113f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d38855366..47c318658 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper Pornit Oprit diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d83dd0395..55ef60fd9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -199,7 +199,7 @@ Off Отладка по USB Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3018f8ba7..8fbd23932 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 2442bb315..b7da15605 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f4ff39184..586593b4c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -199,7 +199,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 652f546e8..b0d22040e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,7 +205,7 @@ Off USB Debugging Color Picker - Are you sure you\'re on Androdi 17? (╯°_°)╯ + Are you sure you\'re on Android 17? (╯°_°)╯ Eye Dropper On Off @@ -226,6 +226,8 @@ Limit to 80% Adaptive Not optimized + Permission missing + Screen locked security @@ -598,6 +600,9 @@ Back + Back + Settings + Report a Bug Done Preview Help Guide @@ -1166,4 +1171,8 @@ Force turn off the AOD when no notifications. Requires accessibility permission. Auto accessibility Automatically grants the accessibility permission on app launch if missing using WRITE_SECURE_SETTINGS. + Help and Guides + Your Android + Storage + Memory \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 14855b486..dabe4d6fc 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,7 @@