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 @@