From 3ed95414591c4e9acd83ed3962d9e742c7733ea6 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Jan 2026 22:44:25 +0530 Subject: [PATCH 1/3] improved haptics --- .../com/sameerasw/essentials/MainActivity.kt | 3 ++ .../ui/activities/AutomationEditorActivity.kt | 44 ++++++++++++++++++- .../sheets/DimWallpaperSettingsSheet.kt | 3 +- .../components/sheets/NewAutomationSheet.kt | 8 +++- .../essentials/ui/composables/DIYScreen.kt | 8 +++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index bee2426f..40dc2d28 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -50,6 +50,7 @@ import com.sameerasw.essentials.ui.composables.configs.FreezeSettingsUI import com.sameerasw.essentials.ui.composables.FreezeGridUI import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @@ -152,6 +153,7 @@ class MainActivity : FragmentActivity() { setContent { EssentialsTheme { val context = LocalContext.current + val view = LocalView.current val versionName = try { context.packageManager.getPackageInfo(context.packageName, 0).versionName } catch (_: Exception) { @@ -249,6 +251,7 @@ class MainActivity : FragmentActivity() { currentPage = pagerState.currentPage, tabs = tabs, onTabSelected = { index -> + HapticUtil.performUIHaptic(view) scope.launch { pagerState.animateScrollToPage(index) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index 2be44c17..d36969da 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -35,6 +35,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.geometry.Offset import androidx.compose.material3.RadioButton import androidx.compose.material3.IconButton import androidx.compose.material3.DropdownMenu @@ -62,6 +66,8 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import com.sameerasw.essentials.utils.HapticUtil class AutomationEditorActivity : ComponentActivity() { @@ -114,6 +120,19 @@ class AutomationEditorActivity : ComponentActivity() { val view = LocalView.current var carouselState = rememberCarouselState { 2 } // 0: Trigger/State, 1: Actions + // Haptic on carousel page change + LaunchedEffect(carouselState) { + var isFirst = true + snapshotFlow { carouselState.currentItem } + .collect { + if (isFirst) { + isFirst = false + } else { + HapticUtil.performHeavyHaptic(view) + } + } + } + // State for selections // Initialize with existing data or defaults var selectedTrigger by remember { mutableStateOf(existingAutomation?.trigger) } @@ -188,6 +207,28 @@ class AutomationEditorActivity : ComponentActivity() { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp + + // Haptic Connection for Swipe Texture + val nestedScrollConnection = remember { + object : NestedScrollConnection { + var accumulatedScroll = 0f + val threshold = 40f + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Only handle drag (user interaction) + if (source == NestedScrollSource.Drag) { + accumulatedScroll += available.x + + if (kotlin.math.abs(accumulatedScroll) >= threshold) { + HapticUtil.performSliderHaptic(view) // Subtle tick + accumulatedScroll = 0f + } + } + return Offset.Zero + } + } + } + Column(modifier = Modifier .fillMaxSize() .padding(innerPadding) @@ -198,7 +239,8 @@ class AutomationEditorActivity : ComponentActivity() { itemSpacing = 4.dp, modifier = Modifier .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .nestedScroll(nestedScrollConnection), contentPadding = PaddingValues(horizontal = 18.dp) ) { index -> Box( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt index 6ce364fa..68eb68f0 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt @@ -117,7 +117,7 @@ fun DimWallpaperSettingsSheet( value = dimAmount, onValueChange = { dimAmount = it - HapticUtil.performUIHaptic(view) + HapticUtil.performSliderHaptic(view) }, valueRange = 0f..1f ) @@ -164,7 +164,6 @@ fun DimWallpaperSettingsSheet( Text(stringResource(R.string.action_save)) } } - Spacer(modifier = Modifier.size(16.dp)) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt index 656e5930..215961b3 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt @@ -27,9 +27,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalView import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.diy.Automation import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.HapticUtil @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -102,12 +104,16 @@ private fun AutomationTypeOption( modifier: Modifier = Modifier, onClick: () -> Unit ) { + val view = LocalView.current Surface( color = MaterialTheme.colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(4.dp), modifier = modifier .fillMaxWidth() - .clickable(onClick = onClick) + .clickable { + HapticUtil.performUIHaptic(view) + onClick() + } ) { Row( modifier = Modifier.padding(12.dp), 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 acb5d3a2..46a6aea5 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 @@ -39,6 +39,8 @@ import androidx.compose.ui.res.stringResource import com.sameerasw.essentials.R import com.sameerasw.essentials.ui.activities.AutomationEditorActivity import com.sameerasw.essentials.ui.components.sheets.NewAutomationSheet +import androidx.compose.ui.platform.LocalView +import com.sameerasw.essentials.utils.HapticUtil @Composable fun DIYScreen( @@ -96,8 +98,12 @@ fun DIYScreen( } // FAB + val view = LocalView.current FloatingActionButton( - onClick = { showNewAutomationSheet = true }, + onClick = { + HapticUtil.performUIHaptic(view) + showNewAutomationSheet = true + }, modifier = Modifier .align(Alignment.BottomEnd) .padding(bottom = 32.dp, end = 32.dp), From bcf551d7176f3b33357201c4e1e1d5a683151d1e Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Jan 2026 22:50:54 +0530 Subject: [PATCH 2/3] improved state automation none action display --- .../ui/activities/AutomationEditorActivity.kt | 2 +- .../ui/components/diy/AutomationItem.kt | 17 +++++------------ .../configs/ButtonRemapSettingsUI.kt | 2 +- app/src/main/res/values/strings.xml | 1 - 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index d36969da..783762b3 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -355,7 +355,7 @@ class AutomationEditorActivity : ComponentActivity() { // None option EditorActionItem( - title = stringResource(R.string.action_none), + title = stringResource(R.string.haptic_none), iconRes = R.drawable.rounded_do_not_disturb_on_24, isSelected = currentSelection == null, onClick = { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt index c2aebb8f..47949959 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt @@ -258,15 +258,8 @@ fun AutomationItem( } } else { // State Actions (In/Out) - // In Action (Top) - automation.entryAction?.let { action -> - ActionItem(action = action) - } - - // Out Action (Bottom) - automation.exitAction?.let { action -> - ActionItem(action = action) - } + ActionItem(action = automation.entryAction) + ActionItem(action = automation.exitAction) } } } @@ -276,7 +269,7 @@ fun AutomationItem( @Composable fun ActionItem( - action: Action, + action: Action?, modifier: Modifier = Modifier ) { Surface( @@ -289,14 +282,14 @@ fun ActionItem( verticalAlignment = Alignment.CenterVertically ) { Icon( - painter = painterResource(id = action.icon), + painter = painterResource(id = action?.icon ?: R.drawable.rounded_do_not_disturb_on_24), contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = stringResource(id = action.title), + text = stringResource(id = action?.title ?: R.string.haptic_none), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt index a12d439a..ec5a104e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt @@ -375,7 +375,7 @@ fun ButtonRemapSettingsUI( } RemapActionItem( - title = stringResource(R.string.action_none), + title = stringResource(R.string.haptic_none), isSelected = currentAction == "None", onClick = { onActionSelected("None") }, iconRes = R.drawable.rounded_do_not_disturb_on_24, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c805a70d..c119198d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,7 +43,6 @@ Screen On Volume Up Volume Down - None Toggle flashlight Media play/pause Media next From a9f572e07574d925ba7a6a31b66db483a2426304 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Jan 2026 23:16:11 +0530 Subject: [PATCH 3/3] Icon handling optimizations --- .../com/sameerasw/essentials/MainActivity.kt | 3 +- .../essentials/ui/composables/FreezeGridUI.kt | 8 ++- .../com/sameerasw/essentials/utils/AppUtil.kt | 61 ++++++++++++++----- .../essentials/viewmodels/MainViewModel.kt | 41 ++++++++----- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index 40dc2d28..fec3b008 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -262,7 +262,8 @@ class MainActivity : FragmentActivity() { HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.Top + verticalAlignment = Alignment.Top, + beyondViewportPageCount = 1 ) { page -> when (tabs[page]) { DIYTabs.ESSENTIALS -> { 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 f7f01509..b221c71d 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 @@ -83,8 +83,12 @@ fun FreezeGridUI( LaunchedEffect(pickedApps) { withContext(Dispatchers.IO) { - pickedApps.forEach { app -> - frozenStates[app.packageName] = FreezeManager.isAppFrozen(context, app.packageName) + val states = pickedApps.associate { app -> + app.packageName to FreezeManager.isAppFrozen(context, app.packageName) + } + // Batch update on Main thread + withContext(Dispatchers.Main) { + frozenStates.putAll(states) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt index cf85a986..9283d423 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt @@ -4,7 +4,9 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.graphics.Canvas import android.graphics.Color +import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.AdaptiveIconDrawable import android.util.Log import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.toBitmap @@ -20,6 +22,12 @@ object AppUtil { // Cache for extracted brand colors private val colorCache = mutableMapOf() + + // Cache for app icons to prevent repeated system calls + private val iconCache = mutableMapOf() + + // Target size for app icons to balance quality and performance + private const val ICON_SIZE = 64 /** * Get all installed apps (not just launcher apps) @@ -46,7 +54,7 @@ object AppUtil { packageName = appInfo.packageName, appName = pm.getApplicationLabel(appInfo).toString(), isEnabled = false, - icon = pm.getApplicationIcon(appInfo).toBitmap().asImageBitmap(), + icon = getLowQualityIcon(context, appInfo.packageName).asImageBitmap(), isSystemApp = isSystemApp, lastUpdated = System.currentTimeMillis() ) @@ -86,7 +94,7 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List): packageName = appInfo.packageName, appName = pm.getApplicationLabel(appInfo).toString(), isEnabled = false, - icon = pm.getApplicationIcon(appInfo).toBitmap().asImageBitmap(), + icon = getLowQualityIcon(context, packageName).asImageBitmap(), isSystemApp = isSystemApp, lastUpdated = System.currentTimeMillis() ) @@ -132,18 +140,7 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List): try { val pm = context.packageManager // Extract bitmap from drawable, handling AdaptiveIcons - val bitmap = when (val drawable = pm.getApplicationIcon(packageName)) { - is BitmapDrawable -> drawable.bitmap - else -> { - val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 128 - val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 128 - val bmp = createBitmap(width, height) - val canvas = Canvas(bmp) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - bmp - } - } + val bitmap = getLowQualityIcon(context, packageName) // Generate palette asynchronously Palette.from(bitmap).generate { palette -> @@ -160,4 +157,40 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List): callback(Color.GRAY) } } + + /** + * Helper to load and scale an app icon to a lower resolution for better performance. + */ + private fun getLowQualityIcon(context: Context, packageName: String): Bitmap { + // Check cache first + iconCache[packageName]?.let { return it } + + val drawable = try { + context.packageManager.getApplicationIcon(packageName) + } catch (e: Exception) { + context.packageManager.defaultActivityIcon + } + + val bitmap = when (drawable) { + is BitmapDrawable -> { + val b = drawable.bitmap + if (b.width > ICON_SIZE || b.height > ICON_SIZE) { + Bitmap.createScaledBitmap(b, ICON_SIZE, ICON_SIZE, true) + } else { + b + } + } + else -> { + val bmp = createBitmap(ICON_SIZE, ICON_SIZE) + val canvas = Canvas(bmp) + drawable.setBounds(0, 0, ICON_SIZE, ICON_SIZE) + drawable.draw(canvas) + bmp + } + } + + // Cache the result + iconCache[packageName] = bitmap + return bitmap + } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index e96ebbb4..84cc85d7 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -38,6 +38,7 @@ import com.sameerasw.essentials.utils.UpdateNotificationHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class MainViewModel : ViewModel() { val isAccessibilityEnabled = mutableStateOf(false) @@ -842,29 +843,39 @@ class MainViewModel : ViewModel() { viewModelScope.launch { if (!silent) isFreezePickedAppsLoading.value = true try { - // Only load apps that are actually marked as secondary selected (picked) - val selections = loadFreezeSelectedApps(context).filter { it.isEnabled } - if (selections.isEmpty()) { - freezePickedApps.value = emptyList() - return@launch + // Background processing for heavy list operations + val result = withContext(Dispatchers.Default) { + // Only load apps that are actually marked as secondary selected (picked) + val selections = loadFreezeSelectedApps(context).filter { it.isEnabled } + if (selections.isEmpty()) return@withContext emptyList() + + // Efficiently load only the apps that are actually marked as secondary selected (picked) + val pickedPkgNames = selections.map { it.packageName } + val relevantApps = AppUtil.getAppsByPackageNames(context, pickedPkgNames) + + val merged = AppUtil.mergeWithSavedApps(relevantApps, selections) + val currentExcluded = freezeAutoExcludedApps.value + + // Cleanup: remove package names that are no longer picked (still on main because it updates state) + val filteredExcluded = currentExcluded.filter { pickedPkgNames.contains(it) }.toSet() + + // Prepare final list in background + merged.map { it.copy(isEnabled = !filteredExcluded.contains(it.packageName)) } + .sortedBy { it.appName.lowercase() } } + + // Final state update on Main + freezePickedApps.value = result - // Efficiently load only the apps that are actually marked as secondary selected (picked) - val pickedPkgNames = selections.map { it.packageName } - val relevantApps = AppUtil.getAppsByPackageNames(context, pickedPkgNames) - - val merged = AppUtil.mergeWithSavedApps(relevantApps, selections) + // Exclude check (this part still needs to update state if cleaned up) val currentExcluded = freezeAutoExcludedApps.value - - // Cleanup: remove package names that are no longer picked + val selections = loadFreezeSelectedApps(context).filter { it.isEnabled } + val pickedPkgNames = selections.map { it.packageName } val filteredExcluded = currentExcluded.filter { pickedPkgNames.contains(it) }.toSet() if (filteredExcluded.size != currentExcluded.size) { freezeAutoExcludedApps.value = filteredExcluded settingsRepository.saveFreezeAutoExcludedApps(filteredExcluded) } - - freezePickedApps.value = merged.map { it.copy(isEnabled = !filteredExcluded.contains(it.packageName)) } - .sortedBy { it.appName.lowercase() } } finally { if (!silent) isFreezePickedAppsLoading.value = false }