Port OneUI miscellaneous display settings
This commit is contained in:
@@ -68,6 +68,9 @@ in Cards mode, with separate lockscreen and always-on-display controls for row
|
|||||||
count, icons per row, balanced rows, width limits, and icon size. In Icons mode,
|
count, icons per row, balanced rows, width limits, and icon size. In Icons mode,
|
||||||
Android—or StatusBarTweak, when installed—continues to render the icons.
|
Android—or StatusBarTweak, when installed—continues to render the icons.
|
||||||
|
|
||||||
|
The OneUI integration can also keep AOD visible during calls and show the
|
||||||
|
battery percentage on the lockscreen or AOD while the phone is unplugged.
|
||||||
|
|
||||||
For visibility control, also give the module the **System UI** scope. Samsung
|
For visibility control, also give the module the **System UI** scope. Samsung
|
||||||
devices need the **Always On Display** scope for AOD filtering and layout.
|
devices need the **Always On Display** scope for AOD filtering and layout.
|
||||||
Restart Android after changing module scopes.
|
Restart Android after changing module scopes.
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||||||
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
|
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
|
||||||
page == Page.CARDS_LAYOUT -> CardsLayoutFragment()
|
page == Page.CARDS_LAYOUT -> CardsLayoutFragment()
|
||||||
page == Page.DEBUG_NOTIFICATIONS -> DebugNotificationsFragment()
|
page == Page.DEBUG_NOTIFICATIONS -> DebugNotificationsFragment()
|
||||||
|
page == Page.MISCELLANEOUS -> MiscellaneousFragment()
|
||||||
page == Page.LOG_DISPLAY -> LogDisplayFragment()
|
page == Page.LOG_DISPLAY -> LogDisplayFragment()
|
||||||
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
|
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
|
||||||
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
|
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
|
||||||
@@ -377,6 +378,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
|||||||
NOTIFICATION_VISIBILITY(R.id.nav_notification_visibility, R.string.page_notification_visibility),
|
NOTIFICATION_VISIBILITY(R.id.nav_notification_visibility, R.string.page_notification_visibility),
|
||||||
CARDS_LAYOUT(R.id.nav_cards_layout, R.string.page_cards_layout),
|
CARDS_LAYOUT(R.id.nav_cards_layout, R.string.page_cards_layout),
|
||||||
DEBUG_NOTIFICATIONS(R.id.nav_debug_notifications, R.string.page_debug_notifications),
|
DEBUG_NOTIFICATIONS(R.id.nav_debug_notifications, R.string.page_debug_notifications),
|
||||||
|
MISCELLANEOUS(R.id.nav_miscellaneous, R.string.page_miscellaneous),
|
||||||
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
|
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
|
||||||
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
|
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
|
||||||
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
|
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package se.ajpanton.notificationsmaster
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import se.ajpanton.notificationsmaster.databinding.FragmentMiscellaneousBinding
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.SystemUiMiscSettings
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
|
||||||
|
|
||||||
|
class MiscellaneousFragment : Fragment(R.layout.fragment_miscellaneous) {
|
||||||
|
private var binding: FragmentMiscellaneousBinding? = null
|
||||||
|
private lateinit var store: VisibilityPolicyStore
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
binding = FragmentMiscellaneousBinding.bind(view)
|
||||||
|
store = VisibilityPolicyStore(requireContext())
|
||||||
|
val settings = store.load().systemUiMisc
|
||||||
|
bind(binding!!.keepAodVisibleDuringCalls, settings.keepAodVisibleDuringCalls) { current, checked ->
|
||||||
|
current.copy(keepAodVisibleDuringCalls = checked)
|
||||||
|
}
|
||||||
|
bind(binding!!.showLockscreenBatteryPercent, settings.showLockscreenBatteryPercentWhenUnplugged) {
|
||||||
|
current, checked ->
|
||||||
|
current.copy(showLockscreenBatteryPercentWhenUnplugged = checked)
|
||||||
|
}
|
||||||
|
bind(binding!!.showAodBatteryPercent, settings.showAodBatteryPercentWhenUnplugged) { current, checked ->
|
||||||
|
current.copy(showAodBatteryPercentWhenUnplugged = checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
activity?.title = getString(R.string.page_miscellaneous)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind(
|
||||||
|
toggle: SwitchMaterial,
|
||||||
|
initial: Boolean,
|
||||||
|
update: (SystemUiMiscSettings, Boolean) -> SystemUiMiscSettings,
|
||||||
|
) {
|
||||||
|
toggle.isChecked = initial
|
||||||
|
toggle.setOnCheckedChangeListener { _, checked ->
|
||||||
|
store.update { policy -> policy.copy(systemUiMisc = update(policy.systemUiMisc, checked)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ class NotificationsMasterModule : XposedModule() {
|
|||||||
OneUiLockscreenBackend(this),
|
OneUiLockscreenBackend(this),
|
||||||
OneUiLockscreenCardsBackend(this),
|
OneUiLockscreenCardsBackend(this),
|
||||||
SamsungAodPluginBackend(this),
|
SamsungAodPluginBackend(this),
|
||||||
|
OneUiMiscBackend(this),
|
||||||
).filter { it.install(param.classLoader) }
|
).filter { it.install(param.classLoader) }
|
||||||
ProcessVisibilityPolicyCache.installWhenReady {
|
ProcessVisibilityPolicyCache.installWhenReady {
|
||||||
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.telecom.TelecomManager
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** OneUI information-display options ported from StatusBarTweak's Misc page. */
|
||||||
|
internal class OneUiMiscBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
|
private val callVisibility = AodCallVisibilityController(framework)
|
||||||
|
private val lockscreenBattery = LockscreenBatteryController(framework)
|
||||||
|
private val aodBattery = AodBatteryController(framework)
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader): Boolean {
|
||||||
|
if (!Build.MANUFACTURER.equals("samsung", ignoreCase = true)) return false
|
||||||
|
val installed = listOf(
|
||||||
|
callVisibility.install(classLoader),
|
||||||
|
lockscreenBattery.install(classLoader),
|
||||||
|
aodBattery.install(classLoader),
|
||||||
|
).count { it }
|
||||||
|
if (installed > 0) Log.i(TAG, "Installed $installed OneUI miscellaneous controllers")
|
||||||
|
return installed > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPolicyChanged() = aodBattery.refresh()
|
||||||
|
|
||||||
|
private companion object { const val TAG = "NotificationsMaster" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AodCallVisibilityController(private val framework: XposedInterface) {
|
||||||
|
fun install(classLoader: ClassLoader): Boolean {
|
||||||
|
var hooks = 0
|
||||||
|
listOf(
|
||||||
|
"com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks" to "suppressAmbientDisplay",
|
||||||
|
"com.android.systemui.statusbar.CommandQueue" to "suppressAmbientDisplay",
|
||||||
|
"com.android.systemui.statusbar.phone.SecLsScrimControlHelper" to "setFrontScrimToBlack",
|
||||||
|
"com.android.systemui.doze.DozeSuppressor\$1" to "onAlwaysOnSuppressedChanged",
|
||||||
|
"com.android.systemui.doze.AODUi\$1" to "onAlwaysOnSuppressedChanged",
|
||||||
|
).forEach { (className, methodName) ->
|
||||||
|
classLoader.findClass(className)?.methodsNamed(methodName)?.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val value = chain.args.firstOrNull() as? Boolean
|
||||||
|
if (value == true && shouldForceAodForCall()) {
|
||||||
|
chain.proceed(chain.args.toTypedArray().apply { this[0] = false })
|
||||||
|
} else chain.proceed()
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classLoader.findClass("com.android.systemui.doze.DozeMachine")?.let { type ->
|
||||||
|
listOf("requestState", "transitionTo").forEach { name ->
|
||||||
|
type.methodsNamed(name).forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val requested = chain.args.firstOrNull() as? Enum<*>
|
||||||
|
val replacement = requested?.takeIf { shouldForceAodForCall() }?.aodReplacement()
|
||||||
|
if (replacement == null) chain.proceed() else {
|
||||||
|
chain.proceed(chain.args.toTypedArray().apply { this[0] = replacement })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classLoader.findClass("com.android.systemui.plugins.aod.PluginAODSystemUIConfiguration")
|
||||||
|
?.methodsNamed("get")?.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
if (shouldForceAodForCall() && chain.args.firstOrNull() == PLUGIN_KEY_PHONE_STATE &&
|
||||||
|
(result as? Int ?: chain.args.getOrNull(1) as? Int) != TelephonyManager.CALL_STATE_IDLE
|
||||||
|
) TelephonyManager.CALL_STATE_IDLE else result
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
return hooks > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Enum<*>.aodReplacement(): Enum<*>? {
|
||||||
|
if (name != "DOZE" && name != "FINISH") return null
|
||||||
|
return runCatching {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
java.lang.Enum.valueOf(declaringJavaClass as Class<out Enum<*>>, "DOZE_AOD")
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldForceAodForCall(): Boolean {
|
||||||
|
if (!ProcessVisibilityPolicyCache.systemUiMisc.keepAodVisibleDuringCalls) return false
|
||||||
|
return currentApplicationContext()?.let(::isInCallLikeState) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun isInCallLikeState(context: Context): Boolean {
|
||||||
|
if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (runCatching { context.getSystemService(TelecomManager::class.java)?.isInCall == true }.getOrDefault(false)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (runCatching {
|
||||||
|
context.getSystemService(TelephonyManager::class.java)?.callState !=
|
||||||
|
TelephonyManager.CALL_STATE_IDLE
|
||||||
|
}.getOrDefault(false)
|
||||||
|
) return true
|
||||||
|
}
|
||||||
|
return runCatching {
|
||||||
|
context.getSystemService(AudioManager::class.java)?.mode in
|
||||||
|
setOf(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object { const val PLUGIN_KEY_PHONE_STATE = 0x2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LockscreenBatteryController(private val framework: XposedInterface) {
|
||||||
|
private var failed = false
|
||||||
|
|
||||||
|
fun install(classLoader: ClassLoader): Boolean {
|
||||||
|
val eventClass = classLoader.findClass("com.android.systemui.statusbar.IndicationEventType") ?: return false
|
||||||
|
val batteryEvent = eventClass.staticField("BATTERY") ?: return false
|
||||||
|
val restingEvent = eventClass.staticField("BATTERY_RESTING") ?: return false
|
||||||
|
val controller = classLoader.findClass(
|
||||||
|
"com.android.systemui.statusbar.KeyguardSecIndicationController",
|
||||||
|
) ?: return false
|
||||||
|
val methods = controller.methodsNamed("addInitialIndication")
|
||||||
|
methods.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
runCatching {
|
||||||
|
val target = chain.thisObject
|
||||||
|
val context = target.field("mContext") as? Context ?: return@runCatching
|
||||||
|
val state = visibleLockscreenBattery(context, target) ?: return@runCatching
|
||||||
|
val text = state.formattedPercent()
|
||||||
|
val color = target.field("mInitialTextColorState") as? ColorStateList
|
||||||
|
target.invokeNamed("addIndicationTimeout", batteryEvent, text, color, false)
|
||||||
|
target.invokeNamed("addIndication", restingEvent, text)
|
||||||
|
}.onFailure(::logFailureOnce)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return methods.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visibleLockscreenBattery(context: Context, controller: Any): BatteryState? {
|
||||||
|
if (!ProcessVisibilityPolicyCache.systemUiMisc.showLockscreenBatteryPercentWhenUnplugged) return null
|
||||||
|
if (controller.field("mDozing") == true) return null
|
||||||
|
return currentBatteryState(context)?.takeUnless { it.plugged || it.charged }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logFailureOnce(error: Throwable) {
|
||||||
|
if (failed) return
|
||||||
|
failed = true
|
||||||
|
Log.w(TAG, "Could not update the OneUI lockscreen battery indication", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AodBatteryController(private val framework: XposedInterface) {
|
||||||
|
private var manager: Any? = null
|
||||||
|
private var host: FrameLayout? = null
|
||||||
|
private val pendingRetries = mutableSetOf<Runnable>()
|
||||||
|
private val savedText = WeakHashMap<TextView, Pair<CharSequence, Int>>()
|
||||||
|
private val savedVisibility = WeakHashMap<View, Int>()
|
||||||
|
private var failed = false
|
||||||
|
|
||||||
|
fun install(classLoader: ClassLoader): Boolean {
|
||||||
|
var hooks = 0
|
||||||
|
classLoader.findClass("com.android.systemui.doze.PluginAODManager\$6")
|
||||||
|
?.methodsNamed("setBottomArea")?.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
manager = chain.thisObject.field("this\$0")
|
||||||
|
host = resolveBottomDozeArea(manager)
|
||||||
|
update(host, chain.args.firstOrNull { it is View } as? View ?: result as? View)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
classLoader.findClass("com.android.systemui.doze.PluginAODManager")
|
||||||
|
?.methodsNamed("setIsDozing")?.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
manager = chain.thisObject
|
||||||
|
host = resolveBottomDozeArea(manager)
|
||||||
|
val target = host?.getChildAt(0)
|
||||||
|
if (chain.args.firstOrNull() == true) update(host, target) else clear(host, target)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
classLoader.findClass(
|
||||||
|
"com.android.systemui.statusbar.KeyguardSecIndicationController\$SecKeyguardCallback",
|
||||||
|
)?.methodsNamed("onRefreshBatteryInfo")?.forEach { method ->
|
||||||
|
framework.intercept(method) { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
update(host, host?.getChildAt(0))
|
||||||
|
result
|
||||||
|
}
|
||||||
|
hooks++
|
||||||
|
}
|
||||||
|
return hooks > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
host?.post { update(host, host?.getChildAt(0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(host: FrameLayout?, source: View?) {
|
||||||
|
runCatching {
|
||||||
|
val frame = host ?: return
|
||||||
|
val state = currentBatteryState(frame.context)
|
||||||
|
if (!ProcessVisibilityPolicyCache.systemUiMisc.showAodBatteryPercentWhenUnplugged ||
|
||||||
|
state == null || state.plugged || state.charged
|
||||||
|
) return clear(frame, source)
|
||||||
|
val text = state.formattedPercent().toString()
|
||||||
|
val component = pluginRoot(source, frame).field("p") ?: return clear(frame, source)
|
||||||
|
val componentView = component.field("c") as? View ?: return clear(frame, source)
|
||||||
|
val indication = componentView.field("d") as? TextView ?: return clear(frame, source)
|
||||||
|
cancelRetries(frame)
|
||||||
|
apply(componentView, indication, text)
|
||||||
|
RETRY_DELAYS.forEach { delay ->
|
||||||
|
val retry = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
pendingRetries -= this
|
||||||
|
if (this@AodBatteryController.host !== frame) return
|
||||||
|
val current = currentBatteryState(frame.context)
|
||||||
|
if (!ProcessVisibilityPolicyCache.systemUiMisc.showAodBatteryPercentWhenUnplugged ||
|
||||||
|
current == null || current.plugged || current.charged
|
||||||
|
) {
|
||||||
|
clear(frame, source)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val root = pluginRoot(source, frame).field("p").field("c") as? View ?: return
|
||||||
|
val textView = root.field("d") as? TextView ?: return
|
||||||
|
apply(root, textView, current.formattedPercent().toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingRetries += retry
|
||||||
|
frame.postDelayed(retry, delay)
|
||||||
|
}
|
||||||
|
}.onFailure(::logFailureOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apply(componentView: View, indication: TextView, text: String) {
|
||||||
|
savedText.putIfAbsent(indication, indication.text to indication.visibility)
|
||||||
|
indication.text = text
|
||||||
|
indication.visibility = View.VISIBLE
|
||||||
|
hide(componentView.field("k") as? View)
|
||||||
|
hide(componentView.field("c") as? View)
|
||||||
|
hide(componentView.field("l") as? View)
|
||||||
|
forceVisible(indication)
|
||||||
|
forceVisible(componentView.field("b") as? View)
|
||||||
|
forceVisible(componentView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clear(host: FrameLayout?, source: View?) {
|
||||||
|
cancelRetries(host)
|
||||||
|
val componentView = pluginRoot(source, host).field("p").field("c") as? View
|
||||||
|
(componentView?.field("d") as? TextView)?.let { view ->
|
||||||
|
savedText.remove(view)?.let { (text, visibility) ->
|
||||||
|
view.text = text
|
||||||
|
view.visibility = visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listOf("k", "c", "l").forEach { name ->
|
||||||
|
(componentView?.field(name) as? View)?.let { restoreVisibility(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hide(view: View?) {
|
||||||
|
view ?: return
|
||||||
|
rememberVisibility(view)
|
||||||
|
view.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rememberVisibility(view: View) {
|
||||||
|
savedVisibility.putIfAbsent(view, view.visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreVisibility(view: View) {
|
||||||
|
savedVisibility.remove(view)?.let { view.visibility = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceVisible(start: View?) {
|
||||||
|
var view = start
|
||||||
|
while (view != null) {
|
||||||
|
view.visibility = View.VISIBLE
|
||||||
|
view.alpha = 1f
|
||||||
|
view.translationX = 0f
|
||||||
|
view.translationY = 0f
|
||||||
|
view.scaleX = 1f
|
||||||
|
view.scaleY = 1f
|
||||||
|
view.requestLayout()
|
||||||
|
view.invalidate()
|
||||||
|
view = view.parent as? View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pluginRoot(source: View?, host: FrameLayout?): Any? = when {
|
||||||
|
source is ViewGroup -> source
|
||||||
|
host?.childCount ?: 0 > 0 -> host?.getChildAt(0)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveBottomDozeArea(manager: Any?): FrameLayout? = runCatching {
|
||||||
|
val panel = manager.field("mPanelViewControllerLazy").invokeNamed("get")
|
||||||
|
val controller = panel.field("mKeyguardSecBottomAreaViewController")
|
||||||
|
val bottomView = controller.invokeNamed("getView")
|
||||||
|
bottomView.field("bottomDozeArea\$delegate").invokeNamed("getValue") as? FrameLayout
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun cancelRetries(host: FrameLayout?) {
|
||||||
|
pendingRetries.forEach { host?.removeCallbacks(it) }
|
||||||
|
pendingRetries.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logFailureOnce(error: Throwable) {
|
||||||
|
if (failed) return
|
||||||
|
failed = true
|
||||||
|
Log.w(TAG, "Could not update the OneUI AOD battery indication", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object { val RETRY_DELAYS = longArrayOf(96L, 224L, 480L) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class BatteryState(val level: Int, val plugged: Boolean, val charged: Boolean) {
|
||||||
|
fun formattedPercent(): CharSequence = NumberFormat.getPercentInstance().format(level / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun currentBatteryState(context: Context): BatteryState? {
|
||||||
|
val app = context.applicationContext
|
||||||
|
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||||
|
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
app.registerReceiver(null, filter, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
app.registerReceiver(null, filter)
|
||||||
|
} ?: return null
|
||||||
|
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||||
|
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)
|
||||||
|
if (level < 0 || scale <= 0) return null
|
||||||
|
val percent = Math.round((level * 100f) / scale).coerceIn(0, 100)
|
||||||
|
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||||
|
val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0 ||
|
||||||
|
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
return BatteryState(percent, plugged, status == BatteryManager.BATTERY_STATUS_FULL || percent >= 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentApplicationContext(): Context? = runCatching {
|
||||||
|
Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null) as? Context
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun ClassLoader.findClass(name: String) = runCatching { Class.forName(name, false, this) }.getOrNull()
|
||||||
|
|
||||||
|
private fun Class<*>.methodsNamed(name: String): List<Method> = declaredMethods.filter { it.name == name }.onEach {
|
||||||
|
it.isAccessible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Class<*>.staticField(name: String): Any? = findField(name)?.get(null)
|
||||||
|
|
||||||
|
private fun Any?.field(name: String): Any? = this?.javaClass?.findField(name)?.get(this)
|
||||||
|
|
||||||
|
private fun Class<*>.findField(name: String): Field? {
|
||||||
|
var type: Class<*>? = this
|
||||||
|
while (type != null) {
|
||||||
|
runCatching { type.getDeclaredField(name) }.getOrNull()?.let {
|
||||||
|
it.isAccessible = true
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
type = type.superclass
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Any?.invokeNamed(name: String, vararg args: Any?): Any? {
|
||||||
|
val target = this ?: return null
|
||||||
|
var type: Class<*>? = target.javaClass
|
||||||
|
while (type != null) {
|
||||||
|
type.declaredMethods.firstOrNull { method ->
|
||||||
|
method.name == name && method.parameterCount == args.size &&
|
||||||
|
method.parameterTypes.indices.all { method.parameterTypes[it].accepts(args[it]) }
|
||||||
|
}?.let { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
return method.invoke(target, *args)
|
||||||
|
}
|
||||||
|
type = type.superclass
|
||||||
|
}
|
||||||
|
error("${target.javaClass.name}.$name(${args.size}) is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Class<*>.accepts(value: Any?): Boolean {
|
||||||
|
if (value == null) return !isPrimitive
|
||||||
|
val boxed = when (this) {
|
||||||
|
Boolean::class.javaPrimitiveType -> Boolean::class.java
|
||||||
|
Int::class.javaPrimitiveType -> Int::class.java
|
||||||
|
Long::class.javaPrimitiveType -> Long::class.java
|
||||||
|
Float::class.javaPrimitiveType -> Float::class.java
|
||||||
|
Double::class.javaPrimitiveType -> Double::class.java
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
return boxed.isInstance(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun XposedInterface.intercept(method: Method, callback: (XposedInterface.Chain) -> Any?) {
|
||||||
|
hook(method).intercept(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "NotificationsMaster"
|
||||||
+1
@@ -35,6 +35,7 @@ internal object ProcessVisibilityPolicyCache {
|
|||||||
val unlockedIconLimit get() = policy.unlockedIconLimit
|
val unlockedIconLimit get() = policy.unlockedIconLimit
|
||||||
val lockscreenCards get() = policy.lockscreenCards
|
val lockscreenCards get() = policy.lockscreenCards
|
||||||
val aodCards get() = policy.aodCards
|
val aodCards get() = policy.aodCards
|
||||||
|
val systemUiMisc get() = policy.systemUiMisc
|
||||||
|
|
||||||
private fun tryInstall() {
|
private fun tryInstall() {
|
||||||
if (installed) return
|
if (installed) return
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ data class AppVisibilityPolicy(
|
|||||||
init { require(packageName.isNotBlank()) }
|
init { require(packageName.isNotBlank()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SystemUiMiscSettings(
|
||||||
|
val keepAodVisibleDuringCalls: Boolean = false,
|
||||||
|
val showLockscreenBatteryPercentWhenUnplugged: Boolean = true,
|
||||||
|
val showAodBatteryPercentWhenUnplugged: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
data class VisibilityPolicy(
|
data class VisibilityPolicy(
|
||||||
val generation: Long = 0,
|
val generation: Long = 0,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
@@ -47,6 +53,7 @@ data class VisibilityPolicy(
|
|||||||
val unlockedIconLimit: Int? = null,
|
val unlockedIconLimit: Int? = null,
|
||||||
val lockscreenCards: CardsGridSettings = CardsGridSettings.LOCKSCREEN_DEFAULT,
|
val lockscreenCards: CardsGridSettings = CardsGridSettings.LOCKSCREEN_DEFAULT,
|
||||||
val aodCards: CardsGridSettings = CardsGridSettings.AOD_DEFAULT,
|
val aodCards: CardsGridSettings = CardsGridSettings.AOD_DEFAULT,
|
||||||
|
val systemUiMisc: SystemUiMiscSettings = SystemUiMiscSettings(),
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(generation >= 0)
|
require(generation >= 0)
|
||||||
@@ -74,6 +81,7 @@ class CompiledVisibilityPolicy(policy: VisibilityPolicy) {
|
|||||||
val unlockedIconLimit = policy.unlockedIconLimit
|
val unlockedIconLimit = policy.unlockedIconLimit
|
||||||
val lockscreenCards = policy.lockscreenCards
|
val lockscreenCards = policy.lockscreenCards
|
||||||
val aodCards = policy.aodCards
|
val aodCards = policy.aodCards
|
||||||
|
val systemUiMisc = policy.systemUiMisc
|
||||||
private val enabled = policy.enabled
|
private val enabled = policy.enabled
|
||||||
private val apps = policy.apps.associate { app ->
|
private val apps = policy.apps.associate { app ->
|
||||||
app.packageName to CompiledAppPolicy(
|
app.packageName to CompiledAppPolicy(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ internal object VisibilityPolicyJson {
|
|||||||
.put("unlockedIconLimit", policy.unlockedIconLimit ?: JSONObject.NULL)
|
.put("unlockedIconLimit", policy.unlockedIconLimit ?: JSONObject.NULL)
|
||||||
.put("lockscreenCards", policy.lockscreenCards.toJson())
|
.put("lockscreenCards", policy.lockscreenCards.toJson())
|
||||||
.put("aodCards", policy.aodCards.toJson())
|
.put("aodCards", policy.aodCards.toJson())
|
||||||
|
.put("systemUiMisc", policy.systemUiMisc.toJson())
|
||||||
.put("apps", JSONArray().apply { policy.apps.forEach { put(it.toJson()) } })
|
.put("apps", JSONArray().apply { policy.apps.forEach { put(it.toJson()) } })
|
||||||
.toString().encodeToByteArray()
|
.toString().encodeToByteArray()
|
||||||
|
|
||||||
@@ -28,9 +29,22 @@ internal object VisibilityPolicyJson {
|
|||||||
?: CardsGridSettings.LOCKSCREEN_DEFAULT,
|
?: CardsGridSettings.LOCKSCREEN_DEFAULT,
|
||||||
aodCards = root.optJSONObject("aodCards")?.toCardsGridSettings()
|
aodCards = root.optJSONObject("aodCards")?.toCardsGridSettings()
|
||||||
?: CardsGridSettings.AOD_DEFAULT,
|
?: CardsGridSettings.AOD_DEFAULT,
|
||||||
|
systemUiMisc = root.optJSONObject("systemUiMisc")?.toSystemUiMiscSettings()
|
||||||
|
?: SystemUiMiscSettings(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SystemUiMiscSettings.toJson() = JSONObject()
|
||||||
|
.put("keepAodVisibleDuringCalls", keepAodVisibleDuringCalls)
|
||||||
|
.put("showLockscreenBatteryPercentWhenUnplugged", showLockscreenBatteryPercentWhenUnplugged)
|
||||||
|
.put("showAodBatteryPercentWhenUnplugged", showAodBatteryPercentWhenUnplugged)
|
||||||
|
|
||||||
|
private fun JSONObject.toSystemUiMiscSettings() = SystemUiMiscSettings(
|
||||||
|
keepAodVisibleDuringCalls = getBoolean("keepAodVisibleDuringCalls"),
|
||||||
|
showLockscreenBatteryPercentWhenUnplugged = getBoolean("showLockscreenBatteryPercentWhenUnplugged"),
|
||||||
|
showAodBatteryPercentWhenUnplugged = getBoolean("showAodBatteryPercentWhenUnplugged"),
|
||||||
|
)
|
||||||
|
|
||||||
private fun CardsGridSettings.toJson() = JSONObject()
|
private fun CardsGridSettings.toJson() = JSONObject()
|
||||||
.put("maxRows", maxRows)
|
.put("maxRows", maxRows)
|
||||||
.put("maxIconsPerRow", maxIconsPerRow)
|
.put("maxIconsPerRow", maxIconsPerRow)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/page_padding">
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/keep_aod_visible_during_calls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/keep_aod_visible_during_calls" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/keep_aod_visible_during_calls_hint"
|
||||||
|
android:textAppearance="?attr/textAppearanceBody2" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/show_lockscreen_battery_percent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/show_lockscreen_battery_percent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/show_lockscreen_battery_percent_hint"
|
||||||
|
android:textAppearance="?attr/textAppearanceBody2" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/show_aod_battery_percent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/show_aod_battery_percent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/show_aod_battery_percent_hint"
|
||||||
|
android:textAppearance="?attr/textAppearanceBody2" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/nav_debug_notifications"
|
android:id="@+id/nav_debug_notifications"
|
||||||
android:title="@string/navigation_indented_debug_notifications" />
|
android:title="@string/navigation_indented_debug_notifications" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_miscellaneous"
|
||||||
|
android:title="@string/navigation_indented_miscellaneous" />
|
||||||
</group>
|
</group>
|
||||||
<group android:checkableBehavior="single">
|
<group android:checkableBehavior="single">
|
||||||
<item
|
<item
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<string name="page_notification_visibility">Notifications</string>
|
<string name="page_notification_visibility">Notifications</string>
|
||||||
<string name="page_debug_notifications">Debug icons</string>
|
<string name="page_debug_notifications">Debug icons</string>
|
||||||
<string name="page_cards_layout">Cards layout</string>
|
<string name="page_cards_layout">Cards layout</string>
|
||||||
|
<string name="page_miscellaneous">Miscellaneous</string>
|
||||||
<string name="page_filter_apps">Filter apps</string>
|
<string name="page_filter_apps">Filter apps</string>
|
||||||
<string name="page_log_display">Log display</string>
|
<string name="page_log_display">Log display</string>
|
||||||
<string name="page_filter_logging">Filter logging</string>
|
<string name="page_filter_logging">Filter logging</string>
|
||||||
@@ -31,6 +32,13 @@
|
|||||||
<string name="navigation_indented_apps">    Apps</string>
|
<string name="navigation_indented_apps">    Apps</string>
|
||||||
<string name="navigation_indented_cards_layout">    Cards layout</string>
|
<string name="navigation_indented_cards_layout">    Cards layout</string>
|
||||||
<string name="navigation_indented_debug_notifications">    Debug icons</string>
|
<string name="navigation_indented_debug_notifications">    Debug icons</string>
|
||||||
|
<string name="navigation_indented_miscellaneous">    Miscellaneous</string>
|
||||||
<string name="navigation_indented_filter_logging">    Filter logging</string>
|
<string name="navigation_indented_filter_logging">    Filter logging</string>
|
||||||
<string name="navigation_indented_filter_apps">    Filter apps</string>
|
<string name="navigation_indented_filter_apps">    Filter apps</string>
|
||||||
|
<string name="keep_aod_visible_during_calls">Keep AOD visible during calls</string>
|
||||||
|
<string name="keep_aod_visible_during_calls_hint">Prevents OneUI from hiding the always-on display while a call is active.</string>
|
||||||
|
<string name="show_lockscreen_battery_percent">Show battery % on lockscreen when unplugged</string>
|
||||||
|
<string name="show_lockscreen_battery_percent_hint">Shows the battery percentage even when the phone is not charging.</string>
|
||||||
|
<string name="show_aod_battery_percent">Show battery % on AOD when unplugged</string>
|
||||||
|
<string name="show_aod_battery_percent_hint">Shows the battery percentage in the bottom AOD area even when the phone is not charging.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ class VisibilityPolicyJsonTest {
|
|||||||
unlockedIconLimit = 7,
|
unlockedIconLimit = 7,
|
||||||
lockscreenCards = CardsGridSettings(2, 7, false, false, 72),
|
lockscreenCards = CardsGridSettings(2, 7, false, false, 72),
|
||||||
aodCards = CardsGridSettings(3, 6, true, true, 48),
|
aodCards = CardsGridSettings(3, 6, true, true, 48),
|
||||||
|
systemUiMisc = SystemUiMiscSettings(true, false, true),
|
||||||
apps = listOf(AppVisibilityPolicy(
|
apps = listOf(AppVisibilityPolicy(
|
||||||
packageName = "example.app",
|
packageName = "example.app",
|
||||||
blockedSurfaces = setOf(NotificationSurface.AOD),
|
blockedSurfaces = setOf(NotificationSurface.AOD),
|
||||||
@@ -37,6 +38,7 @@ class VisibilityPolicyJsonTest {
|
|||||||
|
|
||||||
assertEquals(CardsGridSettings.LOCKSCREEN_DEFAULT, decoded.lockscreenCards)
|
assertEquals(CardsGridSettings.LOCKSCREEN_DEFAULT, decoded.lockscreenCards)
|
||||||
assertEquals(CardsGridSettings.AOD_DEFAULT, decoded.aodCards)
|
assertEquals(CardsGridSettings.AOD_DEFAULT, decoded.aodCards)
|
||||||
|
assertEquals(SystemUiMiscSettings(), decoded.systemUiMisc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException::class)
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
|||||||
Reference in New Issue
Block a user