Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/src/main/java/com/sameerasw/essentials/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -249,6 +251,7 @@ class MainActivity : FragmentActivity() {
currentPage = pagerState.currentPage,
tabs = tabs,
onTabSelected = { index ->
HapticUtil.performUIHaptic(view)
scope.launch {
pagerState.animateScrollToPage(index)
}
Expand All @@ -259,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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Trigger?>(existingAutomation?.trigger) }
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -313,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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand All @@ -276,7 +269,7 @@ fun AutomationItem(

@Composable
fun ActionItem(
action: Action,
action: Action?,
modifier: Modifier = Modifier
) {
Surface(
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fun DimWallpaperSettingsSheet(
value = dimAmount,
onValueChange = {
dimAmount = it
HapticUtil.performUIHaptic(view)
HapticUtil.performSliderHaptic(view)
},
valueRange = 0f..1f
)
Expand Down Expand Up @@ -164,7 +164,6 @@ fun DimWallpaperSettingsSheet(
Text(stringResource(R.string.action_save))
}
}
Spacer(modifier = Modifier.size(16.dp))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 47 additions & 14 deletions app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +22,12 @@ object AppUtil {

// Cache for extracted brand colors
private val colorCache = mutableMapOf<String, Int>()

// Cache for app icons to prevent repeated system calls
private val iconCache = mutableMapOf<String, Bitmap>()

// Target size for app icons to balance quality and performance
private const val ICON_SIZE = 64

/**
* Get all installed apps (not just launcher apps)
Expand All @@ -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()
)
Expand Down Expand Up @@ -86,7 +94,7 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List<String>):
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()
)
Expand Down Expand Up @@ -132,18 +140,7 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List<String>):
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 ->
Expand All @@ -160,4 +157,40 @@ suspend fun getAppsByPackageNames(context: Context, packageNames: List<String>):
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
}
}
Loading