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
399 changes: 258 additions & 141 deletions app/src/main/java/com/brittytino/patchwork/FeatureSettingsActivity.kt

Large diffs are not rendered by default.

89 changes: 14 additions & 75 deletions app/src/main/java/com/brittytino/patchwork/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Install and configure the splash screen
val splashScreen = installSplashScreen()

// Force splash screen to dismiss after 2 seconds no matter what
// to prevent getting stuck if Compose has an issue on this device/OS
window.decorView.postDelayed({ isAppReady = true }, 2000)
splashScreen.setKeepOnScreenCondition { !isAppReady }

WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
Expand All @@ -73,84 +78,24 @@ class MainActivity : FragmentActivity() {
window.isNavigationBarContrastEnforced = false
}

// Keep splash screen visible while app is loading
splashScreen.setKeepOnScreenCondition { !isAppReady }

// Customize the exit animation - scale up and fade out
// Safe implementation for OEM devices that may not provide iconView
splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
try {
val splashScreenView = splashScreenViewProvider.view
val splashIcon = try { splashScreenViewProvider.iconView } catch (e: Exception) { null }

// Animate the splash screen view fade out
val fadeOut = ObjectAnimator.ofFloat(splashScreenView, "alpha", 1f, 0f).apply {
interpolator = AnticipateInterpolator()
duration = 750
}
fadeOut.doOnEnd {
splashScreenViewProvider.remove()
// Re-apply edge to edge AFTER the splash screen view is removed
// to ensure it's not overridden by splash screen cleanup
enableEdgeToEdge()
}

// Safely animate the icon if it exists
// Known issue: Some OEM devices (Samsung One UI 8, Xiaomi on Android 16)
// may not provide iconView, causing NullPointerException
try {
@Suppress("SENSELESS_COMPARISON")
if (splashIcon != null) {
// Scale down animation
val scaleUp = ObjectAnimator.ofFloat(splashIcon, "scaleX", 1f, 0.5f).apply {
interpolator = AnticipateInterpolator()
duration = 750
}

val scaleUpY = ObjectAnimator.ofFloat(splashIcon, "scaleY", 1f, 0.5f).apply {
interpolator = AnticipateInterpolator()
duration = 750
}

// rotate
val rotate360 = ObjectAnimator.ofFloat(splashIcon, "rotation", 0f, -90f).apply {
interpolator = AnticipateInterpolator()
duration = 750
}

scaleUp.start()
scaleUpY.start()
rotate360.start()
} else {
Log.w("SplashScreen", "iconView is null - OEM device detected")
}
} catch (e: NullPointerException) {
// Handle the edge case where iconView becomes null between check and animation
Log.w("SplashScreen", "NullPointerException on iconView animation - likely OEM device", e)
}

fadeOut.start()
} catch (e: Exception) {
// Fallback for any unexpected exceptions during animation
Log.e("SplashScreen", "Exception during splash screen animation", e)
try {
splashScreenViewProvider.remove()
} catch (e2: Exception) {
Log.e("SplashScreen", "Exception during splash screen removal", e2)
}
}
}

Log.d("MainActivity", "onCreate with action: ${intent?.action}")
handleLocationIntent(intent)

// Initialize HapticUtil with saved preferences
HapticUtil.initialize(this)
// initialize permission registry
initPermissionRegistry()
// Initialize viewModel state early for correct initial composition

// viewModel.check is also called in LaunchedEffect inside setContent.
viewModel.check(this)

setContent {
// Confirm composition started and mark app as ready
LaunchedEffect(Unit) {
isAppReady = true
Log.d("MainActivity", "Composition started")
}

val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled
PatchworkTheme(pitchBlackTheme = isPitchBlackThemeEnabled) {
val context = LocalContext.current
Expand Down Expand Up @@ -286,12 +231,6 @@ class MainActivity : FragmentActivity() {
}
}
}


