diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c46fab63a..af8639f2e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,8 +39,8 @@ android { applicationId = "com.sameerasw.essentials" minSdk = 26 targetSdk = 37 - versionCode = 57 - versionName = "16.5" + versionCode = 58 + versionName = "16.6" val whatsNewCounter = 2 buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString()) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 43acf0d19..970dba07d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -534,6 +534,36 @@ android:resource="@xml/favorites_widget_info" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt index 1c0cbb82e..f1e2e19d5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt @@ -161,6 +161,10 @@ class ShutUpShortcutActivity : ComponentActivity() { ) } + if (config.disableUsbDebugging || config.disableWirelessDebugging) { + com.sameerasw.essentials.utils.ShizukuUtils.stopShizuku(this@ShutUpShortcutActivity) + } + // Always explicitly disable USB debugging if requested, even if dev options were already disabled // as some apps check this specific setting directly. if (config.disableUsbDebugging) { diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 463642d31..f086c578b 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -188,6 +188,7 @@ class SettingsRepository(private val context: Context) { const val KEY_BATTERY_WIDGET_BACKGROUND_ENABLED = "battery_widget_background_enabled" const val KEY_PINNED_FEATURES = "pinned_features" + const val KEY_PINNED_QS_TILES = "pinned_qs_tiles" const val KEY_LIKE_SONG_TOAST_ENABLED = "like_song_toast_enabled" const val KEY_LIKE_SONG_AOD_OVERLAY_ENABLED = "like_song_aod_overlay_enabled" const val KEY_AMBIENT_MUSIC_GLANCE_ENABLED = "ambient_music_glance_enabled" @@ -741,7 +742,8 @@ class SettingsRepository(private val context: Context) { if (key == KEY_GITHUB_ACCESS_TOKEN || key == KEY_GITHUB_WORKFLOW_TOKEN || key == KEY_SHIZUKU_AUTH_TOKEN || key.startsWith("mac_battery_") || key == "airsync_mac_connected" || key == KEY_SNOOZE_DISCOVERED_CHANNELS || - key == KEY_MAPS_DISCOVERED_CHANNELS || key == KEY_SHUT_UP_ORIGINAL_SETTINGS + key == KEY_MAPS_DISCOVERED_CHANNELS || key == KEY_SHUT_UP_ORIGINAL_SETTINGS || + key == "battery_history_points" ) { return@forEach } @@ -914,6 +916,22 @@ class SettingsRepository(private val context: Context) { putString(KEY_PINNED_FEATURES, json) } + fun getPinnedQsTiles(): List { + val json = prefs.getString(KEY_PINNED_QS_TILES, null) + return if (json != null) { + try { + gson.fromJson(json, Array::class.java).toList() + } catch (e: Exception) { + emptyList() + } + } else emptyList() + } + + fun savePinnedQsTiles(tiles: List) { + val json = gson.toJson(tiles) + putString(KEY_PINNED_QS_TILES, json) + } + fun getRecentSearches(): List { val json = prefs.getString(KEY_RECENT_SEARCHES, null) return if (json != null) { diff --git a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt index d7ed8ab01..348b8942a 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt @@ -155,7 +155,7 @@ object StatusBarIconRegistry { StatusBarIcon( id = "headset", displayNameRes = R.string.icon_headset, - blacklistNames = listOf("headset", "earphone"), + blacklistNames = listOf("headset", "earphone", "headset_system"), defaultVisible = false, categoryRes = CAT_AUDIO_MEDIA, iconRes = R.drawable.rounded_headset_mic_24 @@ -179,7 +179,7 @@ object StatusBarIconRegistry { StatusBarIcon( id = "clock", displayNameRes = R.string.icon_clock, - blacklistNames = listOf("clock"), + blacklistNames = listOf("clock", "clock_meridiem"), categoryRes = CAT_SYSTEM_STATUS, iconRes = R.drawable.rounded_nest_clock_farsight_analog_24 ), @@ -269,6 +269,13 @@ object StatusBarIconRegistry { categoryRes = CAT_SYSTEM_STATUS, iconRes = R.drawable.rounded_security_24 ), + StatusBarIcon( + id = "network_speed", + displayNameRes = R.string.icon_network_speed, + blacklistNames = listOf("network_speed"), + categoryRes = CAT_SYSTEM_STATUS, + iconRes = null + ), // --- OEM Specific --- StatusBarIcon( @@ -282,7 +289,6 @@ object StatusBarIconRegistry { id = "samsung_smart", displayNameRes = R.string.icon_samsung_smart, blacklistNames = listOf( - "glove", "gesture", "smart_scroll", "face", @@ -292,6 +298,27 @@ object StatusBarIconRegistry { categoryRes = CAT_OEM_SPECIFIC, iconRes = R.drawable.rounded_fiber_smart_record_24 ), + StatusBarIcon( + id = "glove_mode", + displayNameRes = R.string.icon_glove_mode, + blacklistNames = listOf("glove_mode", "glove"), + categoryRes = CAT_OEM_SPECIFIC, + iconRes = null + ), + StatusBarIcon( + id = "frame_rate", + displayNameRes = R.string.icon_frame_rate, + blacklistNames = listOf("frame_rate_mode"), + categoryRes = CAT_OEM_SPECIFIC, + iconRes = null + ), + StatusBarIcon( + id = "no_sim", + displayNameRes = R.string.icon_no_sim, + blacklistNames = listOf("nosim"), + categoryRes = CAT_PHONE_NETWORK, + iconRes = null + ), StatusBarIcon( id = "samsung_services", displayNameRes = R.string.icon_samsung_services, diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 1161d4869..670734ce8 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -271,6 +271,8 @@ object FeatureRegistry { ) { override fun isEnabled(viewModel: MainViewModel) = true override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + + override fun isDeviceSupported(context: Context) = DeviceUtils.isGoogleDevice() }, object : Feature( id = "Lock screen clock", @@ -1338,7 +1340,7 @@ object FeatureRegistry { context.startActivity( Intent( context, - com.sameerasw.essentials.AppUpdatesActivity::class.java + com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java ) ) } diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt index 028981a57..8de3a1a27 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt @@ -41,6 +41,11 @@ object DeviceInfoSyncManager { maxTorchLevel = FlashlightUtil.getMaxLevel(context, cameraId) isIntensitySupported = FlashlightUtil.isIntensitySupported(context, cameraId) syncDeviceInfo(context) + + val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt index 8fe150c45..a9cc210b5 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt @@ -141,6 +141,7 @@ class FlashlightHandler( } else { getCameraId()?.let { id -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !com.sameerasw.essentials.utils.DeviceUtils.isMediatekDevice() && FlashlightUtil.isIntensitySupported(service, id) ) { try { diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt new file mode 100644 index 000000000..aa28c694d --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt @@ -0,0 +1,39 @@ +package com.sameerasw.essentials.services.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.BatteryManager +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.utils.BatteryStatsUtil + +class BatteryAutomationReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + val bm = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager + val level = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1 + val isPlugged = action == Intent.ACTION_POWER_CONNECTED || (action == Intent.ACTION_BATTERY_CHANGED && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) > 0) + + if (level >= 0) { + val force = action == Intent.ACTION_POWER_CONNECTED || action == Intent.ACTION_POWER_DISCONNECTED + com.sameerasw.essentials.utils.BatteryHistoryManager.recordPoint(context, level, isPlugged, forceRecord = force) + } + + if (action == Intent.ACTION_POWER_DISCONNECTED) { + checkAutoResetBatteryStats(context) + } + } + + private fun checkAutoResetBatteryStats(context: Context) { + val repo = SettingsRepository(context) + val autoResetEnabled = repo.getBoolean("auto_reset_battery_stats", false) + if (autoResetEnabled) { + val bm = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager + val level = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1 + if (level >= 90) { + BatteryStatsUtil.resetStats(context) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt new file mode 100644 index 000000000..b47141168 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt @@ -0,0 +1,83 @@ +package com.sameerasw.essentials.services.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.sameerasw.essentials.MainActivity +import com.sameerasw.essentials.domain.controller.CaffeinateController +import com.sameerasw.essentials.services.tiles.* +import com.sameerasw.essentials.utils.HapticUtil + +class QsTileActionRouter : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_TRIGGER_TILE) return + + val serviceClassName = intent.getStringExtra(EXTRA_SERVICE_CLASS_NAME) ?: return + Log.d("QsTileActionRouter", "Triggering tile action for: $serviceClassName") + + // Perform haptics on click trigger + HapticUtil.performHapticForService(context) + + try { + when (serviceClassName) { + CaffeinateTileService::class.java.name -> { + if (CaffeinateController.isStarting.value) { + CaffeinateController.cycleTimeout(context) + } else { + CaffeinateController.toggle(context) + } + } + FlashlightTileService::class.java.name -> { + val flashlightIntent = Intent(context, FlashlightActionReceiver::class.java).apply { + action = FlashlightActionReceiver.ACTION_TOGGLE + } + context.sendBroadcast(flashlightIntent) + } + else -> { + val clazz = Class.forName(serviceClassName) + if (BaseTileService::class.java.isAssignableFrom(clazz)) { + val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService + + val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod( + "attachBaseContext", + Context::class.java + ) + attachBaseContextMethod.isAccessible = true + attachBaseContextMethod.invoke(tileService, context) + + tileService.onTileClick() + } else { + fallbackLaunch(context) + } + } + } + } catch (e: Exception) { + Log.e("QsTileActionRouter", "Failed to trigger tile $serviceClassName headlessly", e) + fallbackLaunch(context) + } + + // Notify widget to update state after action + val updateIntent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply { + setPackage(context.packageName) + } + context.sendBroadcast(updateIntent) + } + + private fun fallbackLaunch(context: Context) { + try { + val launchIntent = Intent(context, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(launchIntent) + } catch (e: Exception) { + Log.e("QsTileActionRouter", "Failed to launch activity", e) + } + } + + companion object { + const val ACTION_TRIGGER_TILE = "com.sameerasw.essentials.action.TRIGGER_QS_TILE" + const val EXTRA_SERVICE_CLASS_NAME = "extra_service_class_name" + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt index 922821dd4..569e21411 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt @@ -42,18 +42,30 @@ class ColorPickerTileService : BaseTileService() { intent, PendingIntent.FLAG_IMMUTABLE ) - startActivityAndCollapse(pendingIntent) + try { + startActivityAndCollapse(pendingIntent) + } catch (e: Exception) { + startActivity(intent) + } } else { - @Suppress("DEPRECATION") - startActivityAndCollapse(intent) + try { + @Suppress("DEPRECATION") + startActivityAndCollapse(intent) + } catch (e: Exception) { + startActivity(intent) + } } } catch (e: Exception) { - Handler(Looper.getMainLooper()).post { - Toast.makeText( - applicationContext, - getString(R.string.toast_eyedropper_failed), - Toast.LENGTH_SHORT - ).show() + try { + startActivity(intent) + } catch (ex: Exception) { + Handler(Looper.getMainLooper()).post { + Toast.makeText( + applicationContext, + getString(R.string.toast_eyedropper_failed), + Toast.LENGTH_SHORT + ).show() + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt index 5d4e8112b..eaa9e8841 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt @@ -19,6 +19,11 @@ class FlashlightTileService : BaseTileService() { super.onTorchModeChanged(cameraId, enabled) isTorchOn = enabled updateTile() + + val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply { + setPackage(packageName) + } + sendBroadcast(intent) } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt new file mode 100644 index 000000000..ea50e6e86 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt @@ -0,0 +1,174 @@ +package com.sameerasw.essentials.services.tiles + +import com.sameerasw.essentials.R + +object QsTileRegistry { + data class QsTileEntry( + val titleRes: Int, + val iconRes: Int, + val serviceClass: Class<*> + ) + + val ALL_TILES: List = listOf( + QsTileEntry(R.string.tile_ui_blur, R.drawable.rounded_blur_on_24, UiBlurTileService::class.java), + QsTileEntry(R.string.tile_bubbles, R.drawable.rounded_bubble_24, BubblesTileService::class.java), + QsTileEntry(R.string.tile_sensitive_content, R.drawable.rounded_notifications_off_24, PrivateNotificationsTileService::class.java), + QsTileEntry(R.string.tile_tap_to_wake, R.drawable.rounded_touch_app_24, TapToWakeTileService::class.java), + QsTileEntry(R.string.tile_aod, R.drawable.rounded_mobile_text_2_24, AlwaysOnDisplayTileService::class.java), + QsTileEntry(R.string.tile_caffeinate, R.drawable.rounded_coffee_24, CaffeinateTileService::class.java), + QsTileEntry(R.string.tile_sound_mode, R.drawable.rounded_volume_up_24, SoundModeTileService::class.java), + QsTileEntry(R.string.tile_notification_lighting, R.drawable.rounded_blur_linear_24, NotificationLightingTileService::class.java), + QsTileEntry(R.string.tile_dynamic_night_light, R.drawable.rounded_nightlight_24, DynamicNightLightTileService::class.java), + QsTileEntry(R.string.tile_locked_security, R.drawable.rounded_security_24, ScreenLockedSecurityTileService::class.java), + QsTileEntry(R.string.tile_app_lock, R.drawable.rounded_shield_lock_24, AppLockTileService::class.java), + QsTileEntry(R.string.tile_mono_audio, R.drawable.rounded_headphones_24, MonoAudioTileService::class.java), + QsTileEntry(R.string.tile_flashlight, R.drawable.rounded_flashlight_on_24, FlashlightTileService::class.java), + QsTileEntry(R.string.tile_app_freezing, R.drawable.rounded_app_badging_24, AppFreezingTileService::class.java), + QsTileEntry(R.string.tile_flashlight_pulse, R.drawable.outline_backlight_high_24, FlashlightPulseTileService::class.java), + QsTileEntry(R.string.tile_stay_awake, R.drawable.rounded_av_timer_24, StayAwakeTileService::class.java), + QsTileEntry(R.string.nfc_tile_label, R.drawable.rounded_nfc_24, NfcTileService::class.java), + QsTileEntry(R.string.tile_adaptive_brightness, R.drawable.rounded_brightness_auto_24, AdaptiveBrightnessTileService::class.java), + QsTileEntry(R.string.tile_scale_animations, R.drawable.rounded_front_hand_24, ScaleAnimationsTileService::class.java), + QsTileEntry(R.string.tile_restart_systemui, R.drawable.reopen_window_24px, RestartSystemUiTileService::class.java), + QsTileEntry(R.string.tile_refresh_rate, R.drawable.rounded_shutter_speed_24, RefreshRateTileService::class.java), + QsTileEntry(R.string.feat_maps_power_saving_title, R.drawable.rounded_navigation_24, MapsPowerSavingTileService::class.java), + QsTileEntry(R.string.tile_private_dns, R.drawable.rounded_dns_24, PrivateDnsTileService::class.java), + QsTileEntry(R.string.tile_usb_debugging, R.drawable.rounded_adb_24, UsbDebuggingTileService::class.java), + QsTileEntry(R.string.tile_color_picker, R.drawable.rounded_colorize_24, ColorPickerTileService::class.java), + QsTileEntry(R.string.tile_developer_options, R.drawable.rounded_mobile_code_24, DeveloperOptionsTileService::class.java), + QsTileEntry(R.string.tile_charge_optimization, R.drawable.rounded_battery_android_frame_shield_24, ChargeQuickTileService::class.java) + ) + + private val tilesMapByClassName: Map by lazy { + ALL_TILES.associateBy { it.serviceClass.name } + } + + fun getTileByClassName(className: String): QsTileEntry? { + return tilesMapByClassName[className] + } + + fun isTileActive(context: android.content.Context, className: String): Boolean { + return try { + when (className) { + CaffeinateTileService::class.java.name -> { + com.sameerasw.essentials.domain.controller.CaffeinateController.isActive.value || + com.sameerasw.essentials.domain.controller.CaffeinateController.isStarting.value + } + FlashlightTileService::class.java.name -> { + val instance = ScreenOffAccessibilityService.instance + if (instance != null) { + instance.flashlightHandler.isTorchOn + } else { + val clazz = Class.forName(className) + val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService + val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod( + "attachBaseContext", + android.content.Context::class.java + ) + attachBaseContextMethod.isAccessible = true + attachBaseContextMethod.invoke(tileService, context) + val getTileStateMethod = BaseTileService::class.java.getDeclaredMethod("getTileState") + getTileStateMethod.isAccessible = true + (getTileStateMethod.invoke(tileService) as Int) == android.service.quicksettings.Tile.STATE_ACTIVE + } + } + else -> { + val clazz = Class.forName(className) + if (BaseTileService::class.java.isAssignableFrom(clazz)) { + val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService + val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod( + "attachBaseContext", + android.content.Context::class.java + ) + attachBaseContextMethod.isAccessible = true + attachBaseContextMethod.invoke(tileService, context) + + val getTileStateMethod = BaseTileService::class.java.getDeclaredMethod("getTileState") + getTileStateMethod.isAccessible = true + val state = getTileStateMethod.invoke(tileService) as Int + state == android.service.quicksettings.Tile.STATE_ACTIVE + } else false + } + } + } catch (e: Exception) { + false + } + } + + fun getTileSubtitle(context: android.content.Context, className: String): String { + return try { + when (className) { + CaffeinateTileService::class.java.name -> { + if (com.sameerasw.essentials.domain.controller.CaffeinateController.isActive.value) { + "Active" + } else if (com.sameerasw.essentials.domain.controller.CaffeinateController.isStarting.value) { + "Starting" + } else "Off" + } + FlashlightTileService::class.java.name -> { + val instance = ScreenOffAccessibilityService.instance + val isTorchOn = if (instance != null) { + instance.flashlightHandler.isTorchOn + } else false + if (isTorchOn) "On" else "Off" + } + else -> { + val clazz = Class.forName(className) + if (BaseTileService::class.java.isAssignableFrom(clazz)) { + val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService + val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod( + "attachBaseContext", + android.content.Context::class.java + ) + attachBaseContextMethod.isAccessible = true + attachBaseContextMethod.invoke(tileService, context) + + val getTileSubtitleMethod = BaseTileService::class.java.getDeclaredMethod("getTileSubtitle") + getTileSubtitleMethod.isAccessible = true + (getTileSubtitleMethod.invoke(tileService) as? String) ?: "" + } else "" + } + } + } catch (e: Exception) { + "" + } + } + + fun getTileIcon(context: android.content.Context, className: String, fallbackIconRes: Int): Int { + return try { + when (className) { + FlashlightTileService::class.java.name -> { + val instance = ScreenOffAccessibilityService.instance + val isTorchOn = if (instance != null) { + instance.flashlightHandler.isTorchOn + } else false + if (isTorchOn) R.drawable.round_flashlight_on_24 else R.drawable.rounded_flashlight_on_24 + } + else -> { + val clazz = Class.forName(className) + if (BaseTileService::class.java.isAssignableFrom(clazz)) { + val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService + val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod( + "attachBaseContext", + android.content.Context::class.java + ) + attachBaseContextMethod.isAccessible = true + attachBaseContextMethod.invoke(tileService, context) + + val getTileIconMethod = BaseTileService::class.java.getDeclaredMethod("getTileIcon") + getTileIconMethod.isAccessible = true + val iconObj = getTileIconMethod.invoke(tileService) as? android.graphics.drawable.Icon + if (iconObj != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + val resId = iconObj.resId + if (resId != 0) resId else fallbackIconRes + } else { + fallbackIconRes + } + } else fallbackIconRes + } + } + } catch (e: Exception) { + fallbackIconRes + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt index 1e8b06124..9d3f5e62b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt @@ -8,6 +8,7 @@ import androidx.annotation.RequiresApi import com.sameerasw.essentials.FeatureSettingsActivity import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.RefreshRateUtils import com.sameerasw.essentials.utils.ShellUtils @@ -54,6 +55,8 @@ class RefreshRateTileService : BaseTileService() { override fun getTileSubtitle(): String = RefreshRateUtils.getDisplaySubtitle(this) + override fun isDeviceSupported(): Boolean = DeviceUtils.isGoogleDevice() + override fun hasFeaturePermission(): Boolean = ShellUtils.hasPermission(this) override fun getTileIcon(): Icon { diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt index 26d8eaf1d..4381a2198 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt @@ -47,7 +47,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene private var proximitySensor: Sensor? = null // Handlers - private lateinit var flashlightHandler: FlashlightHandler + lateinit var flashlightHandler: FlashlightHandler private lateinit var notificationLightingHandler: NotificationLightingHandler private lateinit var buttonRemapHandler: ButtonRemapHandler private lateinit var appFlowHandler: AppFlowHandler diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt index 751b58d19..0df6d946e 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt @@ -100,9 +100,15 @@ class FavoritesWidget : GlanceAppWidget() { val cellWidth = width / columnsCount.toFloat() val cellHeight = height / rowsCount.toFloat() + val scrollMode = if (Build.VERSION.SDK_INT >= 37) { + VerticalScrollMode.SnapScrollMatchHeight(height) + } else { + VerticalScrollMode.Normal + } + LazyColumn( modifier = GlanceModifier.fillMaxSize(), - verticalScrollMode = VerticalScrollMode.SnapScrollMatchHeight(height) + verticalScrollMode = scrollMode ) { items(pages) { pageFeatures -> Column( diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt new file mode 100644 index 000000000..824e28447 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt @@ -0,0 +1,29 @@ +package com.sameerasw.essentials.services.widgets + +import android.content.Context +import android.content.Intent +import androidx.glance.GlanceId +import androidx.glance.action.ActionParameters +import androidx.glance.appwidget.action.ActionCallback +import com.sameerasw.essentials.services.receivers.QsTileActionRouter + +class QsTileClickActionCallback : ActionCallback { + + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + val serviceClassName = parameters[SERVICE_CLASS_KEY] ?: return + // action + val intent = Intent(context, QsTileActionRouter::class.java).apply { + action = QsTileActionRouter.ACTION_TRIGGER_TILE + putExtra(QsTileActionRouter.EXTRA_SERVICE_CLASS_NAME, serviceClassName) + } + context.sendBroadcast(intent) + } + + companion object { + val SERVICE_CLASS_KEY = ActionParameters.Key("service_class_name") + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt new file mode 100644 index 000000000..f096a2039 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt @@ -0,0 +1,309 @@ +package com.sameerasw.essentials.services.widgets + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.ColorFilter +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalSize +import androidx.glance.action.actionParametersOf +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.lazy.VerticalScrollMode +import androidx.glance.appwidget.lazy.items +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxHeight +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.layout.width +import androidx.glance.text.FontFamily +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextAlign +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.services.tiles.QsTileRegistry +import com.sameerasw.essentials.utils.ColorUtil + +class QsTilesWidget : GlanceAppWidget() { + override val sizeMode = androidx.glance.appwidget.SizeMode.Exact + + @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1) + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + GlanceTheme { + val prefs = androidx.glance.currentState() + val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update") + @Suppress("UNUSED_VARIABLE") + val lastUpdate = prefs[KEY_UPDATE] ?: 0L + + val repository = SettingsRepository(context) + val pinnedClassNames = repository.getPinnedQsTiles() + + val pinnedTiles = pinnedClassNames.mapNotNull { QsTileRegistry.getTileByClassName(it) } + val width = LocalSize.current.width + val height = LocalSize.current.height + + Box( + modifier = GlanceModifier + .fillMaxSize() + ) { + if (pinnedTiles.isEmpty()) { + Column( + modifier = GlanceModifier + .fillMaxSize() + .padding(8.dp) + .cornerRadius(16.dp) + .background(GlanceTheme.colors.primary) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = context.getString(R.string.qs_tiles_widget_empty_state), + style = TextStyle( + color = GlanceTheme.colors.onPrimary, + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily("google-sans-flex"), + textAlign = TextAlign.Center + ), + modifier = GlanceModifier.fillMaxWidth() + ) + } + } else { + val columnsCount = if (width >= 300.dp) 2 else 1 + val targetRowHeight = 65.dp + val rowsCount = (height / targetRowHeight).toInt().coerceAtLeast(1) + val pageSize = columnsCount * rowsCount + val pages = pinnedTiles.chunked(pageSize) + + val halfSpacing = 4.dp + val cellHeight = targetRowHeight + + val scrollMode = if (Build.VERSION.SDK_INT >= 37) { + VerticalScrollMode.SnapScrollMatchHeight(height) + } else { + VerticalScrollMode.Normal + } + + LazyColumn( + modifier = GlanceModifier.fillMaxSize(), + verticalScrollMode = scrollMode + ) { + items(pages) { pageTiles -> + Column( + modifier = GlanceModifier.fillMaxSize().padding(halfSpacing), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val rows = pageTiles.chunked(columnsCount) + for (rowIndex in 0 until rowsCount) { + val rowTiles = rows.getOrNull(rowIndex) + if (rowTiles != null) { + Row( + modifier = GlanceModifier.fillMaxWidth() + .height(cellHeight), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally + ) { + for (colIndex in 0 until columnsCount) { + val tile = rowTiles.getOrNull(colIndex) + if (tile != null) { + val resolvedTitle = + context.getString(tile.titleRes) + val pastelColor = + ColorUtil.getPastelColorFor( + resolvedTitle + ) + val vibrantColor = + ColorUtil.getVibrantColorFor( + resolvedTitle + ) + + val isActive = QsTileRegistry.isTileActive( + context, + tile.serviceClass.name + ) + + val tileBg = if (isActive) { + GlanceTheme.colors.primary + } else { + GlanceTheme.colors.widgetBackground + } + val tileTextColor = if (isActive) { + GlanceTheme.colors.onPrimary + } else { + GlanceTheme.colors.onSurface + } + val tileSubtextColor = if (isActive) { + GlanceTheme.colors.onPrimary + } else { + GlanceTheme.colors.onSurfaceVariant + } + + val iconBoxSize = 38.dp + val iconCornerRadius = 10.dp + val iconSize = 22.dp + val fontSize = 13.sp + val spacerSize = 8.dp + + Box( + modifier = GlanceModifier + .defaultWeight() + .fillMaxHeight() + .padding( + horizontal = halfSpacing, + vertical = halfSpacing + ), + contentAlignment = Alignment.Center + ) { + val cardModifier = GlanceModifier + .fillMaxSize() + .cornerRadius(16.dp) + .background(tileBg) + .clickable( + actionRunCallback( + actionParametersOf( + QsTileClickActionCallback.SERVICE_CLASS_KEY to tile.serviceClass.name + ) + ) + ) + + val activeIconRes = QsTileRegistry.getTileIcon( + context, + tile.serviceClass.name, + tile.iconRes + ) + + Row( + modifier = cardModifier.padding( + horizontal = 12.dp, + vertical = 6.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically + ) { + + Box( + modifier = GlanceModifier + .size(iconBoxSize) + .cornerRadius( + iconCornerRadius + ) + .background(pastelColor), + contentAlignment = Alignment.Center + ) { + Image( + provider = ImageProvider( + activeIconRes + ), + contentDescription = resolvedTitle, + colorFilter = ColorFilter.tint( + ColorProvider( + vibrantColor + ) + ), + modifier = GlanceModifier.size( + iconSize + ) + ) + } + + Spacer( + modifier = GlanceModifier.width( + spacerSize + ) + ) + + val subtitle = QsTileRegistry.getTileSubtitle( + context, + tile.serviceClass.name + ) + + Column( + modifier = GlanceModifier.defaultWeight(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = resolvedTitle, + style = TextStyle( + color = tileTextColor, + fontSize = fontSize, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily( + "google-sans-flex" + ), + textAlign = TextAlign.Start + ), + maxLines = 1 + ) + if (subtitle.isNotEmpty()) { + Text( + text = subtitle, + style = TextStyle( + color = tileSubtextColor, + fontSize = 11.sp, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily( + "google-sans-flex" + ), + textAlign = TextAlign.Start + ), + maxLines = 1 + ) + } + } + } + } + } else { + Spacer( + modifier = GlanceModifier + .defaultWeight() + .fillMaxHeight() + .padding( + horizontal = halfSpacing, + vertical = halfSpacing + ) + ) + } + } + } + } else { + Spacer( + modifier = GlanceModifier + .fillMaxWidth() + .height(cellHeight) + .padding(vertical = halfSpacing) + ) + } + } + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt new file mode 100644 index 000000000..140435e2b --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt @@ -0,0 +1,58 @@ +package com.sameerasw.essentials.services.widgets + +import android.content.Context +import android.content.Intent +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import kotlinx.coroutines.launch + +class QsTilesWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = QsTilesWidget() + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + + val action = intent.action + if (action == Intent.ACTION_CONFIGURATION_CHANGED || + action == android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE || + action == "com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE" + ) { + kotlinx.coroutines.MainScope().launch { + try { + val glanceAppWidgetManager = + androidx.glance.appwidget.GlanceAppWidgetManager(context) + if (action == Intent.ACTION_CONFIGURATION_CHANGED) { + kotlinx.coroutines.delay(500) + } + + val glanceIds = glanceAppWidgetManager.getGlanceIds(QsTilesWidget::class.java) + val timestamp = System.currentTimeMillis() + glanceIds.forEach { glanceId -> + androidx.glance.appwidget.state.updateAppWidgetState(context, glanceId) { prefs -> + val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update") + prefs[KEY_UPDATE] = timestamp + } + glanceAppWidget.update(context, glanceId) + } + + // delay for qs status + val updatedGlanceIds = glanceAppWidgetManager.getGlanceIds(QsTilesWidget::class.java) + val newTimestamp = System.currentTimeMillis() + updatedGlanceIds.forEach { glanceId -> + androidx.glance.appwidget.state.updateAppWidgetState(context, glanceId) { prefs -> + val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update") + prefs[KEY_UPDATE] = newTimestamp + } + glanceAppWidget.update(context, glanceId) + } + } catch (e: Exception) { + android.util.Log.e( + "QsTilesWidgetReceiver", + "Error updating QS tiles widget on broadcast", + e + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt new file mode 100644 index 000000000..e89d123f0 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt @@ -0,0 +1,32 @@ +package com.sameerasw.essentials.ui.activities + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import com.sameerasw.essentials.FeatureSettingsActivity + +class QsTilesWidgetConfigurationActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appWidgetId = intent?.extras?.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) ?: AppWidgetManager.INVALID_APPWIDGET_ID + + val resultValue = Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + setResult(RESULT_OK, resultValue) + + val settingsIntent = Intent(this, FeatureSettingsActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra("feature", "Quick settings tiles") + } + startActivity(settingsIntent) + + finish() + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt index b64f71b26..2657fc92c 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap @@ -56,7 +57,11 @@ fun BatteryAppsTabContent( onToggleShowAll: () -> Unit, showPercentage: Boolean, onToggleUnit: () -> Unit, - view: View + view: View, + currentLevel: Int = 100, + chargeTimeRemainingMs: Long? = null, + avgCurrentMa: Int? = null, + isPlugged: Boolean = false ) { if (isLoadingAdvanced) { RoundedCardContainer(modifier = Modifier.fillMaxWidth()) { @@ -74,6 +79,8 @@ fun BatteryAppsTabContent( val displayedApps = if (showAllApps) usageApps else usageApps.take(20) val totalMah = usageApps.sumOf { it.powerMah }.coerceAtLeast(0.0001) + + RoundedCardContainer(modifier = Modifier.fillMaxWidth()) { displayedApps.forEach { app -> val displayValue = if (showPercentage) { @@ -87,6 +94,11 @@ fun BatteryAppsTabContent( var showMenu by remember { mutableStateOf(false) } var translationSheetKey by remember { mutableStateOf(null) } + val context = LocalContext.current + val canOpenAppInfo = remember(app.packageName) { + app.packageName != null && app.packageName.contains(".") + } + Box { Row( modifier = Modifier @@ -100,7 +112,7 @@ fun BatteryAppsTabContent( HapticUtil.performVirtualKeyHaptic(view) onToggleUnit() }, - onLongClick = if (isTranslationModeActive) { + onLongClick = if (isTranslationModeActive || canOpenAppInfo) { { HapticUtil.performVirtualKeyHaptic(view) showMenu = true @@ -168,13 +180,38 @@ fun BatteryAppsTabContent( expanded = showMenu, onDismissRequest = { showMenu = false } ) { - com.sameerasw.essentials.translation.ui.TranslationMenuItems( - title = app.appName, - onSelectKey = { key -> - showMenu = false - translationSheetKey = key - } - ) + if (canOpenAppInfo && app.packageName != null) { + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.label_app_info)) }, + onClick = { + showMenu = false + try { + val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = android.net.Uri.fromParts("package", app.packageName, null) + addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } catch (_: Exception) { + } + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_info_24), + contentDescription = null + ) + } + ) + } + + if (isTranslationModeActive) { + com.sameerasw.essentials.translation.ui.TranslationMenuItems( + title = app.appName, + onSelectKey = { key -> + showMenu = false + translationSheetKey = key + } + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt index 360c301d3..1d9f1ccc8 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt @@ -19,6 +19,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.Image +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonGroupDefaults @@ -36,6 +39,12 @@ 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.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.foundation.Canvas import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -47,6 +56,8 @@ import com.sameerasw.essentials.ui.theme.Shapes import com.sameerasw.essentials.utils.HapticUtil import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable +import androidx.compose.material3.Card +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import java.util.Locale @OptIn(ExperimentalFoundationApi::class) @@ -245,6 +256,104 @@ fun BatteryLoadingIndicatorCard() { } } +@Composable +fun TopAppsBreakdownHeader( + usageApps: List +) { + if (usageApps.isEmpty()) return + + val totalAllMah = remember(usageApps) { usageApps.sumOf { it.powerMah }.coerceAtLeast(0.0001) } + val majorApps = remember(usageApps, totalAllMah) { + usageApps.filter { (it.powerMah / totalAllMah) * 100.0 >= 2.5 } + } + val remainingApps = remember(usageApps, majorApps) { + usageApps.filterNot { majorApps.contains(it) } + } + val otherMah = remember(remainingApps) { remainingApps.sumOf { it.powerMah } } + + val hasOther = otherMah > 0.0001 + val totalSegments = majorApps.size + (if (hasOther) 1 else 0) + val context = androidx.compose.ui.platform.LocalContext.current + + Column( + modifier = Modifier + .fillMaxWidth() + .height(88.dp) + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + majorApps.forEachIndexed { index, app -> + val weight = ((app.powerMah / totalAllMah) * 100.0).toFloat().coerceAtLeast(1f) + var brandColor by remember(app.packageName) { + mutableStateOf(null) + } + + androidx.compose.runtime.LaunchedEffect(app.packageName) { + if (app.packageName != null) { + com.sameerasw.essentials.utils.AppUtil.getAppBrandColor(context, app.packageName) { argb -> + if (argb != android.graphics.Color.TRANSPARENT && argb != android.graphics.Color.GRAY) { + brandColor = Color(argb) + } + } + } + } + + val barColor = brandColor ?: MaterialTheme.colorScheme.primaryContainer + + val shape = when { + totalSegments == 1 -> CircleShape + index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes().shape + index == totalSegments - 1 -> ButtonGroupDefaults.connectedTrailingButtonShapes().shape + else -> ButtonGroupDefaults.connectedMiddleButtonShapes().shape + } + + Box( + modifier = Modifier + .weight(weight) + .fillMaxHeight() + .clip(shape) + .background(barColor), + contentAlignment = Alignment.Center + ) { + if (app.icon != null) { + val bitmap = remember(app.icon) { app.icon.toBitmap(48, 48).asImageBitmap() } + Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } else { + Icon( + painter = painterResource(id = R.drawable.rounded_info_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(18.dp) + ) + } + } + } + + if (hasOther) { + val otherWeight = ((otherMah / totalAllMah) * 100.0).toFloat().coerceAtLeast(1f) + val otherShape = if (majorApps.isEmpty()) CircleShape else ButtonGroupDefaults.connectedTrailingButtonShapes().shape + Box( + modifier = Modifier + .weight(otherWeight) + .fillMaxHeight() + .clip(otherShape) + .background(MaterialTheme.colorScheme.outlineVariant) + ) + } + } + } +} + @Composable fun BatteryUsageBreakdownHeader( appsPct: Float, @@ -345,3 +454,4 @@ private fun BreakdownLegendItem( color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) } + diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt index a60b2f9d0..bbdcbcb29 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt @@ -484,6 +484,61 @@ fun BatteryInfoTabContent( ) } } + + val isShellAvailableForReset = com.sameerasw.essentials.utils.ShellUtils.isAvailable(context) && com.sameerasw.essentials.utils.ShellUtils.hasPermission(context) + if (isShellAvailableForReset) { + RoundedCardContainer(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall + ) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.label_reset_battery_stats_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = stringResource(R.string.label_reset_battery_stats_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Button( + onClick = { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + if (com.sameerasw.essentials.utils.BatteryStatsUtil.resetStats(context)) { + onRefresh() + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(R.string.action_reset_battery_stats)) + } + } + + var autoResetEnabled by remember { + mutableStateOf(settingsRepository.getBoolean("auto_reset_battery_stats", false)) + } + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + title = stringResource(R.string.label_auto_reset_battery_stats_title), + description = stringResource(R.string.label_auto_reset_battery_stats_desc), + isChecked = autoResetEnabled, + onCheckedChange = { isChecked -> + autoResetEnabled = isChecked + settingsRepository.putBoolean("auto_reset_battery_stats", isChecked) + }, + iconRes = R.drawable.rounded_cycle_24 + ) + } + } } // Permission Grant Card at the very bottom if BATTERY_STATS permission is missing AND Shizuku/Root is available & permitted diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt index cdf8c19a9..3ef636914 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt @@ -223,6 +223,10 @@ fun BatteryDetailsBottomSheet( color = MaterialTheme.colorScheme.onSurface ) } + } else if (selectedTab == 1) { + com.sameerasw.essentials.ui.components.battery.TopAppsBreakdownHeader( + usageApps = usageApps + ) } else { com.sameerasw.essentials.ui.components.battery.BatteryUsageBreakdownHeader( appsPct = appsPct, @@ -269,7 +273,11 @@ fun BatteryDetailsBottomSheet( onToggleShowAll = { showAllApps = !showAllApps }, showPercentage = showPercentage, onToggleUnit = { showPercentage = !showPercentage }, - view = view + view = view, + currentLevel = batteryDetails.level, + chargeTimeRemainingMs = batteryDetails.chargeTimeRemainingMs, + avgCurrentMa = batteryDetails.currentAvgMa, + isPlugged = batteryDetails.plugged > 0 ) 2 -> BatterySystemTabContent( isLoadingAdvanced = isLoadingAdvanced, 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 39a5e4222..3818acefc 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 @@ -1466,6 +1466,11 @@ private fun SearchResultsSection( if (targetFeatureKey == "LiveWallpaper") "live" else "daily" ) } + } else if (targetFeatureKey == "App updates") { + Intent( + context, + com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java + ) } else { Intent( context, @@ -1492,6 +1497,11 @@ private fun SearchResultsSection( if (result.featureKey == "LiveWallpaper") "live" else "daily" ) } + } else if (result.featureKey == "App updates") { + Intent( + context, + com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java + ) } else { Intent(context, FeatureSettingsActivity::class.java).apply { putExtra("feature", result.featureKey) 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 aa953d2b9..4596adc4b 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 @@ -293,7 +293,8 @@ fun QuickSettingsTilesSettingsUI( RefreshRateTileService::class.java, listOf("SHIZUKU"), R.string.about_desc_refresh_rate_tile, - R.string.cat_visuals + R.string.cat_visuals, + isSupported = { _ -> DeviceUtils.isGoogleDevice() } ), QSTileInfo( R.string.feat_maps_power_saving_title, @@ -483,6 +484,9 @@ fun QuickSettingsTilesSettingsUI( it.contains(tile.serviceClass.name) } + val pinnedQsTiles by viewModel.pinnedQsTileKeys + val isPinned = pinnedQsTiles.contains(tile.serviceClass.name) + QSTileCard( tile = tile, modifier = Modifier @@ -495,6 +499,7 @@ fun QuickSettingsTilesSettingsUI( ), isMissingPermissions = !allPermissionsGranted, isAdded = isAdded, + isPinned = isPinned, onClick = { if (!allPermissionsGranted) { selectedTileForPermissions = tile @@ -529,6 +534,9 @@ fun QuickSettingsTilesSettingsUI( } } }, + onPinToggle = { + viewModel.togglePinQsTile(tile.serviceClass.name) + }, onHelpClick = if (tile.aboutDescription != null) { { selectedHelpTile = tile @@ -552,6 +560,104 @@ fun QuickSettingsTilesSettingsUI( modifier = Modifier.padding(top = 8.dp) ) + Spacer(modifier = Modifier.height(16.dp)) + + // Favorites Widget Section + val pinnedQsTiles by viewModel.pinnedQsTileKeys + val pinnedTileInfos = remember(pinnedQsTiles, tiles) { + pinnedQsTiles.mapNotNull { className -> tiles.find { it.serviceClass.name == className } } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(start = 12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_widgets_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(26.dp) + ) + Text( + text = stringResource(R.string.qs_tiles_widget_name), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + } + + Text( + text = "Long press tiles to add", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp) + ) + + if (pinnedTileInfos.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.qs_tiles_widget_empty_state), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + pinnedTileInfos.chunked(2).forEach { rowTiles -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + rowTiles.forEach { tile -> + val allPermissionsGranted = tile.permissionKeys.all { key -> + PermissionUIHelper.getPermissionItem( + key, + context, + viewModel + )?.isGranted == true + } + val addedTiles by viewModel.addedQSTiles + val componentName = ComponentName(context, tile.serviceClass) + val isAdded = addedTiles.any { + it.contains(componentName.flattenToString()) || + it.contains(componentName.flattenToShortString()) || + it.contains(tile.serviceClass.name) + } + + QSTileCard( + tile = tile, + modifier = Modifier.weight(1f), + isMissingPermissions = !allPermissionsGranted, + isAdded = isAdded, + isPinned = true, + onClick = { + viewModel.togglePinQsTile(tile.serviceClass.name) + }, + onPinToggle = { + viewModel.togglePinQsTile(tile.serviceClass.name) + } + ) + } + if (rowTiles.size < 2) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) } } @@ -561,8 +667,10 @@ fun QSTileCard( tile: QSTileInfo, isMissingPermissions: Boolean, isAdded: Boolean, + isPinned: Boolean = false, modifier: Modifier = Modifier, onClick: () -> Unit, + onPinToggle: (() -> Unit)? = null, onHelpClick: (() -> Unit)? = null ) { val view = LocalView.current @@ -612,7 +720,7 @@ fun QSTileCard( onClick() }, onLongClick = { - if (onHelpClick != null) { + if (onHelpClick != null || onPinToggle != null) { com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) showMenu = true } @@ -638,7 +746,7 @@ fun QSTileCard( modifier = Modifier.padding(8.dp) ) - Column { + Column(modifier = Modifier.weight(1f)) { Text( text = stringResource(tile.titleRes), style = MaterialTheme.typography.titleMedium, @@ -656,28 +764,65 @@ fun QSTileCard( overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis ) } + + if (isPinned) { + Icon( + painter = painterResource(id = R.drawable.round_star_24), + contentDescription = null, + tint = contentColor, + modifier = Modifier + .size(16.dp) + .padding(top = 2.dp) + ) + } } - if (onHelpClick != null) { + if (onHelpClick != null || onPinToggle != 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 - onHelpClick() - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_help_24), - contentDescription = null - ) - } - ) + if (onPinToggle != null) { + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( + text = { + Text( + if (isPinned) stringResource(R.string.action_unpin) + else stringResource(R.string.action_pin) + ) + }, + onClick = { + showMenu = false + onPinToggle() + }, + leadingIcon = { + Icon( + painter = painterResource( + id = if (isPinned) R.drawable.rounded_bookmark_remove_24 + else R.drawable.rounded_bookmark_24 + ), + contentDescription = null + ) + } + ) + } + + if (onHelpClick != null) { + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( + text = { + Text(stringResource(R.string.action_what_is_this)) + }, + onClick = { + showMenu = false + onHelpClick() + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_help_24), + contentDescription = null + ) + } + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt index 6ae7a8314..c06281a91 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt @@ -318,6 +318,49 @@ fun StatusBarIconSettingsUI( modifier = Modifier.highlight(highlightSetting == "clock_seconds") ) + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = androidx.compose.foundation.shape.RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .highlight(highlightSetting == "clock_position") + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 20.dp, end = 16.dp, bottom = 2.dp), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.stb_clock_position), + style = MaterialTheme.typography.titleSmall, + color = if (isPermissionGranted) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.38f + ) + ) + } + SegmentedPicker( + items = listOf(1, 2, 3), + selectedItem = viewModel.clockPosition.value, + onItemSelected = { pos -> + viewModel.setClockPosition(pos, context) + }, + labelProvider = { pos -> + when (pos) { + 1 -> context.getString(R.string.stb_clock_position_left) + 2 -> context.getString(R.string.stb_clock_position_center) + 3 -> context.getString(R.string.stb_clock_position_right) + else -> "" + } + }, + modifier = Modifier + .fillMaxWidth() + .then(if (!isPermissionGranted) Modifier.alpha(0.5f) else Modifier), + ) + } + IconToggleItem( iconRes = R.drawable.rounded_security_24, title = stringResource(R.string.stb_privacy_chip), diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt new file mode 100644 index 000000000..b57e21e1a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt @@ -0,0 +1,70 @@ +package com.sameerasw.essentials.utils + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.sameerasw.essentials.data.repository.SettingsRepository + +data class BatteryHistoryPoint( + val timestamp: Long, + val level: Int, + val isPlugged: Boolean +) + +object BatteryHistoryManager { + private const val KEY_HISTORY_POINTS = "battery_history_points" + private const val HOURLY_INTERVAL_MS = 45 * 60 * 1000L // 45 minutes + + fun getHistory(context: Context): List { + val repo = SettingsRepository(context) + val json = repo.getString(KEY_HISTORY_POINTS, "[]") + val lastResetTime = repo.getLong("last_battery_stats_reset_time", -1L) + + val list = try { + val type = object : TypeToken>() {}.type + Gson().fromJson>(json, type) ?: emptyList() + } catch (_: Exception) { + emptyList() + } + + val filtered = if (lastResetTime > 0L) { + list.filter { it.timestamp >= lastResetTime } + } else { + list + } + + return filtered.sortedBy { it.timestamp } + } + + fun recordPoint(context: Context, level: Int, isPlugged: Boolean, forceRecord: Boolean = false) { + val repo = SettingsRepository(context) + val now = System.currentTimeMillis() + val currentHistory = getHistory(context).toMutableList() + + if (currentHistory.isNotEmpty()) { + val lastPoint = currentHistory.last() + val stateChanged = lastPoint.isPlugged != isPlugged + val timeElapsed = now - lastPoint.timestamp >= HOURLY_INTERVAL_MS + + if (!forceRecord && !stateChanged && !timeElapsed) { + return + } + } + + currentHistory.add(BatteryHistoryPoint(now, level, isPlugged)) + + // Keep last 100 + val trimmedHistory = if (currentHistory.size > 100) { + currentHistory.takeLast(100) + } else { + currentHistory + } + + repo.putString(KEY_HISTORY_POINTS, Gson().toJson(trimmedHistory)) + } + + fun clearHistory(context: Context) { + val repo = SettingsRepository(context) + repo.putString(KEY_HISTORY_POINTS, "[]") + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt index 4571f1c12..2614547d9 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt @@ -100,10 +100,15 @@ object BatteryInfoUtil { val rawEnergy = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER)?.takeIf { it != Long.MIN_VALUE } val remainingEnergyMwh = rawEnergy?.let { it / 1_000_000 } // nWh to mWh - val chargeTimeRemaining = if (android.os.Build.VERSION.SDK_INT >= 28) { + val isPlugged = plugged > 0 || status == BatteryManager.BATTERY_STATUS_CHARGING + val computedChargeTime = if (android.os.Build.VERSION.SDK_INT >= 28) { bm?.computeChargeTimeRemaining()?.takeIf { it >= 0 } } else null + val chargeTimeRemaining = if (isPlugged) { + computedChargeTime + } else null + val hasStatsPerm = hasBatteryStatsPermission(context) var stateOfHealth: Int? = null diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt index 8f95e90a2..bd819e53d 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository data class BatteryUsageApp( val uid: Int, @@ -24,6 +25,17 @@ data class CpuWakeupItem( object BatteryStatsUtil { + fun resetStats(context: Context): Boolean { + val res1 = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --reset") + ShellUtils.runCommand(context, "cmd battery reset") + if (res1 != null) { + val repo = SettingsRepository(context) + repo.putLong("last_battery_stats_reset_time", System.currentTimeMillis()) + BatteryHistoryManager.clearHistory(context) + } + return res1 != null + } + fun parseUsageApps(context: Context): List { val output = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --usage") ?: return emptyList() @@ -36,13 +48,45 @@ object BatteryStatsUtil { var currentFg = 0L var currentBg = 0L + fun getSystemUidLabel(uid: Int, pkg: String?): String { + return when (uid) { + -5 -> "Tethering & Hotspot" + 0 -> "Root / Kernel" + 1001 -> "Telephony" + 1003 -> "Graphics / GPU" + 1010 -> "Wi-Fi" + 1013 -> "Media Server" + 1017 -> "Keystore" + 1019 -> "DRM" + 1020 -> "Multicast DNS" + 1021 -> "GPS" + 1036 -> "Log Daemon" + 1040 -> "Media Extractor" + 1041 -> "Audio Server" + 1046 -> "Media Codec" + 1047 -> "Camera Server" + 1053 -> "WebView Zygote" + 1058 -> "Crash Dumps" + 1064 -> "Hardware Security" + 1066 -> "Stats Daemon" + 1067 -> "Incident Daemon" + 1069 -> "Low Memory Killer" + 1072 -> "GPU Service" + 1080 -> "Context Hub" + 1082 -> "ART Service" + 1083 -> "UWB Subsystem" + 1092 -> "PRNG Seeder" + else -> pkg?.let { getAppName(pm, it) } ?: "System ($uid)" + } + } + output.lines().forEach { line -> val trimmed = line.trim() if (trimmed.startsWith("UID ")) { val uid = currentUid if (uid != null && currentMah > 0.0001) { val pkg = pm.getPackagesForUid(uid)?.firstOrNull() - val label = pkg?.let { getAppName(pm, it) } ?: "UID $uid" + val label = getSystemUidLabel(uid, pkg) val drawable = pkg?.let { getAppIcon(pm, it) } list.add(BatteryUsageApp(uid, pkg, label, currentMah, currentFg, currentBg, drawable)) } @@ -65,7 +109,7 @@ object BatteryStatsUtil { val lastUid = currentUid if (lastUid != null && currentMah > 0.0001) { val pkg = pm.getPackagesForUid(lastUid)?.firstOrNull() - val label = pkg?.let { getAppName(pm, it) } ?: "UID $lastUid" + val label = getSystemUidLabel(lastUid, pkg) val drawable = pkg?.let { getAppIcon(pm, it) } list.add(BatteryUsageApp(lastUid, pkg, label, currentMah, currentFg, currentBg, drawable)) } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt index 854e8a0b8..c11e86f56 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt @@ -32,17 +32,32 @@ data class DeviceInfo( ) object DeviceUtils { - fun isGoogleDevice(): Boolean { - return Build.MANUFACTURER.equals("google", ignoreCase = true) || + private val isGoogleDeviceLazy: Boolean by lazy { + Build.MANUFACTURER.equals("google", ignoreCase = true) || Build.BRAND.equals("google", ignoreCase = true) || Build.PRODUCT.contains("pixel", ignoreCase = true) } - fun isSamsungDevice(): Boolean { - return Build.MANUFACTURER.equals("samsung", ignoreCase = true) || + private val isSamsungDeviceLazy: Boolean by lazy { + Build.MANUFACTURER.equals("samsung", ignoreCase = true) || Build.BRAND.equals("samsung", ignoreCase = true) } + private val isMediatekDeviceLazy: Boolean by lazy { + val hardware = Build.HARDWARE.lowercase() + val board = Build.BOARD.lowercase() + val brand = Build.BRAND.lowercase() + val manufacturer = Build.MANUFACTURER.lowercase() + + hardware.contains("mt") || hardware.contains("mediatek") || hardware.contains("dimensity") || + board.contains("mt") || board.contains("mediatek") || board.contains("dimensity") || + brand.contains("mediatek") || manufacturer.contains("mediatek") + } + + fun isGoogleDevice(): Boolean = isGoogleDeviceLazy + fun isSamsungDevice(): Boolean = isSamsungDeviceLazy + fun isMediatekDevice(): Boolean = isMediatekDeviceLazy + fun getDeviceInfo(context: Context): DeviceInfo { val deviceName = try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { diff --git a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt index 0577e0ffe..9e49e4b75 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt @@ -118,7 +118,7 @@ object FlashlightUtil { ) val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || DeviceUtils.isMediatekDevice()) { return safeSetTorchMode(cameraManager, cameraId, toLevel > 0) } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt index 2d292d09c..174568224 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt @@ -121,4 +121,16 @@ object ShizukuUtils { null } } + + fun stopShizuku(context: android.content.Context) { + try { + val intent = android.content.Intent("moe.shizuku.privileged.api.STOP").apply { + `package` = "moe.shizuku.privileged.api" + addFlags(android.content.Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + } + context.sendBroadcast(intent) + } catch (e: Exception) { + android.util.Log.e("ShizukuUtils", "Failed to stop Shizuku", e) + } + } } 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 8ad672035..e511451e0 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -83,6 +83,7 @@ class MainViewModel : ViewModel() { val isShizukuPermissionGranted = mutableStateOf(false) val isShizukuAvailable = mutableStateOf(false) val pinnedFeatureKeys = mutableStateOf>(emptyList()) + val pinnedQsTileKeys = mutableStateOf>(emptyList()) val isNotificationListenerEnabled = mutableStateOf(false) val isMapsPowerSavingEnabled = mutableStateOf(false) val isNotificationLightingEnabled = mutableStateOf(false) @@ -1544,6 +1545,7 @@ class MainViewModel : ViewModel() { isPreReleaseCheckEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED) pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures() + pinnedQsTileKeys.value = settingsRepository.getPinnedQsTiles() isLikeSongToastEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_LIKE_SONG_TOAST_ENABLED, true) isLikeSongAodOverlayEnabled.value = settingsRepository.getBoolean( @@ -1699,6 +1701,24 @@ class MainViewModel : ViewModel() { } } + fun togglePinQsTile(serviceClassName: String) { + val current = pinnedQsTileKeys.value.toMutableList() + if (current.contains(serviceClassName)) { + current.remove(serviceClassName) + } else { + current.add(serviceClassName) + } + pinnedQsTileKeys.value = current + settingsRepository.savePinnedQsTiles(current) + + appContext?.let { context -> + val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + } + } + fun setAutoUpdateEnabled(enabled: Boolean, context: Context) { isAutoUpdateEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_AUTO_UPDATE_ENABLED, enabled) diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt index e26111588..0680c8fa6 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt @@ -24,6 +24,7 @@ class StatusBarIconViewModel : ViewModel() { val isSmartDataEnabled = mutableStateOf(false) val selectedNetworkTypes = mutableStateOf(setOf(NetworkType.NETWORK_4G, NetworkType.NETWORK_5G)) val isClockSecondsEnabled = mutableStateOf(false) + val clockPosition = mutableStateOf(0) val batteryPercentageMode = mutableStateOf(0) // 0: Hide, 1: Always, 2: Charging val isPrivacyChipEnabled = mutableStateOf(true) val isWriteSettingsEnabled = mutableStateOf(false) @@ -51,6 +52,7 @@ class StatusBarIconViewModel : ViewModel() { const val PREF_SMART_DATA_ENABLED = "smart_data_enabled" const val PREF_SELECTED_NETWORK_TYPES = "selected_network_types" const val PREF_BATTERY_PERCENT_MODE = "battery_percent_mode" + const val PREF_CLOCK_POSITION = "clock_position" const val PREF_HIDE_SYSTEM_ICONS = "hide_system_icons" const val PREF_HIDE_SYSTEM_ICONS_LOCKED_ONLY = "hide_system_icons_locked_only" const val PREF_HIDE_CLOCK = "hide_clock" @@ -235,7 +237,21 @@ class StatusBarIconViewModel : ViewModel() { private fun updateIconBlacklist(context: Context) { if (!isWriteSecureSettingsEnabled.value) return - val blacklistNames = StatusBarIconRegistry.getBlacklistNames(getIconVisibilities()) + val blacklistNames = StatusBarIconRegistry.getBlacklistNames(getIconVisibilities()).toMutableSet() + when (clockPosition.value) { + 1 -> { + blacklistNames.add("right_clock_position") + blacklistNames.add("middle_clock_position") + } + 2 -> { + blacklistNames.add("left_clock_position") + blacklistNames.add("right_clock_position") + } + 3 -> { + blacklistNames.add("left_clock_position") + blacklistNames.add("middle_clock_position") + } + } updateIconBlacklistSetting(context, blacklistNames) } @@ -322,6 +338,7 @@ class StatusBarIconViewModel : ViewModel() { setAdvancedFlagEnabled(context, PREF_HIDE_CLOCK, false) setAdvancedFlagEnabled(context, PREF_HIDE_NOTIFICATION_ICONS, false) setBatteryPercentageMode(0, context) + setClockPosition(0, context) // Build default visibility map val defaultVisibilities = @@ -334,6 +351,7 @@ class StatusBarIconViewModel : ViewModel() { private fun loadStatusBarSettings(context: Context) { val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) batteryPercentageMode.value = prefs.getInt(PREF_BATTERY_PERCENT_MODE, 0) + clockPosition.value = prefs.getInt(PREF_CLOCK_POSITION, 0) // Load Clock Seconds isClockSecondsEnabled.value = @@ -350,6 +368,14 @@ class StatusBarIconViewModel : ViewModel() { ) == 1) } + fun setClockPosition(position: Int, context: Context) { + clockPosition.value = position + context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE).edit { + putInt(PREF_CLOCK_POSITION, position) + } + updateIconBlacklist(context) + } + fun setClockSecondsEnabled(enabled: Boolean, context: Context) { isClockSecondsEnabled.value = enabled updateSettingsValue(context, "clock_seconds", if (enabled) 1 else 0) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 959dc4ed6..44331252a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ + App Info Customization BETA Attempt Shizuku restart @@ -356,9 +357,17 @@ Samsung Smart Features Samsung Services Ethernet + Network Speed + Glove Mode + Refresh Rate / Frame Rate + No SIM Show Seconds in Clock + Clock Position + Left + Center + Right Battery Percentage Always Charging @@ -1702,7 +1711,17 @@ Critical Throttling Emergency Throttling Shutdown Imminent - Charging mode QS tile options + Options + Reset stats + Reset Battery Statistics + Resets system battery statistics (dumpsys batterystats --reset). This clears historical battery usage and app drain data so it starts tracking fresh from the current charge. + Auto reset + Automatically reset battery statistics when the device is unplugged above 90% charge. + Battery Drain & Prediction + Actual drain + Predicted + %s to go + %s ago CPU wakeup history Hide CPU wakeups Screen On Drain @@ -1838,6 +1857,9 @@ Favorite Features View and launch your favorite features at a glance. No favorite features pinned yet. Open the app to pin some! + Favorite Tiles + Trigger your favorite Quick Settings tiles directly from your home screen. + No favorite tiles pinned yet. Open the app to pin some! control Essentials features read, update and control Essentials features and settings diff --git a/app/src/main/res/xml/qs_tiles_widget_info.xml b/app/src/main/res/xml/qs_tiles_widget_info.xml new file mode 100644 index 000000000..092ff01ef --- /dev/null +++ b/app/src/main/res/xml/qs_tiles_widget_info.xml @@ -0,0 +1,15 @@ + +