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 01baac26a..d5cea4190 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 @@ -369,6 +369,7 @@ class SettingsRepository( const val KEY_DUO_SHOW_MEDIA = "duo_show_media" const val KEY_DUO_SHOW_PROGRESS = "duo_show_progress" const val KEY_DUO_SHOW_FLASHLIGHT = "duo_show_flashlight" + const val KEY_DUO_SHOW_CHARGING_SURGE = "duo_show_charging_surge" const val KEY_DUO_HIDE_WHEN_SCREEN_OFF = "duo_hide_when_screen_off" const val KEY_DUO_HIDE_WHEN_SCREEN_OFF_ONLY_IDLE = "duo_hide_when_screen_off_only_idle" const val KEY_DUO_USE_MATERIAL_YOU = "duo_use_material_you" @@ -1408,8 +1409,9 @@ class SettingsRepository( ): Boolean { return try { val json = inputStream.bufferedReader().use { it.readText() } + val type = object : com.google.gson.reflect.TypeToken>>>() {}.type val allConfigs: Map>> = - gson.fromJson(json, Map::class.java) as Map>> + gson.fromJson(json, type) ?: emptyMap() allConfigs.forEach { (fileName, prefWrapper) -> val p = context.getSharedPreferences(fileName, Context.MODE_PRIVATE) @@ -3109,6 +3111,9 @@ class SettingsRepository( fun isDuoShowFlashlightEnabled(): Boolean = getBoolean(KEY_DUO_SHOW_FLASHLIGHT, true) fun setDuoShowFlashlightEnabled(enabled: Boolean) = putBoolean(KEY_DUO_SHOW_FLASHLIGHT, enabled) + fun isDuoShowChargingSurgeEnabled(): Boolean = getBoolean(KEY_DUO_SHOW_CHARGING_SURGE, true) + fun setDuoShowChargingSurgeEnabled(enabled: Boolean) = putBoolean(KEY_DUO_SHOW_CHARGING_SURGE, enabled) + fun isDuoHideWhenScreenOffEnabled(): Boolean = getBoolean(KEY_DUO_HIDE_WHEN_SCREEN_OFF, true) fun setDuoHideWhenScreenOffEnabled(enabled: Boolean) = putBoolean(KEY_DUO_HIDE_WHEN_SCREEN_OFF, enabled) 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 94cdc30a6..1746ccc20 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 @@ -1387,6 +1387,36 @@ object FeatureRegistry { enabled: Boolean, ) {} }, + object : Feature( + id = "Duo", + title = R.string.duo_title, + iconRes = R.drawable.rounded_motion_play_24, + category = R.string.cat_interface, + description = R.string.duo_desc, + aboutDescription = R.string.duo_desc, + permissionKeys = listOf("ACCESSIBILITY"), + hasMoreSettings = true, + showToggle = true, + isBeta = true, + parentFeatureId = "Display", + ) { + override fun isDeviceSupported(context: Context): Boolean { + return true + } + + override fun isEnabled(viewModel: MainViewModel) = viewModel.isDuoEnabled.value + + override fun isToggleEnabled( + viewModel: MainViewModel, + context: Context, + ) = viewModel.isAccessibilityEnabled.value + + override fun onToggle( + viewModel: MainViewModel, + context: Context, + enabled: Boolean, + ) = viewModel.setDuoEnabled(enabled) + }, object : Feature( id = "Other customizations", title = R.string.feat_other_customizations_title, diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/DuoOverlayHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/DuoOverlayHandler.kt index 4e24546c3..bdc932707 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/DuoOverlayHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/DuoOverlayHandler.kt @@ -165,15 +165,57 @@ class DuoOverlayHandler( } } + private var lastChargingState = false + private val batteryReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == Intent.ACTION_BATTERY_CHANGED) { + val action = intent?.action ?: return + if (action == Intent.ACTION_BATTERY_CHANGED || + action == Intent.ACTION_POWER_CONNECTED || + action == Intent.ACTION_POWER_DISCONNECTED + ) { val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) - if (level >= 0 && scale > 0) { - val batteryPct = (level * 100) / scale - overlayView?.batteryLevel = batteryPct - } + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) + if (level >= 0 && scale > 0) { + val batteryPct = (level * 100) / scale + overlayView?.batteryLevel = batteryPct + } + + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val isChargingStatus = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) + val isPlugged = plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB || + plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS + + val isNowCharging = (isChargingStatus && isPlugged) || action == Intent.ACTION_POWER_CONNECTED + + if (action == Intent.ACTION_POWER_DISCONNECTED || (!isNowCharging && lastChargingState)) { + lastChargingState = false + overlayView?.onPowerDisconnected() + } else if (isNowCharging) { + val voltageMv = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) + val batteryManager = service.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager + val currentNowUa = batteryManager?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) ?: 0 + + val wattage: Float? = if (voltageMv > 0 && currentNowUa != 0 && currentNowUa != Int.MIN_VALUE) { + val currentMa = Math.abs(currentNowUa) / 1000f + val volts = voltageMv / 1000f + (volts * (currentMa / 1000f)) + } else null + + val isFastCharging = (wattage != null && wattage >= 15f) || + plugged == BatteryManager.BATTERY_PLUGGED_AC + + if (!lastChargingState || action == Intent.ACTION_POWER_CONNECTED) { + lastChargingState = true + overlayView?.triggerChargingSurge(isFastCharging, wattage) + } else { + overlayView?.setChargingState(true, isFastCharging, wattage) + } + } } } } @@ -552,7 +594,8 @@ class DuoOverlayHandler( private fun showOrUpdateOverlay() { mainHandler.post { - val wm = windowManager ?: return@post + val wm = windowManager ?: (service.getSystemService(AccessibilityService.WINDOW_SERVICE) as? WindowManager) ?: return@post + windowManager = wm val displayMetrics = DisplayMetrics() @Suppress("DEPRECATION") @@ -566,11 +609,26 @@ class DuoOverlayHandler( var cameraRadiusPx = 18f * density * settingsRepository.getDuoCameraSize() @Suppress("DEPRECATION") - val rotation = wm.defaultDisplay.rotation + val rotation = try { + wm.defaultDisplay.rotation + } catch (_: Exception) { + android.view.Surface.ROTATION_0 + } if (settingsRepository.isDuoAutoDetectEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - @Suppress("DEPRECATION") - val cutout = wm.defaultDisplay.cutout + val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + wm.currentWindowMetrics.windowInsets.displayCutout + } catch (_: Exception) { + null + } ?: run { + @Suppress("DEPRECATION") + try { wm.defaultDisplay.cutout } catch (_: Exception) { null } + } + } else { + @Suppress("DEPRECATION") + try { wm.defaultDisplay.cutout } catch (_: Exception) { null } + } if (cutout != null && cutout.boundingRects.isNotEmpty()) { val targetRect = when (rotation) { android.view.Surface.ROTATION_90 -> { @@ -650,6 +708,7 @@ class DuoOverlayHandler( this.showMedia = settingsRepository.isDuoShowMediaEnabled() this.showProgress = settingsRepository.isDuoShowProgressEnabled() this.showFlashlight = settingsRepository.isDuoShowFlashlightEnabled() + this.showChargingSurge = settingsRepository.isDuoShowChargingSurgeEnabled() } if (!isOverlayAdded) { @@ -661,6 +720,11 @@ class DuoOverlayHandler( WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, isTouchable = false ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } try { wm.addView(overlayView, params) isOverlayAdded = true @@ -822,9 +886,14 @@ class DuoOverlayHandler( private fun registerBatteryReceiver() { if (!isBatteryReceiverRegistered) { try { + val filter = IntentFilter().apply { + addAction(Intent.ACTION_BATTERY_CHANGED) + addAction(Intent.ACTION_POWER_CONNECTED) + addAction(Intent.ACTION_POWER_DISCONNECTED) + } val intent = service.registerReceiver( batteryReceiver, - IntentFilter(Intent.ACTION_BATTERY_CHANGED) + filter ) isBatteryReceiverRegistered = true intent?.let { @@ -833,6 +902,18 @@ class DuoOverlayHandler( if (level >= 0 && scale > 0) { overlayView?.batteryLevel = (level * 100) / scale } + val status = it.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val isChargingStatus = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) + val isPlugged = plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB || + plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS + if (isChargingStatus && isPlugged) { + lastChargingState = true + val isFast = plugged == BatteryManager.BATTERY_PLUGGED_AC + overlayView?.setChargingState(true, isFast, null) + } } } catch (e: Exception) { Log.e("DuoOverlayHandler", "Failed to register battery receiver", e) 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 811476773..14862d683 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 @@ -25,6 +25,8 @@ import android.os.Handler import android.os.Looper import android.os.Vibrator import android.view.KeyEvent +import android.view.Surface +import android.view.WindowManager import android.view.accessibility.AccessibilityEvent import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.domain.HapticFeedbackType @@ -494,6 +496,19 @@ class ScreenOffAccessibilityService : private fun checkFullscreenState() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { try { + val wm = getSystemService(Context.WINDOW_SERVICE) as? WindowManager + @Suppress("DEPRECATION") + val rotation = try { + wm?.defaultDisplay?.rotation ?: Surface.ROTATION_0 + } catch (_: Exception) { + Surface.ROTATION_0 + } + // In portrait orientation, the camera cutout is at the top of the display and Duo must stay visible + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + duoOverlayHandler.setFullscreen(false) + return + } + val currentWindows = windows if (!currentWindows.isNullOrEmpty()) { val hasStatusBar = currentWindows.any { it.type == android.view.accessibility.AccessibilityWindowInfo.TYPE_SYSTEM } @@ -509,9 +524,15 @@ class ScreenOffAccessibilityService : val isFullscreen = isCoveringFullDisplay && !hasStatusBar duoOverlayHandler.setFullscreen(isFullscreen) + } else { + duoOverlayHandler.setFullscreen(false) } + } else { + duoOverlayHandler.setFullscreen(false) } - } catch (_: Exception) {} + } catch (_: Exception) { + duoOverlayHandler.setFullscreen(false) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/DuoSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/DuoSettingsUI.kt index 7405f3001..53695f714 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/DuoSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/DuoSettingsUI.kt @@ -294,6 +294,17 @@ fun DuoSettingsUI( }, modifier = Modifier.highlight(highlightSetting == "duo_show_flashlight"), ) + IconToggleItem( + iconRes = R.drawable.rounded_bolt_24, + title = stringResource(R.string.duo_show_charging_surge_title), + description = stringResource(R.string.duo_show_charging_surge_desc), + isChecked = viewModel.isDuoShowChargingSurge.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setDuoShowChargingSurge(checked) + }, + modifier = Modifier.highlight(highlightSetting == "duo_show_charging_surge"), + ) } Text( diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DuoOverlayView.kt b/app/src/main/java/com/sameerasw/essentials/utils/DuoOverlayView.kt index 803c6253d..b84f3fa29 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/DuoOverlayView.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/DuoOverlayView.kt @@ -9,6 +9,8 @@ package com.sameerasw.essentials.utils +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.animation.ArgbEvaluator import android.animation.ValueAnimator import android.content.Context @@ -22,11 +24,13 @@ import android.graphics.PorterDuffColorFilter import android.graphics.RectF import android.os.Build import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.DecelerateInterpolator import android.view.animation.LinearInterpolator import android.view.animation.OvershootInterpolator import androidx.core.content.ContextCompat import androidx.palette.graphics.Palette +import com.sameerasw.essentials.R import kotlin.math.cos import kotlin.math.sin @@ -75,7 +79,7 @@ class DuoOverlayView(context: Context) : View(context) { set(value) { val clamped = value.coerceIn(0, 100) field = clamped - if (!isCustomProgressActive()) { + if (!isCustomProgressActive() && chargingSurgeAnimator?.isRunning != true) { updateProgressAnimation(clamped.toFloat()) } } @@ -220,6 +224,26 @@ class DuoOverlayView(context: Context) : View(context) { var flashlightBrightnessProgress: Float = 100f private set + var showChargingSurge: Boolean = true + + var isCharging: Boolean = false + private set + + var isFastCharging: Boolean = false + private set + + var chargingWattage: Float? = null + private set + + private var animatedBoltScale: Float = 0f + private var animatedProgressWiggle: Float = 0f + private var chargingGlowAlpha: Float = 0f + + private var boltAnimator: ValueAnimator? = null + private var wiggleAnimator: ValueAnimator? = null + private var chargingSurgeAnimator: ValueAnimator? = null + + private val boltDrawable by lazy { ContextCompat.getDrawable(context, R.drawable.rounded_bolt_24) } private var mediaPaletteColors: Pair? = null private fun extractMediaColors(bitmap: Bitmap): Pair { @@ -398,6 +422,143 @@ class DuoOverlayView(context: Context) : View(context) { } } + fun triggerChargingSurge(isFastCharging: Boolean, wattage: Float?) { + isCharging = true + this.isFastCharging = isFastCharging + this.chargingWattage = wattage + + animateThemeChange() + + if (showChargingSurge) { + startPremiumChargingSurge() + } else { + animatedBoltScale = 1.0f + chargingGlowAlpha = 0f + animatedProgress = getActiveTargetProgress() + animatedProgressWiggle = 0f + animatedScaleBounce = 1.0f + invalidate() + } + } + + fun setChargingState(isCharging: Boolean, isFastCharging: Boolean, wattage: Float?) { + val wasCharging = this.isCharging + val changed = this.isCharging != isCharging || this.isFastCharging != isFastCharging + this.isCharging = isCharging + this.isFastCharging = isFastCharging + this.chargingWattage = wattage + if (changed) { + animateThemeChange() + if (isCharging && !wasCharging && showChargingSurge) { + startPremiumChargingSurge() + } + } + } + + fun onPowerDisconnected() { + isCharging = false + isFastCharging = false + chargingWattage = null + chargingSurgeAnimator?.cancel() + wiggleAnimator?.cancel() + animatedProgressWiggle = 0f + animatedScaleBounce = 1.0f + chargingGlowAlpha = 0f + animatedProgress = getActiveTargetProgress() + + boltAnimator?.cancel() + boltAnimator = ValueAnimator.ofFloat(animatedBoltScale, 0f).apply { + duration = 350 + interpolator = DecelerateInterpolator() + addUpdateListener { animation -> + animatedBoltScale = animation.animatedValue as Float + invalidate() + } + start() + } + + animateThemeChange() + invalidate() + } + + private fun startPremiumChargingSurge() { + chargingSurgeAnimator?.cancel() + wiggleAnimator?.cancel() + boltAnimator?.cancel() + scaleAnimator?.cancel() + + val target = getActiveTargetProgress() + val startProgress = 0f + + chargingSurgeAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = 950 + interpolator = LinearInterpolator() + addUpdateListener { animator -> + val t = animator.animatedValue as Float + + // 1. Bolt Scale: spring pop up to 1.22 in first 550ms, then settling at 1.0f + if (t <= 0.55f) { + val boltNorm = t / 0.55f + val tension = 1.8f + val x = boltNorm - 1f + animatedBoltScale = ((x * x * ((tension + 1) * x + tension) + 1f)).coerceIn(0f, 1.25f) + } else { + animatedBoltScale = 1.0f + } + + // 2. Ring Elastic Scale Breath: 1.0 -> 1.075 -> 0.995 -> 1.0 + animatedScaleBounce = when { + t < 0.35f -> { + val p = t / 0.35f + 1.0f + 0.075f * sin(p * (Math.PI / 2).toFloat()) + } + t < 0.75f -> { + val p = (t - 0.35f) / 0.40f + 1.075f - 0.08f * sin(p * (Math.PI / 2).toFloat()) + } + else -> { + val p = (t - 0.75f) / 0.25f + 0.995f + 0.005f * p + } + } + + // 3. Liquid Progress Sweep & Damped Settle + if (t < 0.65f) { + val p = t / 0.65f + val decel = 1f - (1f - p) * (1f - p) + animatedProgress = (startProgress + (target + 3.5f - startProgress) * decel).coerceIn(0f, 100f) + animatedProgressWiggle = 0f + } else { + val p = (t - 0.65f) / 0.35f + val decay = 1f - p + val oscillation = sin(p * Math.PI.toFloat() * 2.5f) + animatedProgress = target + animatedProgressWiggle = 3.5f * decay * oscillation + } + + // 4. Luminous Electric Glow Flare + chargingGlowAlpha = when { + t < 0.30f -> (t / 0.30f) * 0.65f + t < 0.85f -> (1f - (t - 0.30f) / 0.55f) * 0.65f + else -> 0f + } + + invalidate() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + animatedProgress = target + animatedProgressWiggle = 0f + animatedBoltScale = 1.0f + animatedScaleBounce = 1.0f + chargingGlowAlpha = 0f + invalidate() + } + }) + start() + } + } + private var targetProgress: Float = 100f private var animatedProgress: Float = 100f private var progressAnimator: ValueAnimator? = null @@ -460,6 +621,12 @@ class DuoOverlayView(context: Context) : View(context) { val dimTrack = Color.argb(50, Color.red(accent), Color.green(accent), Color.blue(accent)) return Triple(dimTrack, dimProgress, dimProgress) } + if (isCharging) { + val chargeAccent = if (isFastCharging) Color.rgb(0, 229, 255) else Color.rgb(0, 230, 118) + val dimProgress = Color.argb(160, Color.red(chargeAccent), Color.green(chargeAccent), Color.blue(chargeAccent)) + val dimTrack = Color.argb(50, Color.red(chargeAccent), Color.green(chargeAccent), Color.blue(chargeAccent)) + return Triple(dimTrack, dimProgress, dimProgress) + } return Triple( Color.argb(40, 255, 255, 255), Color.argb(128, 255, 255, 255), @@ -472,6 +639,13 @@ class DuoOverlayView(context: Context) : View(context) { return Triple(track, progress, progress) } + if (isCharging) { + val chargeAccent = if (isFastCharging) Color.rgb(0, 229, 255) else Color.rgb(0, 230, 118) + val trackAlpha = if (isDarkTheme) 90 else 110 + val track = Color.argb(trackAlpha, Color.red(chargeAccent), Color.green(chargeAccent), Color.blue(chargeAccent)) + return Triple(track, chargeAccent, chargeAccent) + } + if (useMaterialYouColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (isDarkTheme) { val accent = ContextCompat.getColor(context, android.R.color.system_accent1_200) @@ -548,6 +722,9 @@ class DuoOverlayView(context: Context) : View(context) { private fun updateProgressAnimation(target: Float = getActiveTargetProgress()) { targetProgress = target + if (chargingSurgeAnimator?.isRunning == true) { + return + } if (kotlin.math.abs(animatedProgress - targetProgress) < 0.05f) { animatedProgress = targetProgress invalidate() @@ -616,6 +793,11 @@ class DuoOverlayView(context: Context) : View(context) { strokeCap = Paint.Cap.ROUND } + private val chargingGlowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeCap = Paint.Cap.ROUND + } + private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } @@ -662,8 +844,21 @@ class DuoOverlayView(context: Context) : View(context) { ) canvas.drawArc(arcBounds, animatedStartAngle, animatedTotalSweep, false, trackPaint) - val progressSweep = (animatedProgress.coerceIn(0f, 100f) / 100f) * animatedTotalSweep + val effectiveProgress = (animatedProgress + animatedProgressWiggle).coerceIn(0f, 100f) + val progressSweep = (effectiveProgress / 100f) * animatedTotalSweep if (progressSweep > 0.5f) { + if (chargingGlowAlpha > 0.01f) { + chargingGlowPaint.strokeWidth = progressPaint.strokeWidth * 1.85f + val glowAlpha = (Color.alpha(currentProgressColor) * chargingGlowAlpha * animatedVisibilityAlpha).toInt().coerceIn(0, 255) + chargingGlowPaint.color = Color.argb( + glowAlpha, + Color.red(currentProgressColor), + Color.green(currentProgressColor), + Color.blue(currentProgressColor) + ) + canvas.drawArc(arcBounds, animatedStartAngle, progressSweep, false, chargingGlowPaint) + } + val progAlpha = (Color.alpha(currentProgressColor) * animatedVisibilityAlpha).toInt() progressPaint.color = Color.argb( progAlpha, @@ -696,6 +891,30 @@ class DuoOverlayView(context: Context) : View(context) { } } + + + // Electric Bolt in Gap + if (animatedBoltScale > 0.01f && animatedCustomFraction < 0.2f) { + val gapCenterAngleDeg = (animatedStartAngle + animatedTotalSweep + (360f - animatedTotalSweep) / 2f) % 360f + val angleRad = Math.toRadians(gapCenterAngleDeg.toDouble()) + val downwardOffset = 4f * resources.displayMetrics.density + val boltCenterX = (cameraCenterX + (baseRadius + downwardOffset) * cos(angleRad)).toFloat() + val boltCenterY = (cameraCenterY + (baseRadius + downwardOffset) * sin(angleRad)).toFloat() + + val boltRadius = (dotRadiusPx * 2.85f) * animatedBoltScale * (1f - animatedCustomFraction) + if (boltRadius > 1f && boltDrawable != null) { + val b = boltDrawable!! + val left = (boltCenterX - boltRadius).toInt() + val top = (boltCenterY - boltRadius).toInt() + val right = (boltCenterX + boltRadius).toInt() + val bottom = (boltCenterY + boltRadius).toInt() + b.setBounds(left, top, right, bottom) + b.setTint(currentProgressColor) + b.alpha = (255 * animatedBoltScale * animatedVisibilityAlpha).toInt() + b.draw(canvas) + } + } + if (animatedCustomFraction > 0.01f) { val icon = getCurrentCustomIcon() if (icon != null) { @@ -741,6 +960,9 @@ class DuoOverlayView(context: Context) : View(context) { scaleAnimator?.cancel() themeAnimator?.cancel() visibilityAnimator?.cancel() + boltAnimator?.cancel() + wiggleAnimator?.cancel() + chargingSurgeAnimator?.cancel() } } 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 9bfd1fdfe..10e4bfee7 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -135,6 +135,7 @@ class MainViewModel : ViewModel() { val isDuoShowMedia = mutableStateOf(true) val isDuoShowProgress = mutableStateOf(true) val isDuoShowFlashlight = mutableStateOf(true) + val isDuoShowChargingSurge = mutableStateOf(true) val isDuoHideWhenScreenOff = mutableStateOf(true) val isDuoHideWhenScreenOffOnlyIdle = mutableStateOf(false) val isDuoUseMaterialYou = mutableStateOf(true) @@ -555,6 +556,9 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_DUO_SHOW_FLASHLIGHT -> isDuoShowFlashlight.value = settingsRepository.isDuoShowFlashlightEnabled() + SettingsRepository.KEY_DUO_SHOW_CHARGING_SURGE -> + isDuoShowChargingSurge.value = settingsRepository.isDuoShowChargingSurgeEnabled() + SettingsRepository.KEY_DUO_HIDE_WHEN_SCREEN_OFF -> isDuoHideWhenScreenOff.value = settingsRepository.isDuoHideWhenScreenOffEnabled() @@ -1798,6 +1802,7 @@ class MainViewModel : ViewModel() { isDuoShowMedia.value = settingsRepository.isDuoShowMediaEnabled() isDuoShowProgress.value = settingsRepository.isDuoShowProgressEnabled() isDuoShowFlashlight.value = settingsRepository.isDuoShowFlashlightEnabled() + isDuoShowChargingSurge.value = settingsRepository.isDuoShowChargingSurgeEnabled() isDuoHideWhenScreenOff.value = settingsRepository.isDuoHideWhenScreenOffEnabled() isDuoHideWhenScreenOffOnlyIdle.value = settingsRepository.isDuoHideWhenScreenOffOnlyIdleEnabled() isDuoUseMaterialYou.value = settingsRepository.isDuoUseMaterialYouEnabled() @@ -3809,7 +3814,7 @@ class MainViewModel : ViewModel() { val sessions = manager.getActiveSessions(componentName) val activeSession = sessions - ?.sortedWith( + .sortedWith( compareByDescending { val state = it.playbackState?.state state == android.media.session.PlaybackState.STATE_PLAYING || @@ -3817,8 +3822,8 @@ class MainViewModel : ViewModel() { }.thenByDescending { val state = it.playbackState?.state state == android.media.session.PlaybackState.STATE_PAUSED - }, - )?.firstOrNull() + } + ).firstOrNull() if (activeSession != null) { val metadata = activeSession.metadata @@ -4417,6 +4422,11 @@ class MainViewModel : ViewModel() { settingsRepository.setDuoShowFlashlightEnabled(enabled) } + fun setDuoShowChargingSurge(enabled: Boolean) { + isDuoShowChargingSurge.value = enabled + settingsRepository.setDuoShowChargingSurgeEnabled(enabled) + } + fun setDuoHideWhenScreenOff(enabled: Boolean) { isDuoHideWhenScreenOff.value = enabled settingsRepository.setDuoHideWhenScreenOffEnabled(enabled) @@ -7119,9 +7129,9 @@ class MainViewModel : ViewModel() { } else { val backupData = gson.fromJson(json, FreezeBackupData::class.java) if (backupData != null) { - importedApps = backupData.apps ?: emptyList() - importedTags = backupData.tags ?: emptyList() - importedMap = backupData.appTagMap ?: emptyMap() + importedApps = backupData.apps + importedTags = backupData.tags + importedMap = backupData.appTagMap } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index befd87c98..c215d9f47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2472,11 +2472,20 @@ Style What to show Networks + Show mobile network status indicator dots Media playback + Display playback seekbar and media player icon around the cutout Progress notifications + Display ongoing task and download progress around the indicator Flashlight + Display flashlight state and brightness level around the indicator + Charging animation + Animate battery progress when connected to power Behavior Use Material You colors + Use accent color from wallpaper Hide when screen off + Hide the overlay on AOD Only hide on AOD while idle + Keep active media, progress notifications, or flashlight visible on AOD