// Mark app as ready after composing (happens very quickly)
LaunchedEffect(Unit) {
isAppReady = true
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,70 @@ object FeatureRegistry {
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "App Behavior Controller",
title = R.string.feat_app_behavior_title,
iconRes = R.drawable.rounded_settings_accessibility_24,
category = R.string.cat_tools,
description = R.string.feat_app_behavior_desc,
permissionKeys = listOf("ACCESSIBILITY"),
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "Smart App Cooldown",
title = R.string.feat_app_cooldown_title,
iconRes = R.drawable.rounded_timer_24,
category = R.string.cat_tools,
description = R.string.feat_app_cooldown_desc,
permissionKeys = listOf("ACCESSIBILITY", "DRAW_OVERLAYS"),
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "Idle App Auto-Action",
title = R.string.feat_idle_app_title,
iconRes = R.drawable.rounded_av_timer_24,
category = R.string.cat_tools,
description = R.string.feat_idle_app_desc,
permissionKeys = listOf("USAGE_STATS"),
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "Action History Timeline",
title = R.string.feat_action_history_title,
iconRes = R.drawable.rounded_fiber_smart_record_24,
category = R.string.cat_tools,
description = R.string.feat_action_history_desc,
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "System State Snapshots",
title = R.string.feat_system_snapshots_title,
iconRes = R.drawable.rounded_save_24,
category = R.string.cat_tools,
description = R.string.feat_system_snapshots_desc,
permissionKeys = listOf("WRITE_SECURE_SETTINGS"),
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "Watermarks",
title = R.string.feat_watermark_title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,11 @@ fun initPermissionRegistry() {

// Modify system settings permission
PermissionRegistry.register("WRITE_SETTINGS", R.string.feat_qs_tiles_title)

// New Features
PermissionRegistry.register("USAGE_STATS", R.string.feat_idle_app_title)
PermissionRegistry.register("ACCESSIBILITY", R.string.feat_app_behavior_title)
PermissionRegistry.register("ACCESSIBILITY", R.string.feat_app_cooldown_title)
PermissionRegistry.register("DRAW_OVERLAYS", R.string.feat_app_cooldown_title)
PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_system_snapshots_title)
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ fun AboutSection(
modifier = Modifier.padding(horizontal = 4.dp)
) {
Icon(
painter = painterResource(id = R.drawable.brand_github),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down Expand Up @@ -161,7 +161,7 @@ fun AboutSection(
modifier = Modifier.padding(horizontal = 4.dp)
) {
Icon(
painter = painterResource(id = R.drawable.brand_telegram),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ fun BugReportBottomSheet(
},
modifier = Modifier.fillMaxWidth()
) {
Icon(painter = painterResource(R.drawable.brand_github), contentDescription = null, modifier = Modifier.size(18.dp))
Icon(painter = painterResource(R.drawable.rounded_globe_24), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.action_report_github))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ fun InstructionsBottomSheet(
modifier = Modifier.padding(horizontal = 4.dp)
) {
Icon(
painter = painterResource(id = R.drawable.brand_github),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down Expand Up @@ -222,7 +222,7 @@ fun InstructionsBottomSheet(
modifier = Modifier.padding(horizontal = 4.dp)
) {
Icon(
painter = painterResource(id = R.drawable.brand_telegram),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down Expand Up @@ -336,7 +336,7 @@ fun ExpandableGuideSection(section: InstructionSection) {
shape = RoundedCornerShape(12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.brand_github),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ fun UpdateBottomSheet(
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.brand_github),
painter = painterResource(id = R.drawable.rounded_globe_24),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -967,16 +967,7 @@ private fun LogoCarouselPicker(
modifier: Modifier = Modifier
) {
val logos = listOf(
R.drawable.apple,
R.drawable.cmf,
R.drawable.google,
R.drawable.moto,
R.drawable.nothing,
R.drawable.oppo,
R.drawable.samsung,
R.drawable.sony,
R.drawable.vivo,
R.drawable.xiaomi
R.mipmap.ic_launcher
)

val carouselState = androidx.compose.material3.carousel.rememberCarouselState { logos.size }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,11 @@ fun KeyboardInputView(
content = {
val functions = remember(isClipboardEnabled) {
val list = mutableListOf(
R.drawable.ic_emoji to "Emoji",
R.drawable.ic_undo to "Undo"
R.drawable.rounded_heart_smile_24 to "Emoji",
R.drawable.rounded_arrow_back_24 to "Undo"
)
if (isClipboardEnabled) {
list.add(1, R.drawable.ic_clipboard to "Clipboard")
list.add(1, R.drawable.rounded_content_paste_24 to "Clipboard")
}
list
}
Expand Down Expand Up @@ -644,7 +644,7 @@ fun KeyboardInputView(
.fillMaxHeight()
) {
Icon(
painter = painterResource(id = R.drawable.key_shift),
painter = painterResource(id = R.drawable.rounded_keyboard_arrow_up_24),
contentDescription = "Shift",
modifier = Modifier.size(24.dp),
tint = if (shiftState != ShiftState.OFF) MaterialTheme.colorScheme.onPrimary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ object PermissionUtils {
}
}

fun hasUsageStatsPermission(context: Context): Boolean {
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
appOps.unsafeCheckOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
} else {
appOps.checkOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName)
}
return mode == android.app.AppOpsManager.MODE_ALLOWED
}

fun isDeviceAdminActive(context: Context): Boolean {
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminComponent = ComponentName(context, SecurityDeviceAdminReceiver::class.java)
Expand Down
26 changes: 22 additions & 4 deletions app/src/main/java/com/brittytino/patchwork/utils/RootUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ object RootUtils {
fun isRootAvailable(): Boolean {
return try {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "which su"))
val exitCode = process.waitFor()
exitCode == 0
// Add a short timeout to prevent hanging the app on problematic devices
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (!process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {
process.destroyForcibly()
return false
}
} else {
// Fallback for older versions (unlikely to be used here but for safety)
val exitCode = process.waitFor()
return exitCode == 0
}
process.exitValue() == 0
} catch (e: Exception) {
false
}
Expand All @@ -19,8 +29,16 @@ object RootUtils {
// In many root managers, 'su -c id' will return 0 if granted
return try {
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "id"))
val exitCode = process.waitFor()
exitCode == 0
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (!process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {
process.destroyForcibly()
return false
}
} else {
val exitCode = process.waitFor()
return exitCode == 0
}
process.exitValue() == 0
} catch (e: Exception) {
false
}
Expand Down
Loading
Loading