From 73aaf3963883a4781c714611fbd2320068488410 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:09:21 +0800 Subject: [PATCH 0001/1315] Adding dynamic font feature optimized overriding of system fonts without reflection usage and wider font override support Change-Id: I4940b525584a3ad7c83193ff2484e6726c5437f9 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 6 + core/java/android/widget/TextView.java | 15 + .../internal/util/android/FontController.java | 259 ++++++++++++++++++ graphics/java/android/graphics/Typeface.java | 51 +++- 4 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 core/java/com/android/internal/util/android/FontController.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 6777dbaad9b9..204a04b0e661 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -253,6 +253,7 @@ import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.internal.policy.DecorView; import com.android.internal.protolog.ProtoLog; +import com.android.internal.util.android.FontController; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.Preconditions; @@ -7181,6 +7182,8 @@ public void handleConfigurationChanged(Configuration config, int deviceId) { mConfigurationController.handleConfigurationChanged(config); updateDeviceIdForNonUIContexts(deviceId); + FontController.OnConfigurationChanged(getApplication().getResources()); + // These are only done to maintain @UnsupportedAppUsage and should be removed someday. mCurDefaultDisplayDpi = mConfigurationController.getCurDefaultDisplayDpi(); mConfiguration = mConfigurationController.getConfiguration(); @@ -7935,6 +7938,9 @@ private void handleBindApplication(AppBindData data) { data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null /* baseLoader */, false /* securityViolation */, true /* includeCode */, false /* registerPackage */, isSdkSandbox); + + FontController.OnConfigurationChanged(data.info.getResources()); + if (isSdkSandbox) { data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid, data.sdkSandboxClientAppPackage); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0589afd1185e..695d5e2835a3 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -873,6 +873,7 @@ private void applyErrorDrawableIfNeeded(int layoutDirection) { // more bold. private int mFontWeightAdjustment; private Typeface mOriginalTypeface; + private String mFontFamily; // True if setKeyListener() has been explicitly called private boolean mListenerChanged = false; @@ -4388,6 +4389,7 @@ private void readTextAppearance(Context context, TypedArray appearance, attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex); if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } break; case com.android.internal.R.styleable.TextAppearance_fontFamily: @@ -4400,6 +4402,7 @@ private void readTextAppearance(Context context, TypedArray appearance, } if (attributes.mFontTypeface == null) { attributes.mFontFamily = appearance.getString(attr); + mFontFamily = attributes.mFontFamily; } attributes.mFontFamilyExplicit = true; break; @@ -4495,6 +4498,7 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight); @@ -4528,6 +4532,10 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { setFontVariationSettings(attributes.mFontVariationSettings); } + if (Typeface.getFontName().equals("inter")) { + setFontFeatureSettings("'ss01'"); + } + if (attributes.mHasLineBreakStyle || attributes.mHasLineBreakWordStyle) { updateLineBreakConfigFromTextAppearance(attributes.mHasLineBreakStyle, attributes.mHasLineBreakWordStyle, attributes.mLineBreakStyle, @@ -4669,6 +4677,13 @@ protected void onConfigurationChanged(Configuration newConfig) { invalidate(); } } + + if (!TextUtils.equals(mFontFamily, Typeface.getFontName())) { + Typeface tf = Typeface.getOverrideTypeface(mFontFamily); + setTypeface(tf); + mFontFamily = Typeface.getFontName(); + } + if (mFontWeightAdjustment != newConfig.fontWeightAdjustment) { mFontWeightAdjustment = newConfig.fontWeightAdjustment; setTypeface(getTypeface()); diff --git a/core/java/com/android/internal/util/android/FontController.java b/core/java/com/android/internal/util/android/FontController.java new file mode 100644 index 000000000000..a684b6daeb12 --- /dev/null +++ b/core/java/com/android/internal/util/android/FontController.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2025 AxionOS + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util.android; + +import android.app.ActivityThread; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Log; +import android.text.TextUtils; +import com.android.internal.R; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.Set; + +public class FontController { + + private static final String TAG = "FontController"; + + private static FontController sInstance = null; + + private volatile Resources sResources = null; + + private volatile String sFontFamily = "sans-serif"; + + private static final Set OVERRIDE_FONTS = new HashSet<>(Arrays.asList( + "google", "sans-serif", "gsf-" + )); + + private static final Set SYS_OVERRIDE_FONTS = new HashSet<>(Arrays.asList( + "serif", "monospace", "variable" + )); + + private static final Set EXCLUDED_APPS = new HashSet<>(Arrays.asList( + "it.subito", + "tv.arte.plus7", + "com.google.android.gm" + )); + + private static final Set SYS_OVERRIDE_APPS = new HashSet<>(Arrays.asList( + "com.android.settings", + "com.android.systemui", + "com.android.launcher3", + "android" + )); + + private static final Map WEIGHT_MAP = new ArrayMap<>(); + static { + WEIGHT_MAP.put("thin", 100); + WEIGHT_MAP.put("extralight", 200); + WEIGHT_MAP.put("light", 300); + WEIGHT_MAP.put("normal", 400); + WEIGHT_MAP.put("regular", 400); + WEIGHT_MAP.put("medium", 500); + WEIGHT_MAP.put("semibold", 600); + WEIGHT_MAP.put("bold", 700); + WEIGHT_MAP.put("extrabold", 800); + WEIGHT_MAP.put("black", 900); + + WEIGHT_MAP.put("variable-display-large-emphasized", 500); + WEIGHT_MAP.put("variable-display-medium-emphasized", 500); + WEIGHT_MAP.put("variable-display-small-emphasized", 500); + WEIGHT_MAP.put("variable-headline-large-emphasized", 500); + WEIGHT_MAP.put("variable-headline-medium-emphasized", 500); + WEIGHT_MAP.put("variable-headline-small-emphasized", 500); + WEIGHT_MAP.put("variable-title-large-emphasized", 500); + WEIGHT_MAP.put("variable-title-medium-emphasized", 600); + WEIGHT_MAP.put("variable-title-small-emphasized", 600); + WEIGHT_MAP.put("variable-label-large-emphasized", 600); + WEIGHT_MAP.put("variable-label-medium-emphasized", 600); + WEIGHT_MAP.put("variable-label-small-emphasized", 600); + WEIGHT_MAP.put("variable-body-large-emphasized", 500); + WEIGHT_MAP.put("variable-body-medium-emphasized", 500); + WEIGHT_MAP.put("variable-body-small-emphasized", 500); + + WEIGHT_MAP.put("variable-display-large", 400); + WEIGHT_MAP.put("variable-display-medium", 400); + WEIGHT_MAP.put("variable-display-small", 400); + WEIGHT_MAP.put("variable-headline-large", 400); + WEIGHT_MAP.put("variable-headline-medium", 400); + WEIGHT_MAP.put("variable-headline-small", 400); + WEIGHT_MAP.put("variable-title-large", 400); + WEIGHT_MAP.put("variable-title-medium", 500); + WEIGHT_MAP.put("variable-title-small", 500); + WEIGHT_MAP.put("variable-label-large", 500); + WEIGHT_MAP.put("variable-label-medium", 500); + WEIGHT_MAP.put("variable-label-small", 500); + WEIGHT_MAP.put("variable-body-large", 400); + WEIGHT_MAP.put("variable-body-medium", 400); + WEIGHT_MAP.put("variable-body-small", 400); + } + + public static FontController get() { + if (sInstance == null) { + sInstance = new FontController(); + } + return sInstance; + } + + private FontController() {} + + public static void OnConfigurationChanged(Resources res) { + get().handleOnConfiguration(res); + } + + public static Typeface getOverrideTypeface(String fontToOverride) { + return get().getOverrideTypefaceInternal(fontToOverride); + } + + public static String getCurrentFontFamily() { + return get().getCurrentFont(); + } + + private String getCurrentFont() { + if (sResources == null) return sFontFamily; + try { + int configId = sResources.getIdentifier("config_bodyFontFamily", "string", "android"); + if (configId != 0) { + String currFont = sResources.getString(configId); + if (!TextUtils.equals(sFontFamily, currFont)) { + sFontFamily = currFont; + logger("Font changed to: " + sFontFamily); + } + } + } catch (Exception e) { + logger("getCurrentFont failed: " + e.getMessage()); + } + return sFontFamily; + } + + private Typeface getOverrideTypefaceInternal(String fontToOverride) { + if (fontToOverride == null) return null; + + String pkgName = getCurrentPackageName(); + + if (pkgName == null) return null; + + final boolean isSysPkg = SYS_OVERRIDE_APPS.contains(pkgName); + + if (pkgName != null && EXCLUDED_APPS.contains(pkgName) && !isSysPkg) { + logger("Excluded app, skipping override: " + pkgName); + return null; + } + + String currentFont = getCurrentFont(); + + if (fontToOverride.matches("^" + Pattern.quote(currentFont) + "(-.*)?$")) { + logger(fontToOverride + " matches current font root '" + currentFont + "', skipping override!"); + return null; + } + + boolean override = OVERRIDE_FONTS.stream().anyMatch(fontToOverride::contains) + || (isSysPkg && SYS_OVERRIDE_FONTS.stream().anyMatch(fontToOverride::contains)); + if (!override) { + logger("Not on override list, skipping override: " + fontToOverride); + return null; + } + + int adjustment = getFontWeightAdjustment(); + return TypefaceFactory.create(fontToOverride, currentFont, adjustment); + } + + private void handleOnConfiguration(Resources res) { + sResources = res; + String pkgName = getCurrentPackageName(); + if (pkgName == null || EXCLUDED_APPS.contains(pkgName)) return; + logger("handleOnConfiguration: Changing default font to: " + Typeface.getFontName()); + Typeface.changeFont(); + } + + private String getCurrentPackageName() { + try { + return ActivityThread.currentPackageName(); + } catch (Exception e) { + logger("getCurrentPackageName failed: " + e.getMessage()); + return null; + } + } + + private int getFontWeightAdjustment() { + try { + Resources res = sResources; + if (res == null) return 0; + Configuration cfg = res.getConfiguration(); + return cfg != null ? cfg.fontWeightAdjustment : 0; + } catch (Exception e) { + logger("getFontWeightAdjustment failed: " + e.getMessage()); + return 0; + } + } + + private static void logger(String msg) { + if (SystemProperties.getBoolean("persist.sys.ax_font_debug", false)) { + Log.d(TAG, msg); + } + } + + private static class TypefaceFactory { + + public static Typeface create(String fontToOverride, String currentFont, int fontWeightAdjustment) { + int weight = resolveWeightByName(fontToOverride); + + if (fontWeightAdjustment != 0) { + weight = Math.min(1000, Math.max(100, weight + fontWeightAdjustment)); + } + + boolean isBold = weight >= 700; + boolean isItalic = fontToOverride.contains("italic"); + + int style = Typeface.NORMAL; + if (isBold && isItalic) style = Typeface.BOLD_ITALIC; + else if (isBold) style = Typeface.BOLD; + else if (isItalic) style = Typeface.ITALIC; + + Typeface base = Typeface.getSystemDefaultTypeface(currentFont); + Typeface result = Typeface.create(base, style); + result = Typeface.create(result, weight, isItalic); + + logger("TypefaceFactory.create: fontToOverride=" + fontToOverride + + ", style=" + style + + ", weight=" + weight + + ", adj=" + fontWeightAdjustment + + ", isItalic=" + isItalic + + ", success=" + (result != null)); + + return result; + } + + private static int resolveWeightByName(String familyName) { + Integer exactMatch = WEIGHT_MAP.get(familyName); + if (exactMatch != null) { + return exactMatch; + } + for (Map.Entry entry : WEIGHT_MAP.entrySet()) { + if (familyName.contains(entry.getKey())) { + return entry.getValue(); + } + } + return 400; + } + } +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 6f94566599bd..2adb60d74c8e 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -27,8 +27,11 @@ import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UiThread; +import android.app.ActivityThread; +import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; +import android.content.res.Resources; import android.graphics.fonts.Font; import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontStyle; @@ -56,6 +59,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.android.FontController; import com.android.internal.util.Preconditions; import com.android.text.flags.Flags; @@ -77,6 +81,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -178,7 +183,7 @@ public static void clearTypefaceCachesForTestingPurpose() { */ @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769347) - static final Map sSystemFontMap = new ArrayMap<>(); + static final Map sSystemFontMap = new HashMap<>(); // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping. static ByteBuffer sSystemFontMapBuffer = null; @@ -291,6 +296,8 @@ private void completeTypefaceInitialization(@NonNull Typeface initializedTypefac */ public static final String DEFAULT_FAMILY = "sans-serif"; + private static volatile String sFontName = DEFAULT_FAMILY; + static { if (Flags.doNotOverwriteStaticFinalField()) { DEFAULT_BOLD = new Typeface(Typeface.BOLD, 700, null); @@ -997,7 +1004,7 @@ public CustomFallbackBuilder(@NonNull FontFamily family) { * @return The best matching typeface. */ public static Typeface create(String familyName, @Style int style) { - return create(getSystemDefaultTypeface(familyName), style); + return create(getOverrideTypeface(familyName), style); } /** @@ -1380,7 +1387,14 @@ public void releaseNativeObjectForTest() { mCleaner.run(); } - private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { + /** @hide */ + public static Typeface getOverrideTypeface(@NonNull String familyName) { + Typeface tf = FontController.getOverrideTypeface(familyName); + return tf == null ? getSystemDefaultTypeface(familyName) : tf; + } + + /** @hide */ + public static Typeface getSystemDefaultTypeface(@NonNull String familyName) { Typeface tf = sSystemFontMap.get(familyName); return tf == null ? Typeface.DEFAULT : tf; } @@ -1584,6 +1598,37 @@ public static void initializePendingTypefaceLocked(Typeface pending, String fami systemFontMap.put(familyName, pending); } + /** @hide */ + public static void changeFont() { + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.evictAll(); + } + + String fontFamily = FontController.getCurrentFontFamily(); + + sFontName = fontFamily; + + Typeface tf = getOverrideTypeface(sFontName); + + Typeface tfBold = create(tf, BOLD); + Typeface tfItalic = create(tf, ITALIC); + Typeface tfItalicBold = create(tf, BOLD_ITALIC); + + nativeForceSetStaticFinalField("DEFAULT", tf); + nativeForceSetStaticFinalField("DEFAULT_BOLD", tfBold); + nativeForceSetStaticFinalField("SANS_SERIF", tf); + + changeDefaultFontForTest( + Arrays.asList( + tf, tfBold, tfItalic, tfItalicBold), + Arrays.asList(tf, Typeface.SERIF, Typeface.MONOSPACE)); + } + + /** @hide */ + public static String getFontName() { + return sFontName; + } + /** @hide */ @VisibleForTesting public static void setSystemFontMap(Map systemFontMap) { From 59ac8f9580f68376c69becd11ff363cd1da58a66 Mon Sep 17 00:00:00 2001 From: maxwen Date: Thu, 16 Jan 2020 13:03:21 +0100 Subject: [PATCH 0002/1315] Set alert dialog message to use system font Change-Id: I4a6aa42c5c4277a2d3ecc2847e3395185b655e3b Signed-off-by: Edwiin Kusuma Jaya Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/layout/alert_dialog_material.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/res/res/layout/alert_dialog_material.xml b/core/res/res/layout/alert_dialog_material.xml index 178505c264a4..b1510fdcb93d 100644 --- a/core/res/res/layout/alert_dialog_material.xml +++ b/core/res/res/layout/alert_dialog_material.xml @@ -54,7 +54,7 @@ android:layout_height="wrap_content" android:paddingEnd="?attr/dialogPreferredPadding" android:paddingStart="?attr/dialogPreferredPadding" - style="@style/TextAppearance.Material.Subhead" /> + style="@style/TextAppearance.DeviceDefault.Subhead" /> Date: Wed, 9 Apr 2025 07:43:10 +0800 Subject: [PATCH 0003/1315] Resources: don't crash the app if font is not found chrome crash: 04-08 19:47:38.590 E/cr_JniAndroid(7847): Handling uncaught Java exception 04-08 19:47:38.590 E/cr_JniAndroid(7847): android.content.res.Resources$NotFoundException: Font resource ID #0x7f0a0005 could not be retrieved. 04-08 19:47:38.590 E/cr_JniAndroid(7847): at AF4.b(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:220) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at AF4.a(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:20) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at Rx5.a(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:21) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at m56.(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:51) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at org.chromium.chrome.browser.compositor.LayerTitleCache.(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:84) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at dG2.w(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:36) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at ve0.s2(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:232) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at org.chromium.chrome.browser.ChromeTabbedActivity.P2(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:19) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at org.chromium.chrome.browser.ChromeTabbedActivity.J(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:86) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at xf0.run(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:51) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at Y90.run(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:52) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at org.chromium.base.task.TaskRunnerImpl.runTask(chromium-TrichromeChromeGoogle6432.aab-stable-704903833:31) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at android.os.MessageQueue.nativePollOnce(Native Method) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at android.os.MessageQueue.next(MessageQueue.java:387) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at android.os.Looper.loopOnce(Looper.java:189) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at android.os.Looper.loop(Looper.java:317) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at android.app.ActivityThread.main(ActivityThread.java:8934) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at java.lang.reflect.Method.invoke(Native Method) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591) 04-08 19:47:38.590 E/cr_JniAndroid(7847): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911) Change-Id: I375d1f09c8ee4c54a826803f8cd1662c03d160cb Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/content/res/Resources.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index d9ad0313ddc4..52958ee41a66 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -501,11 +501,11 @@ public ConfigurationBoundResourceCache getStateListAnimatorCa if (typeface != null) { return typeface; } + } catch (Exception e) { } finally { releaseTempTypedValue(value); } - throw new NotFoundException("Font resource ID #0x" - + Integer.toHexString(id)); + return Typeface.SANS_SERIF; } @NonNull From c2b7b9720c7a05a6350e50e98a505ef2b3a15bd2 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 6 Sep 2018 03:38:32 +0530 Subject: [PATCH 0004/1315] base: Add utils * Cleaned up version from A11. Change-Id: I0fba60e6b8a096309f5257db051e278d47ec211a Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/internal/util/matrixx/Utils.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 core/java/com/android/internal/util/matrixx/Utils.java diff --git a/core/java/com/android/internal/util/matrixx/Utils.java b/core/java/com/android/internal/util/matrixx/Utils.java new file mode 100644 index 000000000000..ebc1bc491ca9 --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/Utils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util.matrixx; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.SystemProperties; + +import java.util.List; + +public class Utils { + + public static boolean isPackageInstalled(Context context, String packageName, boolean ignoreState) { + if (packageName != null) { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, 0); + if (!pi.applicationInfo.enabled && !ignoreState) { + return false; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + return true; + } + + public static boolean isPackageInstalled(Context context, String packageName) { + return isPackageInstalled(context, packageName, true); + } + + public static boolean isPackageEnabled(Context context, String packageName) { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, 0); + return pi.applicationInfo.enabled; + } catch (PackageManager.NameNotFoundException notFound) { + return false; + } + } + + public static void switchScreenOff(Context ctx) { + PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE); + if (pm!= null) { + pm.goToSleep(SystemClock.uptimeMillis()); + } + } + + public static boolean deviceHasFlashlight(Context ctx) { + return ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + } + + public static boolean hasNavbarByDefault(Context context) { + boolean needsNav = context.getResources().getBoolean( + com.android.internal.R.bool.config_showNavigationBar); + String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); + if ("1".equals(navBarOverride)) { + needsNav = false; + } else if ("0".equals(navBarOverride)) { + needsNav = true; + } + return needsNav; + } +} From 93989922a7f6aca537f4da07df554b0698370780 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Fri, 20 Mar 2026 11:27:54 +0530 Subject: [PATCH 0005/1315] base: Add metric for ProjectMatrixx Change-Id: I1703fe467fe6e59ef20656de3e3afa2fdfd7a1e5 --- proto/src/metrics_constants/metrics_constants.proto | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proto/src/metrics_constants/metrics_constants.proto b/proto/src/metrics_constants/metrics_constants.proto index 7ac7859f3c83..706e76268564 100644 --- a/proto/src/metrics_constants/metrics_constants.proto +++ b/proto/src/metrics_constants/metrics_constants.proto @@ -7444,5 +7444,8 @@ message MetricsEvent { // ---- End Q Constants, all Q constants go above this line ---- // Add new aosp constants above this line. // END OF AOSP CONSTANTS + + // Project Matrixx + MATRIXX = 4000; } } From 279f7b885e136de9054fc62c3f6ebd850807a829 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 10 Jan 2024 01:58:52 +0530 Subject: [PATCH 0006/1315] Revert "Deprecate TunerService" This reverts commit 0aeb3700369302384fccc50a07daf2d526946336. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/tuner/TunerService.java | 5 ----- .../src/com/android/systemui/tuner/TunerServiceImpl.java | 4 ---- 2 files changed, 9 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java index a501e87902a8..5d09e064604a 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -20,11 +20,6 @@ import com.android.systemui.Dependency; -/** - * @deprecated Don't use this class to listen to Secure Settings. Use {@code SecureSettings} instead - * or {@code SettingsObserver} to be able to specify the handler. - */ -@Deprecated public abstract class TunerService { public static final String ACTION_CLEAR = "com.android.systemui.action.CLEAR_TUNER"; diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 4bc5581c4869..c1c1203bcb05 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -57,11 +57,7 @@ import javax.inject.Inject; /** - * @deprecated Don't use this class to listen to Secure Settings. Use {@code SecureSettings} instead - * or {@code SettingsObserver} to be able to specify the handler. - * This class will interact with SecureSettings using the main looper. */ -@Deprecated @SysUISingleton public class TunerServiceImpl extends TunerService { From ecd6c722f7438ab482c29c20ffeb4b738f056490 Mon Sep 17 00:00:00 2001 From: Rashed Abdel-Tawab Date: Tue, 3 Oct 2017 21:47:41 -0400 Subject: [PATCH 0007/1315] SystemUI: Allow using tuner API for LineageSettings Rebrand to Lineage and add support for custom string settings. Based on a squash of the following changes: Author: Steve Kondik Date: Tue Sep 13 23:46:34 2016 -0700 systemui: Allow using tuner API for CMSettings * Use the prefix "cm:" on the key supplied and it will redirect to CMSettings. Change-Id: Id4266a568deb8fb32857136990711b5d9d487721 Author: Zhao Wei Liew Date: Thu Oct 13 19:19:03 2016 +0800 SystemUI: tuner: Allow Tuner API for System settings We'd like to modify some AOSP System settings as well. Change-Id: I4e2e7bf680f20bce9619ff2adf243d0f8f4a9906 Author: Bruno Martins Date: Mon Sep 17 11:49:27 2018 +0100 TunerServiceImpl: Add support for Lineage global settings Change-Id: I7f3a2e024286f2d7c49e173a7a0bb9a2f486c5d9 Author: Wang Han <416810799@qq.com> Date: Thu, 21 May 2020 20:07:32 +0800 TunerServiceImpl: Prevent Lineage keys from tuner reset * This way, we don't need to update tuner blacklist. Change-Id: I8d8b356b4a8699483c158610e82f67c0fba652e1 Change-Id: Ide9622ea4c7be371b83ff7ae9a73ce5565423af1 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/tuner/TunerServiceImpl.java | 139 +++++++++++++++--- 1 file changed, 122 insertions(+), 17 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index c1c1203bcb05..23ea1d56ae86 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -27,6 +27,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; +import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.provider.Settings.Secure; @@ -50,6 +51,8 @@ import dagger.Lazy; +import lineageos.providers.LineageSettings; + import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -166,31 +169,120 @@ private void upgradeTuner(int oldVersion, int newVersion, Handler mainHandler) { setValue(TUNER_VERSION, newVersion); } + private boolean isLineageSetting(String key) { + return isLineageGlobal(key) || isLineageSystem(key) || isLineageSecure(key); + } + + private boolean isLineageGlobal(String key) { + return key.startsWith("lineageglobal:"); + } + + private boolean isLineageSystem(String key) { + return key.startsWith("lineagesystem:"); + } + + private boolean isLineageSecure(String key) { + return key.startsWith("lineagesecure:"); + } + + private boolean isSystem(String key) { + return key.startsWith("system:"); + } + + private String chomp(String key) { + return key.replaceFirst("^(lineageglobal|lineagesecure|lineagesystem|system):", ""); + } + @Override public String getValue(String setting) { - return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser); + if (isLineageGlobal(setting)) { + return LineageSettings.Global.getString(mContentResolver, chomp(setting)); + } else if (isLineageSecure(setting)) { + return LineageSettings.Secure.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else if (isLineageSystem(setting)) { + return LineageSettings.System.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else if (isSystem(setting)) { + return Settings.System.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else { + return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser); + } } @Override public void setValue(String setting, String value) { - Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser); + if (isLineageGlobal(setting)) { + LineageSettings.Global.putString(mContentResolver, chomp(setting), value); + } else if (isLineageSecure(setting)) { + LineageSettings.Secure.putStringForUser( + mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isLineageSystem(setting)) { + LineageSettings.System.putStringForUser( + mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isSystem(setting)) { + Settings.System.putStringForUser( + mContentResolver, chomp(setting), value, mCurrentUser); + } else { + Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser); + } } @Override public int getValue(String setting, int def) { - return Settings.Secure.getIntForUser(mContentResolver, setting, def, mCurrentUser); + if (isLineageGlobal(setting)) { + return LineageSettings.Global.getInt(mContentResolver, chomp(setting), def); + } else if (isLineageSecure(setting)) { + return LineageSettings.Secure.getIntForUser( + mContentResolver, chomp(setting), def, mCurrentUser); + } else if (isLineageSystem(setting)) { + return LineageSettings.System.getIntForUser( + mContentResolver, chomp(setting), def, mCurrentUser); + } else if (isSystem(setting)) { + return Settings.System.getIntForUser( + mContentResolver, chomp(setting), def, mCurrentUser); + } else { + return Settings.Secure.getIntForUser(mContentResolver, setting, def, mCurrentUser); + } } @Override public String getValue(String setting, String def) { - String ret = Secure.getStringForUser(mContentResolver, setting, mCurrentUser); + String ret; + if (isLineageGlobal(setting)) { + ret = LineageSettings.Global.getString(mContentResolver, chomp(setting)); + } else if (isLineageSecure(setting)) { + ret = LineageSettings.Secure.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else if (isLineageSystem(setting)) { + ret = LineageSettings.System.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else if (isSystem(setting)) { + ret = Settings.System.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); + } else { + ret = Secure.getStringForUser(mContentResolver, setting, mCurrentUser); + } if (ret == null) return def; return ret; } @Override public void setValue(String setting, int value) { - Settings.Secure.putIntForUser(mContentResolver, setting, value, mCurrentUser); + if (isLineageGlobal(setting)) { + LineageSettings.Global.putInt(mContentResolver, chomp(setting), value); + } else if (isLineageSecure(setting)) { + LineageSettings.Secure.putIntForUser( + mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isLineageSystem(setting)) { + LineageSettings.System.putIntForUser( + mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isSystem(setting)) { + Settings.System.putIntForUser(mContentResolver, chomp(setting), value, mCurrentUser); + } else { + Settings.Secure.putIntForUser(mContentResolver, setting, value, mCurrentUser); + } } @Override @@ -209,14 +301,25 @@ private void addTunable(Tunable tunable, String key) { mTunables.add(tunable); mLeakDetector.trackCollection(mTunables, "TunerService.mTunables"); } - Uri uri = Settings.Secure.getUriFor(key); + final Uri uri; + if (isLineageGlobal(key)) { + uri = LineageSettings.Global.getUriFor(chomp(key)); + } else if (isLineageSecure(key)) { + uri = LineageSettings.Secure.getUriFor(chomp(key)); + } else if (isLineageSystem(key)) { + uri = LineageSettings.System.getUriFor(chomp(key)); + } else if (isSystem(key)) { + uri = Settings.System.getUriFor(chomp(key)); + } else { + uri = Settings.Secure.getUriFor(key); + } if (!mListeningUris.containsKey(uri)) { mListeningUris.put(uri, key); - mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser); + mContentResolver.registerContentObserver(uri, false, mObserver, + isLineageGlobal(key) ? UserHandle.USER_ALL : mCurrentUser); } // Send the first state. - String value = DejankUtils.whitelistIpcs(() -> Settings.Secure - .getStringForUser(mContentResolver, key, mCurrentUser)); + String value = DejankUtils.whitelistIpcs(() -> getValue(key)); tunable.onTuningChanged(key, value); } @@ -236,7 +339,9 @@ protected void reregisterAll() { } mContentResolver.unregisterContentObserver(mObserver); for (Uri uri : mListeningUris.keySet()) { - mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser); + String key = mListeningUris.get(uri); + mContentResolver.registerContentObserver(uri, false, mObserver, + isLineageGlobal(key) ? UserHandle.USER_ALL : mCurrentUser); } } @@ -246,7 +351,7 @@ private void reloadSetting(Uri uri) { if (tunables == null) { return; } - String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); + String value = getValue(key); for (Tunable tunable : tunables) { tunable.onTuningChanged(key, value); } @@ -254,8 +359,7 @@ private void reloadSetting(Uri uri) { private void reloadAll() { for (String key : mTunableLookup.keySet()) { - String value = Settings.Secure.getStringForUser(mContentResolver, key, - mCurrentUser); + String value = getValue(key); for (Tunable tunable : mTunableLookup.get(key)) { tunable.onTuningChanged(key, value); } @@ -274,10 +378,10 @@ public void clearAllFromUser(int user) { // A couple special cases. for (String key : mTunableLookup.keySet()) { - if (ArrayUtils.contains(RESET_EXCEPTION_LIST, key)) { + if (ArrayUtils.contains(RESET_EXCEPTION_LIST, key) || isLineageSetting(key)) { continue; } - Settings.Secure.putStringForUser(mContentResolver, key, null, user); + setValue(key, null); } } @@ -330,8 +434,9 @@ public Observer() { @Override public void onChange(boolean selfChange, java.util.Collection uris, int flags, int userId) { - if (userId == mUserTracker.getUserId()) { - for (Uri u : uris) { + for (Uri u : uris) { + String key = mListeningUris.get(u); + if (userId == mUserTracker.getUserId() || isLineageGlobal(key)) { reloadSetting(u); } } From 36694483105cbb1d6101bab0f390f2fe7b11329b Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 19 Jan 2018 23:48:34 +0530 Subject: [PATCH 0008/1315] TunerService: Prevent NPE with tunable Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/tuner/TunerServiceImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 23ea1d56ae86..39ee352b1431 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -353,7 +353,9 @@ private void reloadSetting(Uri uri) { } String value = getValue(key); for (Tunable tunable : tunables) { - tunable.onTuningChanged(key, value); + if (tunable != null) { + tunable.onTuningChanged(key, value); + } } } @@ -361,7 +363,9 @@ private void reloadAll() { for (String key : mTunableLookup.keySet()) { String value = getValue(key); for (Tunable tunable : mTunableLookup.get(key)) { - tunable.onTuningChanged(key, value); + if (tunable != null) { + tunable.onTuningChanged(key, value); + } } } } From 27439e6350a98d06beaa7f01990ee9561af2f0fc Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 10 Jan 2024 02:02:40 +0530 Subject: [PATCH 0009/1315] TunerService: Add parseInteger method Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/tuner/TunerService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java index 5d09e064604a..b3bd5206cd8e 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -75,4 +75,12 @@ public static boolean parseIntegerSwitch(String value, boolean defaultValue) { return defaultValue; } } + + public static int parseInteger(String value, int defaultValue) { + try { + return value != null ? Integer.parseInt(value) : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } } From cba3be79a19bab45cda660d00ea98f7178c91e75 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 24 Dec 2017 11:29:10 +0530 Subject: [PATCH 0010/1315] SystemUI: Allow using tuner API for Global settings Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/tuner/TunerServiceImpl.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 39ee352b1431..fe9a0043d8c5 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -189,8 +189,12 @@ private boolean isSystem(String key) { return key.startsWith("system:"); } + private boolean isGlobal(String key) { + return key.startsWith("global:"); + } + private String chomp(String key) { - return key.replaceFirst("^(lineageglobal|lineagesecure|lineagesystem|system):", ""); + return key.replaceFirst("^(lineageglobal|lineagesecure|lineagesystem|system|global):", ""); } @Override @@ -206,6 +210,9 @@ public String getValue(String setting) { } else if (isSystem(setting)) { return Settings.System.getStringForUser( mContentResolver, chomp(setting), mCurrentUser); + } else if (isGlobal(setting)) { + return Settings.Global.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); } else { return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser); } @@ -224,6 +231,9 @@ public void setValue(String setting, String value) { } else if (isSystem(setting)) { Settings.System.putStringForUser( mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isGlobal(setting)) { + Settings.Global.putStringForUser( + mContentResolver, chomp(setting), value, mCurrentUser); } else { Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser); } @@ -242,6 +252,9 @@ public int getValue(String setting, int def) { } else if (isSystem(setting)) { return Settings.System.getIntForUser( mContentResolver, chomp(setting), def, mCurrentUser); + } else if (isGlobal(setting)) { + return Settings.Global.getInt( + mContentResolver, chomp(setting), def); } else { return Settings.Secure.getIntForUser(mContentResolver, setting, def, mCurrentUser); } @@ -261,6 +274,9 @@ public String getValue(String setting, String def) { } else if (isSystem(setting)) { ret = Settings.System.getStringForUser( mContentResolver, chomp(setting), mCurrentUser); + } else if (isGlobal(setting)) { + ret = Settings.Global.getStringForUser( + mContentResolver, chomp(setting), mCurrentUser); } else { ret = Secure.getStringForUser(mContentResolver, setting, mCurrentUser); } @@ -280,6 +296,8 @@ public void setValue(String setting, int value) { mContentResolver, chomp(setting), value, mCurrentUser); } else if (isSystem(setting)) { Settings.System.putIntForUser(mContentResolver, chomp(setting), value, mCurrentUser); + } else if (isGlobal(setting)) { + Settings.Global.putInt(mContentResolver, chomp(setting), value); } else { Settings.Secure.putIntForUser(mContentResolver, setting, value, mCurrentUser); } @@ -310,6 +328,8 @@ private void addTunable(Tunable tunable, String key) { uri = LineageSettings.System.getUriFor(chomp(key)); } else if (isSystem(key)) { uri = Settings.System.getUriFor(chomp(key)); + } else if (isGlobal(key)) { + uri = Settings.Global.getUriFor(chomp(key)); } else { uri = Settings.Secure.getUriFor(key); } From 5f3b2bdc47a4f946745b2f9e5f8bcfd1a84475bb Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 26 Nov 2025 03:09:32 +0530 Subject: [PATCH 0011/1315] SystemUI: TunerServiceImpl: Safe concurrent iteration and modification Fixes: https://github.com/crdroidandroid/issue_tracker/issues/830 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/tuner/TunerServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index fe9a0043d8c5..264be0b1ce60 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -56,6 +56,7 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; import javax.inject.Inject; @@ -312,7 +313,7 @@ public void addTunable(Tunable tunable, String... keys) { private void addTunable(Tunable tunable, String key) { if (!mTunableLookup.containsKey(key)) { - mTunableLookup.put(key, new ArraySet()); + mTunableLookup.put(key, new CopyOnWriteArraySet()); } mTunableLookup.get(key).add(tunable); if (LeakDetector.ENABLED) { From 5a69c1301f1f32a88843acdcf34d9beddc12b92a Mon Sep 17 00:00:00 2001 From: sb6596 Date: Sat, 25 Dec 2021 06:49:59 +0000 Subject: [PATCH 0012/1315] Bring back ThemeUtils for Theming Change-Id: I4324e8bc8d7c6d7df1809a1bd3424abf4dfa1306 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/ThemeUtils.java | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 core/java/com/android/internal/util/matrixx/ThemeUtils.java diff --git a/core/java/com/android/internal/util/matrixx/ThemeUtils.java b/core/java/com/android/internal/util/matrixx/ThemeUtils.java new file mode 100644 index 000000000000..fd8240f8f3bd --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/ThemeUtils.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util.matrixx; + +import static android.os.UserHandle.USER_SYSTEM; + +import android.util.PathParser; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.om.IOverlayManager; +import android.content.om.OverlayInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Typeface; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.net.Uri; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class ThemeUtils { + + public static final String TAG = "ThemeUtils"; + + public static final String FONT_KEY = "android.theme.customization.font"; + public static final String ICON_SHAPE_KEY= "android.theme.customization.adaptive_icon_shape"; + + public static final Comparator OVERLAY_INFO_COMPARATOR = + Comparator.comparingInt(a -> a.priority); + + private Context mContext; + private IOverlayManager mOverlayManager; + private PackageManager pm; + private Resources overlayRes; + + public ThemeUtils(Context context) { + mContext = context; + mOverlayManager = IOverlayManager.Stub + .asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE)); + pm = context.getPackageManager(); + } + + public void setOverlayEnabled(String category, String packageName) { + final String currentPackageName = getOverlayInfos(category).stream() + .filter(info -> info.isEnabled()) + .map(info -> info.packageName) + .findFirst() + .orElse(null); + + try { + if ("android".equals(packageName)) { + mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); + } else { + mOverlayManager.setEnabledExclusiveInCategory(packageName, + USER_SYSTEM); + } + + writeSettings(category, packageName, "android".equals(packageName)); + + } catch (RemoteException e) { + } + } + + public void writeSettings(String category, String packageName, boolean disable) { + final String overlayPackageJson = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, USER_SYSTEM); + JSONObject object; + try { + if (overlayPackageJson == null) { + object = new JSONObject(); + } else { + object = new JSONObject(overlayPackageJson); + } + if (disable) { + if (object.has(category)) object.remove(category); + } else { + object.put(category, packageName); + } + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + object.toString(), USER_SYSTEM); + } catch (JSONException e) { + Log.e(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e); + } + } + + public List getOverlayPackagesForCategory(String category) { + return getOverlayPackagesForCategory(category, "android"); + } + + public List getOverlayPackagesForCategory(String category, String target) { + List overlays = new ArrayList<>(); + overlays.add("android"); + for (OverlayInfo info : getOverlayInfos(category, target)) { + if (category.equals(info.getCategory())) { + overlays.add(info.getPackageName()); + } + } + return overlays; + } + + public List getOverlayInfos(String category) { + return getOverlayInfos(category, "android"); + } + + public List getOverlayInfos(String category, String target) { + final List filteredInfos = new ArrayList<>(); + try { + List overlayInfos = mOverlayManager + .getOverlayInfosForTarget(target, USER_SYSTEM); + for (OverlayInfo overlayInfo : overlayInfos) { + if (category.equals(overlayInfo.category)) { + filteredInfos.add(overlayInfo); + } + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + filteredInfos.sort(OVERLAY_INFO_COMPARATOR); + return filteredInfos; + } + + public List getLabels(String category) { + List labels = new ArrayList<>(); + labels.add("Default"); + for (OverlayInfo info : getOverlayInfos(category)) { + if (category.equals(info.getCategory())) { + try { + labels.add(pm.getApplicationInfo(info.packageName, 0) + .loadLabel(pm).toString()); + } catch (PackageManager.NameNotFoundException e) { + labels.add(info.packageName); + } + } + } + return labels; + } + + public List getFonts() { + final List fontlist = new ArrayList<>(); + for (String overlayPackage : getOverlayPackagesForCategory(FONT_KEY)) { + try { + overlayRes = overlayPackage.equals("android") ? Resources.getSystem() + : pm.getResourcesForApplication(overlayPackage); + final String font = overlayRes.getString( + overlayRes.getIdentifier("config_bodyFontFamily", + "string", overlayPackage)); + fontlist.add(Typeface.create(font, Typeface.NORMAL)); + } catch (NameNotFoundException | NotFoundException e) { + // Do nothing + } + } + return fontlist; + } + + public List getShapeDrawables() { + final List shapelist = new ArrayList<>(); + for (String overlayPackage : getOverlayPackagesForCategory(ICON_SHAPE_KEY)) { + shapelist.add(createShapeDrawable(overlayPackage)); + } + return shapelist; + } + + public ShapeDrawable createShapeDrawable(String overlayPackage) { + try { + if (overlayPackage.equals("android")) { + overlayRes = Resources.getSystem(); + } else { + if (overlayPackage.equals("default")) overlayPackage = "android"; + overlayRes = pm.getResourcesForApplication(overlayPackage); + } + } catch (NameNotFoundException | NotFoundException e) { + // Do nothing + } + final String shape = overlayRes.getString( + overlayRes.getIdentifier("config_icon_mask", + "string", overlayPackage)); + Path path = TextUtils.isEmpty(shape) ? null : PathParser.createPathFromPathData(shape); + PathShape pathShape = new PathShape(path, 100f, 100f); + ShapeDrawable shapeDrawable = new ShapeDrawable(pathShape); + int mThumbSize = (int) (mContext.getResources().getDisplayMetrics().density * 72); + shapeDrawable.setIntrinsicHeight(mThumbSize); + shapeDrawable.setIntrinsicWidth(mThumbSize); + return shapeDrawable; + } + + public boolean isOverlayEnabled(String overlayPackage) { + try { + OverlayInfo info = mOverlayManager.getOverlayInfo(overlayPackage, USER_SYSTEM); + return info == null ? false : info.isEnabled(); + } catch (RemoteException e) { + e.printStackTrace(); + } + return false; + } + + public boolean isDefaultOverlay(String category) { + for (String overlayPackage : getOverlayPackagesForCategory(category)) { + try { + OverlayInfo info = mOverlayManager.getOverlayInfo(overlayPackage, USER_SYSTEM); + if (info != null && info.isEnabled()) { + return false; + } else { + continue; + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + return true; + } +} From f5d64b816242c2c78dd16a04f7c25fd5a54de1db Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 19 Feb 2022 18:44:31 +0530 Subject: [PATCH 0013/1315] ThemeUtils: Make it compatible for all targets * Also, sort packages here itself and remove unused function. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/ThemeUtils.java | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/ThemeUtils.java b/core/java/com/android/internal/util/matrixx/ThemeUtils.java index fd8240f8f3bd..3666668c812c 100644 --- a/core/java/com/android/internal/util/matrixx/ThemeUtils.java +++ b/core/java/com/android/internal/util/matrixx/ThemeUtils.java @@ -48,6 +48,7 @@ import org.json.JSONObject; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -73,22 +74,22 @@ public ThemeUtils(Context context) { pm = context.getPackageManager(); } - public void setOverlayEnabled(String category, String packageName) { - final String currentPackageName = getOverlayInfos(category).stream() + public void setOverlayEnabled(String category, String packageName, String target) { + final String currentPackageName = getOverlayInfos(category, target).stream() .filter(info -> info.isEnabled()) .map(info -> info.packageName) .findFirst() .orElse(null); try { - if ("android".equals(packageName)) { + if (target.equals(packageName)) { mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); } else { mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM); } - writeSettings(category, packageName, "android".equals(packageName)); + writeSettings(category, packageName, target.equals(packageName)); } catch (RemoteException e) { } @@ -124,12 +125,15 @@ public List getOverlayPackagesForCategory(String category) { public List getOverlayPackagesForCategory(String category, String target) { List overlays = new ArrayList<>(); - overlays.add("android"); + List mPkgs = new ArrayList<>(); + overlays.add(target); for (OverlayInfo info : getOverlayInfos(category, target)) { if (category.equals(info.getCategory())) { - overlays.add(info.getPackageName()); + mPkgs.add(info.getPackageName()); } } + Collections.sort(mPkgs); + overlays.addAll(mPkgs); return overlays; } @@ -154,22 +158,6 @@ public List getOverlayInfos(String category, String target) { return filteredInfos; } - public List getLabels(String category) { - List labels = new ArrayList<>(); - labels.add("Default"); - for (OverlayInfo info : getOverlayInfos(category)) { - if (category.equals(info.getCategory())) { - try { - labels.add(pm.getApplicationInfo(info.packageName, 0) - .loadLabel(pm).toString()); - } catch (PackageManager.NameNotFoundException e) { - labels.add(info.packageName); - } - } - } - return labels; - } - public List getFonts() { final List fontlist = new ArrayList<>(); for (String overlayPackage : getOverlayPackagesForCategory(FONT_KEY)) { From 976bb3c26b76e616c141c8df0b6f719ee2db3e86 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 24 Aug 2024 02:07:37 +0530 Subject: [PATCH 0014/1315] ThemeUtils: Use current user for THEME_CUSTOMIZATION_OVERLAY_PACKAGES * Follow ThemePicker and SystemUI. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/util/matrixx/ThemeUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/ThemeUtils.java b/core/java/com/android/internal/util/matrixx/ThemeUtils.java index 3666668c812c..c1fc3d6d57a2 100644 --- a/core/java/com/android/internal/util/matrixx/ThemeUtils.java +++ b/core/java/com/android/internal/util/matrixx/ThemeUtils.java @@ -40,6 +40,7 @@ import android.net.Uri; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -98,7 +99,7 @@ public void setOverlayEnabled(String category, String packageName, String target public void writeSettings(String category, String packageName, boolean disable) { final String overlayPackageJson = Settings.Secure.getStringForUser( mContext.getContentResolver(), - Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, USER_SYSTEM); + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, UserHandle.USER_CURRENT); JSONObject object; try { if (overlayPackageJson == null) { @@ -113,7 +114,7 @@ public void writeSettings(String category, String packageName, boolean disable) } Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, - object.toString(), USER_SYSTEM); + object.toString(), UserHandle.USER_CURRENT); } catch (JSONException e) { Log.e(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e); } From 90fb5d8994149858e90fd8a18585513a6acab785 Mon Sep 17 00:00:00 2001 From: spezi77 Date: Tue, 14 Dec 2021 17:18:13 +0100 Subject: [PATCH 0015/1315] ThemeOverlayApplier: Catch a potential NPE. java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.om.OverlayIdentifier android.content.om.FabricatedOverlay.getIdentifier()' on a null object reference Signed-off-by: spezi77 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/systemui/theme/ThemeOverlayApplier.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index 9bfc193a508e..b8da99faf0f8 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -234,8 +234,12 @@ public void applyCurrentUserOverlays( if (pendingCreation != null) { isBlackMode = pendingCreation.length == 2; for (FabricatedOverlay overlay : pendingCreation) { - identifiersPending.add(overlay.getIdentifier()); - transaction.registerFabricatedOverlay(overlay); + try { + identifiersPending.add(overlay.getIdentifier()); + transaction.registerFabricatedOverlay(overlay); + } catch (NullPointerException e) { + Log.e(TAG, "NPE for overlay.getIdentifier()", e); + } } } From bb190770fd88010d1662cadbeda66084561bb732 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 23 Mar 2025 10:53:02 +0530 Subject: [PATCH 0016/1315] ThemeUtils: Improve overall usage Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/ThemeUtils.java | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/ThemeUtils.java b/core/java/com/android/internal/util/matrixx/ThemeUtils.java index c1fc3d6d57a2..ac58fd4ea081 100644 --- a/core/java/com/android/internal/util/matrixx/ThemeUtils.java +++ b/core/java/com/android/internal/util/matrixx/ThemeUtils.java @@ -63,6 +63,8 @@ public class ThemeUtils { public static final Comparator OVERLAY_INFO_COMPARATOR = Comparator.comparingInt(a -> a.priority); + private static ThemeUtils instance; + private Context mContext; private IOverlayManager mOverlayManager; private PackageManager pm; @@ -75,6 +77,17 @@ public ThemeUtils(Context context) { pm = context.getPackageManager(); } + public static ThemeUtils getInstance(Context context) { + if (instance == null) { + synchronized (ThemeUtils.class) { + if (instance == null) { + instance = new ThemeUtils(context); + } + } + } + return instance; + } + public void setOverlayEnabled(String category, String packageName, String target) { final String currentPackageName = getOverlayInfos(category, target).stream() .filter(info -> info.isEnabled()) @@ -84,15 +97,17 @@ public void setOverlayEnabled(String category, String packageName, String target try { if (target.equals(packageName)) { - mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); + if (currentPackageName != null) { + mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); + } } else { - mOverlayManager.setEnabledExclusiveInCategory(packageName, - USER_SYSTEM); + mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM); } writeSettings(category, packageName, target.equals(packageName)); } catch (RemoteException e) { + Log.e(TAG, "RemoteException while setting overlay: " + e.getMessage(), e); } } @@ -153,7 +168,7 @@ public List getOverlayInfos(String category, String target) { } } } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); + Log.e(TAG, "RemoteException while getting overlay info: " + re.getMessage(), re); } filteredInfos.sort(OVERLAY_INFO_COMPARATOR); return filteredInfos; @@ -161,18 +176,22 @@ public List getOverlayInfos(String category, String target) { public List getFonts() { final List fontlist = new ArrayList<>(); - for (String overlayPackage : getOverlayPackagesForCategory(FONT_KEY)) { - try { - overlayRes = overlayPackage.equals("android") ? Resources.getSystem() - : pm.getResourcesForApplication(overlayPackage); - final String font = overlayRes.getString( - overlayRes.getIdentifier("config_bodyFontFamily", - "string", overlayPackage)); - fontlist.add(Typeface.create(font, Typeface.NORMAL)); - } catch (NameNotFoundException | NotFoundException e) { - // Do nothing + for (String overlayPackage : getOverlayPackagesForCategory(FONT_KEY)) { + Resources overlayRes = null; + try { + overlayRes = overlayPackage.equals("android") ? Resources.getSystem() + : pm.getResourcesForApplication(overlayPackage); + if (overlayRes != null) { + int fontId = overlayRes.getIdentifier("config_bodyFontFamily", "string", overlayPackage); + if (fontId != 0) { + String fontName = overlayRes.getString(fontId); + fontlist.add(Typeface.create(fontName, Typeface.NORMAL)); + } } + } catch (NameNotFoundException | NotFoundException e) { + Log.e(TAG, "Error fetching fonts for package: " + overlayPackage, e); } + } return fontlist; } @@ -195,6 +214,10 @@ public ShapeDrawable createShapeDrawable(String overlayPackage) { } catch (NameNotFoundException | NotFoundException e) { // Do nothing } + if (overlayRes == null) { + Log.e(TAG, "Resources not found for package: " + overlayPackage); + return null; + } final String shape = overlayRes.getString( overlayRes.getIdentifier("config_icon_mask", "string", overlayPackage)); @@ -212,24 +235,13 @@ public boolean isOverlayEnabled(String overlayPackage) { OverlayInfo info = mOverlayManager.getOverlayInfo(overlayPackage, USER_SYSTEM); return info == null ? false : info.isEnabled(); } catch (RemoteException e) { - e.printStackTrace(); + Log.e(TAG, "RemoteException while checking if overlay is enabled: " + e.getMessage(), e); } return false; } public boolean isDefaultOverlay(String category) { - for (String overlayPackage : getOverlayPackagesForCategory(category)) { - try { - OverlayInfo info = mOverlayManager.getOverlayInfo(overlayPackage, USER_SYSTEM); - if (info != null && info.isEnabled()) { - return false; - } else { - continue; - } - } catch (RemoteException e) { - e.printStackTrace(); - } - } - return true; + return getOverlayPackagesForCategory(category).stream() + .noneMatch(pkg -> isOverlayEnabled(pkg)); } } From 0bfbde834a59019754424e771e71b423f1eba304 Mon Sep 17 00:00:00 2001 From: t-m-w Date: Sat, 30 Jul 2022 08:30:20 -0400 Subject: [PATCH 0017/1315] Avoid Settings app NPE on broken packages Avoid Settings app NullPointerException that results from broken uninstalled packages in a work profile or secondary user. It's unclear what causes some packages to have a nonexistent codePath/sourceDir, but in the event that they do, this shouldn't cause Settings to crash. The NPE does not occur when reverting I0ff770f2, but doing so is likely not desirable. Issue: calyxos#1105 Change-Id: I7a1ec0f6ded2e93e29bec39146fd8007b77e2fc0 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/settingslib/applications/ApplicationsState.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 4e7611ed802d..38dfdfae12cc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -767,6 +767,10 @@ private AppEntry getEntryLocked(ApplicationInfo info) { } return null; } + if (info.sourceDir == null) { + // Avoid NullPointerException for broken uninstalled packages. + return null; + } if (DEBUG) { Log.i(TAG, "Creating AppEntry for " + info.packageName); } From 7cc193fb7e5ac9a0d53706011e7905c4304891be Mon Sep 17 00:00:00 2001 From: Oliver Scott Date: Thu, 5 Jan 2023 22:37:42 -0500 Subject: [PATCH 0018/1315] Set FakeStore/PlayStore as Aurora Store installer package name Issue: calyxos#240 Co-authored-by: Chirayu Desai Co-authored-by: Tad Change-Id: I883bc16231fa7417b71e273a1909f3178bb8e0b0 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/pm/ComputerEngine.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 1cc02de121f8..bf439180f52d 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -87,6 +87,7 @@ import android.content.pm.InstrumentationInfo; import android.content.pm.KeySet; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; @@ -427,6 +428,10 @@ public List getVolumePackages( private static final Signature MICROG_FAKE_SIGNATURE = new Signature("308204433082032ba003020102020900c2e08746644a308d300d06092a864886f70d01010405003074310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f69643110300e06035504031307416e64726f6964301e170d3038303832313233313333345a170d3336303130373233313333345a3074310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f69643110300e06035504031307416e64726f696430820120300d06092a864886f70d01010105000382010d00308201080282010100ab562e00d83ba208ae0a966f124e29da11f2ab56d08f58e2cca91303e9b754d372f640a71b1dcb130967624e4656a7776a92193db2e5bfb724a91e77188b0e6a47a43b33d9609b77183145ccdf7b2e586674c9e1565b1f4c6a5955bff251a63dabf9c55c27222252e875e4f8154a645f897168c0b1bfc612eabf785769bb34aa7984dc7e2ea2764cae8307d8c17154d7ee5f64a51a44a602c249054157dc02cd5f5c0e55fbef8519fbe327f0b1511692c5a06f19d18385f5c4dbc2d6b93f68cc2979c70e18ab93866b3bd5db8999552a0e3b4c99df58fb918bedc182ba35e003c1b4b10dd244a8ee24fffd333872ab5221985edab0fc0d0b145b6aa192858e79020103a381d93081d6301d0603551d0e04160414c77d8cc2211756259a7fd382df6be398e4d786a53081a60603551d2304819e30819b8014c77d8cc2211756259a7fd382df6be398e4d786a5a178a4763074310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f69643110300e06035504031307416e64726f6964820900c2e08746644a308d300c0603551d13040530030101ff300d06092a864886f70d010104050003820101006dd252ceef85302c360aaace939bcff2cca904bb5d7a1661f8ae46b2994204d0ff4a68c7ed1a531ec4595a623ce60763b167297a7ae35712c407f208f0cb109429124d7b106219c084ca3eb3f9ad5fb871ef92269a8be28bf16d44c8d9a08e6cb2f005bb3fe2cb96447e868e731076ad45b33f6009ea19c161e62641aa99271dfd5228c5c587875ddb7f452758d661f6cc0cccb7352e424cc4365c523532f7325137593c4ae341f4db41edda0d0b1071a7c440f0fe9ea01cb627ca674369d084bd2fd911ff06cdbf2cfa10dc0f893ae35762919048c7efc64c7144178342f70581c9de573af55b390dd7fdb9418631895d5f759f30112687ff621410c069308a"); private static final Signature MICROG_REAL_SIGNATURE = new Signature("308202ed308201d5a003020102020426ffa009300d06092a864886f70d01010b05003027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a656374301e170d3132313030363132303533325a170d3337303933303132303533325a3027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a65637430820122300d06092a864886f70d01010105000382010f003082010a02820101009a8d2a5336b0eaaad89ce447828c7753b157459b79e3215dc962ca48f58c2cd7650df67d2dd7bda0880c682791f32b35c504e43e77b43c3e4e541f86e35a8293a54fb46e6b16af54d3a4eda458f1a7c8bc1b7479861ca7043337180e40079d9cdccb7e051ada9b6c88c9ec635541e2ebf0842521c3024c826f6fd6db6fd117c74e859d5af4db04448965ab5469b71ce719939a06ef30580f50febf96c474a7d265bb63f86a822ff7b643de6b76e966a18553c2858416cf3309dd24278374bdd82b4404ef6f7f122cec93859351fc6e5ea947e3ceb9d67374fe970e593e5cd05c905e1d24f5a5484f4aadef766e498adf64f7cf04bddd602ae8137b6eea40722d0203010001a321301f301d0603551d0e04160414110b7aa9ebc840b20399f69a431f4dba6ac42a64300d06092a864886f70d01010b0500038201010007c32ad893349cf86952fb5a49cfdc9b13f5e3c800aece77b2e7e0e9c83e34052f140f357ec7e6f4b432dc1ed542218a14835acd2df2deea7efd3fd5e8f1c34e1fb39ec6a427c6e6f4178b609b369040ac1f8844b789f3694dc640de06e44b247afed11637173f36f5886170fafd74954049858c6096308fc93c1bc4dd5685fa7a1f982a422f2a3b36baa8c9500474cf2af91c39cbec1bc898d10194d368aa5e91f1137ec115087c31962d8f76cd120d28c249cf76f4c70f5baa08c70a7234ce4123be080cee789477401965cfe537b924ef36747e8caca62dfefdd1a6288dcb1c4fd2aaa6131a7ad254e9742022cfd597d2ca5c660ce9e41ff537e5a4041e37"); + private static final String AURORA_SERVICES = "com.aurora.services"; + private static final String AURORA_STORE = "com.aurora.store"; + private static final String PLAY_STORE = "com.android.vending"; + // PackageManagerService attributes that are primitives are referenced through the // pms object directly. Primitives are the only attributes so referenced. protected final PackageManagerService mService; @@ -5148,6 +5153,22 @@ private InstallSource getInstallSource(@NonNull String packageName, int callingU return null; } + InstallSource installSource = ps.getInstallSource(); + final String installerPackageName = installSource.mInstallerPackageName; + if (installSource != null && installerPackageName != null + && mSettings.getPackage(PLAY_STORE) != null + && callingUid != Process.SYSTEM_UID + && (AURORA_STORE.equals(installerPackageName) + || AURORA_SERVICES.equals(installerPackageName))) { + return InstallSource.create(PLAY_STORE, PLAY_STORE, PLAY_STORE, + installSource.mInstallerPackageUid, // FIXME: likely wrong + installSource.mUpdateOwnerPackageName, + installSource.mInstallerAttributionTag, + PackageInstaller.PACKAGE_SOURCE_STORE) + .setInitiatingPackageSignatures(new PackageSignatures( + mSettings.getPackage(PLAY_STORE).getSigningDetails())); + } + return ps.getInstallSource(); } From d1463eed49742df606f89862c75b558096308767 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 16 Mar 2024 20:01:22 +0530 Subject: [PATCH 0019/1315] Allow signature spoofing on user builds * This is custom rom breaking google's integrity check. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/pm/ComputerEngine.java | 6 --- services/core/jni/Android.bp | 7 ---- .../com_android_server_pm_ComputerEngine.cpp | 38 ------------------- services/core/jni/onload.cpp | 2 - 4 files changed, 53 deletions(-) delete mode 100644 services/core/jni/com_android_server_pm_ComputerEngine.cpp diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index bf439180f52d..19b8263be021 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -1488,13 +1488,7 @@ private List maybeAddInstantAppInstaller(List result, return result; } - private static native boolean isDebuggable(); - public static boolean isMicrogSigned(AndroidPackage p) { - if (!isDebuggable()) { - return false; - } - // Allowlist the following apps: // * com.android.vending - microG Companion // * com.google.android.gms - microG Services diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 109abd5330be..6bd4e821b3d7 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -70,7 +70,6 @@ cc_library_static { "com_android_server_vibrator_VibratorManagerService.cpp", "com_android_server_pdb_PersistentDataBlockService.cpp", "com_android_server_am_LowMemDetector.cpp", - "com_android_server_pm_ComputerEngine.cpp", "com_android_server_pm_PackageManagerShellCommandDataLoader.cpp", "com_android_server_sensor_SensorService.cpp", "com_android_server_utils_LongMethodTracer.cpp", @@ -96,12 +95,6 @@ cc_library_static { header_libs: [ "bionic_libc_platform_headers", ], - - product_variables: { - debuggable: { - cflags: ["-DANDROID_DEBUGGABLE"], - } - }, } cc_defaults { diff --git a/services/core/jni/com_android_server_pm_ComputerEngine.cpp b/services/core/jni/com_android_server_pm_ComputerEngine.cpp deleted file mode 100644 index bbe298097a2a..000000000000 --- a/services/core/jni/com_android_server_pm_ComputerEngine.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2024 The LineageOS Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -namespace android { - -static bool isDebuggable(JNIEnv* env) { -#ifdef ANDROID_DEBUGGABLE - return true; -#else - return false; -#endif -} - -static const JNINativeMethod method_table[] = { - {"isDebuggable", "()Z", (void*)isDebuggable}, -}; - -int register_android_server_com_android_server_pm_ComputerEngine(JNIEnv* env) { - return jniRegisterNativeMethods(env, "com/android/server/pm/ComputerEngine", - method_table, NELEM(method_table)); -} - -} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index f9d952e16469..d50b0f909ab6 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -58,7 +58,6 @@ int register_android_server_utils_AnrTimer(JNIEnv *env); int register_android_server_utils_LazyJniRegistrar(JNIEnv* env); int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env); int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env); -int register_android_server_com_android_server_pm_ComputerEngine(JNIEnv* env); int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env); int register_android_server_AdbDebuggingManager(JNIEnv* env); int register_android_server_FaceService(JNIEnv* env); @@ -126,7 +125,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_utils_LazyJniRegistrar(env); register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env); register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env); - register_android_server_com_android_server_pm_ComputerEngine(env); register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env); register_android_server_AdbDebuggingManager(env); register_android_server_FaceService(env); From a3e8a382d31f95bb4fca65433c103d8b207ea33b Mon Sep 17 00:00:00 2001 From: someone5678 Date: Wed, 5 Jun 2024 16:01:18 +0900 Subject: [PATCH 0020/1315] SystemUI: Avoid NPE in ClockRegistry Log: 06-05 15:49:21.806 3530 3530 E AndroidRuntime: FATAL EXCEPTION: main 06-05 15:49:21.806 3530 3530 E AndroidRuntime: Process: com.android.systemui, PID: 3530 06-05 15:49:21.806 3530 3530 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'java.util.List com.android.systemui.plugins.clocks.ClockProvider.getClocks()' on a null object reference 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.clocks.ClockRegistry$pluginListener$1.onPluginLoaded(ClockRegistry.kt:223) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.clocks.ClockRegistry$pluginListener$1.onPluginLoaded(ClockRegistry.kt:147) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginInstance.loadPlugin(PluginInstance.java:166) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginInstance.onCreate(PluginInstance.java:116) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginActionManager.onPluginConnected(PluginActionManager.java:213) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginActionManager.lambda$handleQueryPlugins$6(PluginActionManager.java:279) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginActionManager.$r8$lambda$3aJVE1yNrdzfP7_csYMIjrdk6Hc(PluginActionManager.java:0) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.systemui.shared.plugins.PluginActionManager$$ExternalSyntheticLambda4.run(R8$$SyntheticClass:0) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:959) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:100) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8502) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:555) 06-05 15:49:21.806 3530 3530 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878) Change-Id: I42512041db498a963ed726efd62d748d5d5033d2 Signed-off-by: someone5678 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/shared/clocks/ClockRegistry.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index a490ad21f172..530a86c27486 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -195,6 +195,7 @@ open class ClockRegistry( plugin.initialize(clockBuffers) var isClockListChanged = false + plugin ?: return for (clock in plugin.getClocks()) { val id = clock.clockId val info = @@ -232,6 +233,7 @@ open class ClockRegistry( plugin: ClockProviderPlugin, manager: PluginLifecycleManager, ) { + plugin ?: return for (clock in plugin.getClocks()) { val id = clock.clockId val info = availableClocks[id] From 4a19908a7edb7cd28dd243c791d0fadce8b36f90 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 2 Nov 2025 21:55:02 +0530 Subject: [PATCH 0021/1315] SystemUI: Add defensive checks in ClockRegistry Log: 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): Failed to parse clock settings 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at org.json.JSONTokener.nextCleanInternal(JSONTokener.java:121) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at org.json.JSONTokener.nextValue(JSONTokener.java:98) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at org.json.JSONObject.(JSONObject.java:168) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at org.json.JSONObject.(JSONObject.java:185) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at com.android.systemui.shared.clocks.ClockRegistry.querySettings(go/retraceme 58953af1c47628b0bd6e0edf694cb559a1e6d114937272e2ebf36a10b1a129c4:24) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at com.android.systemui.shared.clocks.ClockRegistry$registerListeners$1.invokeSuspend(go/retraceme 58953af1c47628b0bd6e0edf694cb559a1e6d114937272e2ebf36a10b1a129c4:12) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(go/retraceme 58953af1c47628b0bd6e0edf694cb559a1e6d114937272e2ebf36a10b1a129c4:8) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at kotlinx.coroutines.DispatchedTask.run(go/retraceme 58953af1c47628b0bd6e0edf694cb559a1e6d114937272e2ebf36a10b1a129c4:110) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:524) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.util.concurrent.FutureTask.run(FutureTask.java:317) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651) 10-04 22:58:17.482 7192 7230 E ClockRegistry (System): at java.lang.Thread.run(Thread.java:1119) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/shared/clocks/ClockRegistry.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 530a86c27486..181112bfa847 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -328,6 +328,17 @@ open class ClockRegistry( var isRegistered: Boolean = false private set + private fun parseClockSettingsOrNull(raw: String?): ClockSettings? { + if (raw.isNullOrBlank()) return null + if (raw.equals("null", ignoreCase = true)) return null + + return try { + ClockSettings.fromJson(JSONObject(raw)) + } catch (_: Exception) { + null + } + } + @OpenForTesting open fun querySettings() { assert.isNotMainThread() @@ -346,7 +357,7 @@ open class ClockRegistry( Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, ) } - json?.let { ClockSettings.fromJson(JSONObject(it)) } + parseClockSettingsOrNull(json) } catch (ex: Exception) { logger.e("Failed to parse clock settings", ex) null From 6f82f83af0de5203674c627430e364eacf9ce11e Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 14 Nov 2019 01:40:35 +0530 Subject: [PATCH 0022/1315] core: pm: Wipe package cache on upgrade Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/pm/PackageManagerService.java | 2 +- .../com/android/server/pm/PackageManagerServiceUtils.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index fb3e19f18a6a..9a2b67a382e8 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2284,7 +2284,7 @@ public Set getInstallConstraintsAllowlist() { } mCacheDir = PackageManagerServiceUtils.preparePackageParserCache( - mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion); + mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion, mIsUpgrade); mInitialNonStoppedSystemPackages = mInjector.getSystemConfig() .getInitialNonStoppedSystemPackages(); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 121dc089d666..ae517755084d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -1275,7 +1275,7 @@ public static void enforceSystemOrRoot(String message) { } public static @Nullable File preparePackageParserCache(boolean forEngBuild, - boolean isUserDebugBuild, String incrementalVersion) { + boolean isUserDebugBuild, String incrementalVersion, boolean isUpgrade) { if (!FORCE_PACKAGE_PARSED_CACHE_ENABLED) { if (!DEFAULT_PACKAGE_PARSER_CACHE_ENABLED) { return null; @@ -1306,7 +1306,7 @@ public static void enforceSystemOrRoot(String message) { // Reconcile cache directories, keeping only what we'd actually use. for (File cacheDir : FileUtils.listFilesOrEmpty(cacheBaseDir)) { - if (Objects.equals(cacheName, cacheDir.getName())) { + if (!isUpgrade && Objects.equals(cacheName, cacheDir.getName())) { Slog.d(TAG, "Keeping known cache " + cacheDir.getName()); } else { Slog.d(TAG, "Destroying unknown cache " + cacheDir.getName()); @@ -1331,7 +1331,7 @@ public static void enforceSystemOrRoot(String message) { // NOTE: When no BUILD_NUMBER is set by the build system, it defaults to a build // that starts with "eng." to signify that this is an engineering build and not // destined for release. - if (isUserDebugBuild && incrementalVersion.startsWith("eng.")) { + if (isUpgrade || (isUserDebugBuild && incrementalVersion.startsWith("eng."))) { // Heuristic: If the /system directory has been modified recently due to an "adb sync" // or a regular make, then blow away the cache. Note that mtimes are *NOT* reliable // in general and should not be used for production changes. In this specific case, From f123e009b85837a2ee89633d527a08a2c422907e Mon Sep 17 00:00:00 2001 From: Mesquita Date: Wed, 18 Oct 2023 20:56:20 -0300 Subject: [PATCH 0023/1315] LockPatternUtils: Decrease minimum pin length for auto confirmation Change-Id: I3bb5558a40769dbdc21765292271c463378233d7 Signed-off-by: Mesquita Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/widget/LockPatternUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 842404753fb8..ff58e9b992f2 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -128,7 +128,7 @@ public class LockPatternUtils { public static final int PIN_LENGTH_UNAVAILABLE = -1; // This is the minimum pin length at which auto confirmation is supported - public static final int MIN_AUTO_PIN_REQUIREMENT_LENGTH = 6; + public static final int MIN_AUTO_PIN_REQUIREMENT_LENGTH = 4; /** * Header used for the encryption and decryption of the device credential for From f65f4b77c1647e3ccc8f35e0cc3fcd410248a546 Mon Sep 17 00:00:00 2001 From: Adithya Date: Tue, 14 Sep 2021 13:38:47 +0530 Subject: [PATCH 0024/1315] SettingsLib: Update LTE+ icon as per new Silk design * according to 084c13c8216f6a899cd3eda04fc1d7acff3d1248 Change-Id: I6b2947441217ddc2215d1dcb7b2e659f9bd82743 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_lte_plus_mobiledata.xml | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata.xml b/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata.xml index e5cdff33fe98..d1f4b6fd818b 100644 --- a/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata.xml +++ b/packages/SettingsLib/res/drawable/ic_lte_plus_mobiledata.xml @@ -14,23 +14,20 @@ limitations under the License. --> - - + android:width="31dp" + android:height="16dp" + android:viewportWidth="31.0" + android:viewportHeight="16.0"> + android:fillColor="#FF000000" + android:pathData="M2,13V2.98h1.53v8.57H8.3V13H2z"/> + android:fillColor="#FF000000" + android:pathData="M11.24,13V4.43H8.19V2.98h7.63v1.46h-3.05V13H11.24z"/> + android:fillColor="#FF000000" + android:pathData="M17.41,13V2.98h6.36v1.46h-4.83v2.65h4.4v1.46h-4.4v3.01h4.83V13H17.41z"/> + android:fillColor="#FF000000" + android:pathData="M28.72 5.24 28.72 2.89 27.42 2.89 27.42 5.24 25.07 5.24 25.07 6.54 27.42 6.54 27.42 8.89 28.72 8.89 28.72 6.54 31.07 6.54 31.07 5.24Z"/> From 56f728b767a675881116fa9ada852b524b03c9dd Mon Sep 17 00:00:00 2001 From: TH779 Date: Thu, 18 Nov 2021 00:10:54 +0800 Subject: [PATCH 0025/1315] SettingsLib: Update 4G+ icon to Silk design as well * The plus icon ref from I6b2947441217ddc2215d1dcb7b2e659f9bd82743. Co-authored-by: Adithya Signed-off-by: TH779 Change-Id: I28004d62013be13dfd0825cc53049019f0e06966 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_4g_plus_mobiledata.xml | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml index 1d048ae44049..1272ea7a30e1 100644 --- a/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml +++ b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml @@ -14,20 +14,17 @@ limitations under the License. --> - - + android:width="27dp" + android:height="16dp" + android:viewportWidth="27.0" + android:viewportHeight="16.0"> + android:fillColor="#FF000000" + android:pathData="M2,10.98V9.81l4.28,-6.83h1.6v6.59h1.19v1.41H7.88V13H6.4v-2.02H2zM3.68,9.57H6.4V5.33H6.31L3.68,9.57z"/> + android:fillColor="#FF000000" + android:pathData="M15,13.22c-0.88,0 -1.66,-0.21 -2.34,-0.64c-0.67,-0.44 -1.2,-1.05 -1.6,-1.83c-0.38,-0.79 -0.57,-1.71 -0.57,-2.76c0,-1.05 0.21,-1.96 0.62,-2.74c0.41,-0.79 0.97,-1.4 1.67,-1.83c0.7,-0.44 1.5,-0.66 2.39,-0.66c1.09,0 2.01,0.25 2.74,0.76c0.75,0.5 1.22,1.21 1.41,2.11l-1.47,0.39c-0.17,-0.56 -0.48,-1 -0.94,-1.32c-0.45,-0.33 -1.03,-0.49 -1.75,-0.49c-0.57,0 -1.09,0.14 -1.57,0.43c-0.48,0.29 -0.85,0.71 -1.13,1.27c-0.28,0.56 -0.42,1.25 -0.42,2.07c0,0.81 0.14,1.5 0.41,2.07c0.28,0.56 0.65,0.98 1.11,1.27c0.47,0.29 0.98,0.43 1.54,0.43c0.57,0 1.06,-0.11 1.47,-0.34c0.42,-0.23 0.75,-0.55 0.99,-0.94c0.25,-0.4 0.41,-0.85 0.46,-1.36h-2.88V7.79h4.37c0,0.87 0,1.74 0,2.6c0,0.87 0,1.74 0,2.6h-1.41v-1.4h-0.08c-0.28,0.49 -0.67,0.88 -1.18,1.18C16.34,13.07 15.73,13.22 15,13.22z"/> + android:fillColor="#FF000000" + android:pathData="M24.62 5.24 24.62 2.89 23.32 2.89 23.32 5.24 20.97 5.24 20.97 6.54 23.32 6.54 23.32 8.89 24.62 8.89 24.62 6.54 26.97 6.54 26.97 5.24Z"/> From 50286258d2fcefa9c7ffa0b7a39d882c89be09aa Mon Sep 17 00:00:00 2001 From: Adithya R Date: Wed, 22 Mar 2023 07:10:45 +0530 Subject: [PATCH 0026/1315] SettingsLib: Update 5G+ icon to Silk design * according to 084c13c Change-Id: I8cb611f5e54efd9c1293dabda50cd234e76e7375 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_5g_plus_mobiledata.xml | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata.xml index 10bbcc7b3737..60de42cfdc95 100644 --- a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata.xml +++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata.xml @@ -1,5 +1,6 @@ - - - - - - - - - + android:width="27dp" + android:height="16dp" + android:viewportWidth="27" + android:viewportHeight="16"> + + + From b639cd0dee60d1f35eb81157886aee8d904b1329 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Wed, 11 Jan 2023 08:29:25 +0800 Subject: [PATCH 0027/1315] SettingsLib: Make IllustrationPreference bg protection transparent * similar to lottie animations seen on OEMS like oplus and samsung Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/protection_background.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SettingsLib/IllustrationPreference/res/drawable/protection_background.xml b/packages/SettingsLib/IllustrationPreference/res/drawable/protection_background.xml index a027f287a0aa..2a1f38507bdf 100644 --- a/packages/SettingsLib/IllustrationPreference/res/drawable/protection_background.xml +++ b/packages/SettingsLib/IllustrationPreference/res/drawable/protection_background.xml @@ -18,7 +18,7 @@ - + From f291f2827a774a6168d1ff0a04608f05f82c8fc1 Mon Sep 17 00:00:00 2001 From: Henrique Silva Date: Sat, 25 Jan 2020 17:15:30 -0500 Subject: [PATCH 0028/1315] SettingsLib: Don't show system overlays on apps list **DU Edits** - Removed the need to add individual overlays to an array list and just checked whether is an RRO using the ApplicationInfo flag that Google added. https://github.com/DirtyUnicorns/android_frameworks_base/commit/946e32f39436eaeb1d51a13b1bc6df0a282870cb Work smarter not harder, just 1 line :-) Change-Id: I7e45d0ee59bd973245ce5e6b7c54ad0ad0e634f2 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/settingslib/applications/ApplicationsState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 38dfdfae12cc..9b07cbbabadb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -2126,7 +2126,7 @@ public void init() { @Override public boolean filterApp(AppEntry entry) { - return true; + return !hasFlag(entry.info.privateFlags, ApplicationInfo.PRIVATE_FLAG_IS_RESOURCE_OVERLAY); } }; From dcfe138fc7a69fb035d94bd3cf711489a64a44be Mon Sep 17 00:00:00 2001 From: Blaster4385 Date: Thu, 13 Oct 2022 07:10:30 +0800 Subject: [PATCH 0029/1315] SettingsLib: Change collapse mode to scale * Again, IDK who thought that fade would look better. @neobuddy89: * titleCollapseMode re-added for A15. * Updated for A16 QPR2. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/layout-v31/collapsing_toolbar_content_layout.xml | 1 + .../res/values-v36/styles_expressive.xml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/collapsing_toolbar_content_layout.xml index 9848749e2d1c..78c24ee250dc 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/collapsing_toolbar_content_layout.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/collapsing_toolbar_content_layout.xml @@ -33,6 +33,7 @@ android:layout_width="match_parent" android:layout_height="@dimen/settingslib_toolbar_layout_height" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:titleCollapseMode="scale" app:toolbarId="@id/action_bar" style="@style/CollapsingToolbarLayoutStyle.SettingsLib"> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml index 344b336dc675..ccfcaf929e18 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml @@ -28,7 +28,7 @@ 50 @color/settingslib_materialColorOnSurface @color/settingslib_materialColorOnSurface - fade + scale true - \ No newline at end of file + From da3302107b0c76a4714113d4c8e2d299c8e465aa Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Wed, 25 Oct 2023 15:21:47 +0800 Subject: [PATCH 0030/1315] SettingsLib: Animate the UsageProgressBarPreference Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../widget/UsageProgressBarPreference.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java index bb2c56baa910..7aa1fc3afddc 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java +++ b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java @@ -16,6 +16,7 @@ package com.android.settingslib.widget; +import android.animation.ValueAnimator; import android.content.Context; import android.text.SpannableString; import android.text.Spanned; @@ -203,7 +204,7 @@ public void onBindViewHolder(PreferenceViewHolder holder) { progressBar.setIndeterminate(true); } else { progressBar.setIndeterminate(false); - progressBar.setProgress(mPercent); + animateBatteryLevel(progressBar, 0, mPercent); } final FrameLayout customLayout = (FrameLayout) holder.findViewById(R.id.custom_content); @@ -234,4 +235,17 @@ private CharSequence enlargeFontOfNumber(CharSequence summary) { } return summary; } + + private void animateBatteryLevel(final ProgressBar progressbar, final int startValue, final int endValue) { + final ValueAnimator animator = ValueAnimator.ofInt(startValue, endValue); + animator.setDuration(1000); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int animatedValue = (int) animation.getAnimatedValue(); + progressbar.setProgress(animatedValue); + } + }); + animator.start(); + } } From a4c8c5901967cb76604e3220e21b5799ad3f8df8 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 7 Nov 2023 14:06:29 +0800 Subject: [PATCH 0031/1315] SettingsLib: UsageProgressBarPreference: Fix multiple NPEs 11-07 14:02:52.728 14175 14175 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference 11-07 14:02:52.728 14175 14175 E AndroidRuntime: at com.android.settingslib.widget.UsageProgressBarPreference.onBindViewHolder(UsageProgressBarPreference.java:164) 11-07 14:02:52.728 14175 14175 E AndroidRuntime: at androidx.preference.PreferenceGroupAdapter.onBindViewHolder(PreferenceGroupAdapter.java:420) neobuddy89: Adapted for A15. Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../widget/UsageProgressBarPreference.java | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java index 7aa1fc3afddc..c2d45e2119d6 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java +++ b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java @@ -180,41 +180,49 @@ public void onBindViewHolder(PreferenceViewHolder holder) { holder.setDividerAllowedBelow(false); final TextView usageSummary = (TextView) holder.findViewById(R.id.usage_summary); - usageSummary.setText(enlargeFontOfNumber(mUsageSummary)); + if (usageSummary != null && mUsageSummary != null) { + usageSummary.setText(enlargeFontOfNumber(mUsageSummary)); + } final TextView totalSummary = (TextView) holder.findViewById(R.id.total_summary); - if (mTotalSummary != null) { + if (totalSummary != null && mTotalSummary != null) { totalSummary.setText(mTotalSummary); } final TextView bottomSummary = (TextView) holder.findViewById(R.id.bottom_summary); - if (TextUtils.isEmpty(mBottomSummary)) { - bottomSummary.setVisibility(View.GONE); - } else { - bottomSummary.setVisibility(View.VISIBLE); - bottomSummary.setMovementMethod(LinkMovementMethod.getInstance()); - bottomSummary.setText(mBottomSummary); - if (!TextUtils.isEmpty(mBottomSummaryContentDescription)) { - bottomSummary.setContentDescription(mBottomSummaryContentDescription); + if (bottomSummary != null) { + if (TextUtils.isEmpty(mBottomSummary)) { + bottomSummary.setVisibility(View.GONE); + } else { + bottomSummary.setVisibility(View.VISIBLE); + bottomSummary.setMovementMethod(LinkMovementMethod.getInstance()); + bottomSummary.setText(mBottomSummary); + if (!TextUtils.isEmpty(mBottomSummaryContentDescription)) { + bottomSummary.setContentDescription(mBottomSummaryContentDescription); + } } } final ProgressBar progressBar = (ProgressBar) holder.findViewById(android.R.id.progress); - if (mPercent < 0) { - progressBar.setIndeterminate(true); - } else { - progressBar.setIndeterminate(false); - animateBatteryLevel(progressBar, 0, mPercent); + if (progressBar != null) { + if (mPercent < 0) { + progressBar.setIndeterminate(true); + } else { + progressBar.setIndeterminate(false); + animateBatteryLevel(progressBar, 0, mPercent); + } } final FrameLayout customLayout = (FrameLayout) holder.findViewById(R.id.custom_content); - if (mCustomImageView == null) { - customLayout.removeAllViews(); - customLayout.setVisibility(View.GONE); - } else { - customLayout.removeAllViews(); - customLayout.addView(mCustomImageView); - customLayout.setVisibility(View.VISIBLE); + if (customLayout != null) { + if (mCustomImageView == null) { + customLayout.removeAllViews(); + customLayout.setVisibility(View.GONE); + } else { + customLayout.removeAllViews(); + customLayout.addView(mCustomImageView); + customLayout.setVisibility(View.VISIBLE); + } } } From 02a21803d49deac64b18ffdfee76c612cef28593 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Wed, 15 Nov 2023 17:50:14 +0800 Subject: [PATCH 0032/1315] SettingsLib: Fix crash when checking emergency gesture state 11-15 08:19:37.346 13652 13652 E AndroidRuntime: Caused by: java.lang.IllegalArgumentException: Unknown authority com.android.emergency.gesture 11-15 08:19:37.346 13652 13652 E AndroidRuntime: at android.content.ContentResolver.call(ContentResolver.java:2463) 11-15 08:19:37.346 13652 13652 E AndroidRuntime: at android.content.ContentResolver.call(ContentResolver.java:2446) 11-15 08:19:37.346 13652 13652 E AndroidRuntime: at com.android.settingslib.emergencynumber.EmergencyNumberUtils.getEmergencyGestureEnabled(EmergencyNumberUtils.java:150) Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../emergencynumber/EmergencyNumberUtils.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java b/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java index 60ec91508930..7dbde286a66b 100644 --- a/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java +++ b/packages/SettingsLib/EmergencyNumber/src/com/android/settingslib/emergencynumber/EmergencyNumberUtils.java @@ -147,11 +147,16 @@ public void setEmergencySoundEnabled(boolean enabled) { * Whether or not emergency gesture is enabled. */ public boolean getEmergencyGestureEnabled() { - final Bundle bundle = mContext.getContentResolver().call( - EMERGENCY_NUMBER_OVERRIDE_AUTHORITY, - METHOD_NAME_GET_EMERGENCY_GESTURE_ENABLED, null /* args */, null /* bundle */); - return bundle == null ? true : bundle.getInt(EMERGENCY_SETTING_VALUE, EMERGENCY_SETTING_ON) - == EMERGENCY_SETTING_ON; + try { + final Bundle bundle = mContext.getContentResolver().call( + EMERGENCY_NUMBER_OVERRIDE_AUTHORITY, + METHOD_NAME_GET_EMERGENCY_GESTURE_ENABLED, null /* args */, null /* bundle */); + return bundle != null && bundle.getInt(EMERGENCY_SETTING_VALUE, EMERGENCY_SETTING_ON) + == EMERGENCY_SETTING_ON; + } catch (Exception e) { + Log.e("EmergencyGesture", "Failed to get emergency gesture status", e); + return false; + } } /** From 95d3687d7c58554557529da0dd03ac30331b2cc1 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 6 Jul 2025 02:20:32 +0530 Subject: [PATCH 0033/1315] SettingsLib: Start collapsing appbar as expanded by default Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../settingslib_expressive_collapsing_toolbar_content_layout.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v36/settingslib_expressive_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v36/settingslib_expressive_collapsing_toolbar_content_layout.xml index 092f76b7d61a..9466eb6509f1 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v36/settingslib_expressive_collapsing_toolbar_content_layout.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v36/settingslib_expressive_collapsing_toolbar_content_layout.xml @@ -27,7 +27,6 @@ android:outlineSpotShadowColor="@android:color/transparent" android:background="@android:color/transparent" android:touchscreenBlocksFocus="false" - app:expanded="false" android:theme="@style/SettingsLibTheme.CollapsingToolbar.Expressive"> Date: Thu, 21 Nov 2024 13:00:07 +0800 Subject: [PATCH 0034/1315] SettingsLib: Use legacy material colors for settings' surface container * the monet engine background color changes doesnt seem to be noticeable with the latest material surface colors Change-Id: I0c449d9c8464aa8fb5bef9fe77130c476b2b755e Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SettingsLib/SettingsTheme/res/values-night-v36/colors.xml | 1 + packages/SettingsLib/SettingsTheme/res/values-v36/colors.xml | 3 ++- packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml | 2 +- .../SettingsTheme/res/values-v36/themes_expressive.xml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v36/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v36/colors.xml index e31e80176625..26b652a7594d 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-night-v36/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v36/colors.xml @@ -46,4 +46,5 @@ @color/settingslib_materialColorSurfaceVariant @color/settingslib_materialColorPrimary + @android:color/system_neutral1_900 diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/colors.xml index b1b37b12c572..b6a103741a0f 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/colors.xml @@ -54,4 +54,5 @@ @color/settingslib_materialColorOnPrimaryContainer @color/settingslib_materialColorOnPrimaryContainer - \ No newline at end of file + @android:color/system_neutral1_50 + diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml index 54bd069f2fc3..724def5545f6 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v36/themes.xml @@ -18,7 +18,7 @@ - - + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_colors.xml b/packages/SystemUI/res/values/matrixx_colors.xml index e8fa2c2c5788..144d78ea9074 100644 --- a/packages/SystemUI/res/values/matrixx_colors.xml +++ b/packages/SystemUI/res/values/matrixx_colors.xml @@ -7,4 +7,10 @@ #ff000000 + + + #40000000 + #89000000 + @android:color/system_accent2_200 + @android:color/system_accent2_700 diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index e57842697898..29438a5a212b 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -24,4 +24,41 @@ 280dp 0dp + + + 1.5dp + 24.0dp + 8.0dp + 4.0dp + 76.0dp + 20.0dp + 104.0dp + 2.0dp + 4.0dp + 20.0dp + 20.0dp + 12.0dp + 1.0dp + 5.0dp + 16.0sp + 14.0sp + 18.0sp + 0.5dp + 0.5dp + 0.5dp + 24.0dp + 28.0dp + 16.0dp + 10.0dp + 20.0dp + 3.0dp + 6.0dp + 10.0dp + 0.01 + 48.0dp + 36.0dp + 1.0dp + 1.0dp + 52.0dp + 45.0dp diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 5646416955e4..108c14a6d6ab 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -52,4 +52,15 @@ %2$s • Turbo Charging (%1$s until full) %s • Turbo Charging + + + Next alarm at %s + Page %1$d of %2$d + %1$s is on + %1$s, %2$s + No title + At a glance + QR code + EEEMMMd + View diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 48a8119b8430..e2e34009f56e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -16,10 +16,13 @@ package com.android.systemui.dagger; +import android.app.AlarmManager; import android.app.INotificationManager; import android.app.Service; import android.app.backup.BackupManager; +import android.content.ComponentName; import android.content.Context; +import android.os.Handler; import android.service.dreams.IDreamManager; import android.view.Display; @@ -40,6 +43,7 @@ import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; import com.android.systemui.authentication.AuthenticationModule; +import com.android.systemui.res.R; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.biometrics.FingerprintReEnrollNotification; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; @@ -49,6 +53,7 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractorModule; import com.android.systemui.bouncer.ui.BouncerViewModule; import com.android.systemui.brightness.dagger.ScreenBrightnessModule; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.common.data.CommonDataLayerModule; @@ -60,6 +65,7 @@ import com.android.systemui.compose.ComposeModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.SystemUser; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -90,6 +96,7 @@ import com.android.systemui.log.table.impl.TableLogBufferModule; import com.android.systemui.lowlight.dagger.LowLightModule; import com.android.systemui.lowlightclock.dagger.LowLightClockModule; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.mediaprojection.MediaProjectionModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule; import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule; @@ -161,11 +168,14 @@ import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; import com.android.systemui.statusbar.policy.DeviceStateRotationLockSettingController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.policy.NextAlarmController; +import com.android.systemui.statusbar.policy.NextAlarmControllerImpl; import com.android.systemui.statusbar.policy.PolicyModule; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor; import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsModule; import com.android.systemui.statusbar.ui.binder.StatusBarViewBinderModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; @@ -179,6 +189,7 @@ import com.android.systemui.user.UserModule; import com.android.systemui.user.domain.UserDomainLayerModule; import com.android.systemui.util.EventLogModule; +import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.SysUIConcurrencyModule; import com.android.systemui.util.dagger.UtilModule; import com.android.systemui.util.kotlin.SysUICoroutinesModule; @@ -190,6 +201,13 @@ import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; +import com.google.android.systemui.smartspace.BcSmartspaceDataProvider; +import com.google.android.systemui.smartspace.DateSmartspaceDataProvider; +import com.google.android.systemui.smartspace.KeyguardMediaViewController; +import com.google.android.systemui.smartspace.KeyguardZenAlarmViewController; +import com.google.android.systemui.smartspace.WeatherSmartspaceDataProvider; +import com.google.android.systemui.smartspace.dagger.SmartspaceStartableModule; + import dagger.Binds; import dagger.BindsOptionalOf; import dagger.Module; @@ -198,8 +216,6 @@ import dagger.multibindings.IntoMap; import dagger.multibindings.Multibinds; -import kotlinx.coroutines.CoroutineScope; - import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -208,6 +224,9 @@ import javax.inject.Named; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** * A dagger module for injecting components of System UI that are required by System UI. * @@ -292,6 +311,7 @@ SettingsUtilModule.class, SmartRepliesInflationModule.class, SmartspaceModule.class, + SmartspaceStartableModule.class, StatusBarEventsModule.class, StatusBarModule.class, StatusBarChipsModule.class, @@ -396,11 +416,15 @@ static BackupManager provideBackupManager(@Application Context context) { @BindsOptionalOf @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN) - abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin(); + abstract BcSmartspaceDataPlugin optionalDateSmartspaceDataPlugin(); + + @BindsOptionalOf + @Named(SmartspaceModule.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) + abstract BcSmartspaceDataPlugin optionalGlanceableHubSmartspaceDataPlugin(); @BindsOptionalOf @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN) - abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin(); + abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceDataPlugin(); @BindsOptionalOf abstract Recents optionalRecents(); @@ -518,4 +542,85 @@ static SceneDataSourceDelegator providesSceneDataSourceDelegator( static SettingsProxy.CurrentUserIdProvider provideCurrentUserId(UserTracker userTracker) { return userTracker::getUserId; } + + @Provides + @SysUISingleton + static KeyguardZenAlarmViewController provideKeyguardZenAlarmViewController( + Context context, + @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN) BcSmartspaceDataPlugin datePlugin, + ZenModeController zenModeController, + ZenModeInteractor zenModeInteractor, + AlarmManager alarmManager, + NextAlarmControllerImpl nextAlarmController, + @Main Handler handler, + @Application CoroutineScope applicationScope, + @Background CoroutineDispatcher bgDispatcher) { + KeyguardZenAlarmViewController controller = new KeyguardZenAlarmViewController( + context, + datePlugin, + zenModeController, + zenModeInteractor, + alarmManager, + nextAlarmController, + handler, + applicationScope, + bgDispatcher + ); + controller.alarmImage = context.getResources().getDrawable(R.drawable.ic_access_alarms_big, null); + return controller; + } + + @Provides + @SysUISingleton + static KeyguardMediaViewController provideKeyguardMediaViewController( + Context context, + NotificationMediaManager mediaManager, + @Named(SmartspaceModule.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) + BcSmartspaceDataPlugin plugin, + UserTracker userTracker, + @Main DelayableExecutor uiExecutor) { + KeyguardMediaViewController controller = new KeyguardMediaViewController( + context, mediaManager, plugin, userTracker, uiExecutor); + controller.mediaComponent = new ComponentName(context, KeyguardMediaViewController.class); + return controller; + } + + @Provides + @SysUISingleton + static BcSmartspaceDataPlugin provideBcSmartspaceDataPlugin() { + return new BcSmartspaceDataProvider(); + } + + @Provides + @SysUISingleton + @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN) + static BcSmartspaceDataPlugin provideDateSmartspaceDataPlugin() { + return new DateSmartspaceDataProvider(); + } + + @Provides + @SysUISingleton + @Named(SmartspaceModule.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) + static BcSmartspaceDataPlugin provideGlanceableHubSmartspaceDataPlugin() { + return new BcSmartspaceDataProvider(); + } + + @Provides + @SysUISingleton + @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN) + static BcSmartspaceDataPlugin provideWeatherSmartspaceDataPlugin() { + return new WeatherSmartspaceDataProvider(); + } + + @Provides + @SysUISingleton + static DateSmartspaceDataProvider provideDateSmartspaceDataProvider() { + return new DateSmartspaceDataProvider(); + } + + @Provides + @SysUISingleton + static WeatherSmartspaceDataProvider provideWeatherSmartspaceDataProvider() { + return new WeatherSmartspaceDataProvider(); + } } diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcNextAlarmData.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcNextAlarmData.java new file mode 100644 index 000000000000..04ff4b2cf9f8 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcNextAlarmData.java @@ -0,0 +1,11 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.content.Intent; +import android.graphics.drawable.Drawable; + +public final class BcNextAlarmData { + public static final SmartspaceAction SHOW_ALARMS_ACTION = new SmartspaceAction.Builder("nextAlarmId", "Next alarm").setIntent(new Intent("android.intent.action.SHOW_ALARMS")).build(); + public String mDescription; + public Drawable mImage; +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java new file mode 100644 index 000000000000..f47d7c21c4a4 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java @@ -0,0 +1,233 @@ +package com.google.android.systemui.smartspace; + +import android.app.PendingIntent; +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceTargetEvent; +import android.app.smartspace.uitemplatedata.TapAction; +import android.content.ActivityNotFoundException; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLogger; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; +import java.util.Map; + +public abstract class BcSmartSpaceUtil { + public static final Map FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP; + public static FalsingManager sFalsingManager; + + public static class InteractionHandler implements RemoteViews.InteractionHandler { + public final BcSmartspaceCardLoggingInfo loggingInfo; + public final BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier; + public final SmartspaceTarget target; + public final SmartspaceAction action; + + public InteractionHandler( + BcSmartspaceCardLoggingInfo loggingInfo, + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, + SmartspaceTarget target, + SmartspaceAction action) { + this.loggingInfo = loggingInfo; + this.eventNotifier = eventNotifier; + this.target = target; + this.action = action; + } + + @Override + public final boolean onInteraction(View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse remoteResponse) { + BcSmartspaceDataPlugin.IntentStarter intentStarter = BcSmartSpaceUtil.getIntentStarter(eventNotifier, "BcSmartspaceRemoteViewsCard"); + if (pendingIntent != null) { + BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_CLICK, loggingInfo); + if (eventNotifier != null) { + eventNotifier.notifySmartspaceEvent(new SmartspaceTargetEvent.Builder(1).setSmartspaceTarget(target).setSmartspaceActionId(action.getId()).build()); + } + intentStarter.startPendingIntent(view, pendingIntent, false); + } + return true; + } + } + + public static class DefaultIntentStarter implements BcSmartspaceDataPlugin.IntentStarter { + public final String tag; + + public DefaultIntentStarter(String tag) { + this.tag = tag; + } + + @Override + public final void startIntent(View view, Intent intent, boolean showOnLockscreen) { + try { + view.getContext().startActivity(intent); + } catch (ActivityNotFoundException | NullPointerException | SecurityException e) { + Log.e(tag, "Cannot invoke smartspace intent", e); + } + } + + @Override + public final void startPendingIntent(View view, PendingIntent pendingIntent, boolean showOnLockscreen) { + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.e(tag, "Cannot invoke canceled smartspace intent", e); + } + } + } + + static { + FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP = Map.ofEntries(Map.entry(-1, Integer.valueOf(R.layout.smartspace_card_combination)), Map.entry(-2, Integer.valueOf(R.layout.smartspace_card_combination_at_store)), Map.entry(3, Integer.valueOf(R.layout.smartspace_card_generic_landscape_image)), Map.entry(18, Integer.valueOf(R.layout.smartspace_card_generic_landscape_image)), Map.entry(4, Integer.valueOf(R.layout.smartspace_card_flight)), Map.entry(14, Integer.valueOf(R.layout.smartspace_card_loyalty)), Map.entry(13, Integer.valueOf(R.layout.smartspace_card_shopping_list)), Map.entry(9, Integer.valueOf(R.layout.smartspace_card_sports)), Map.entry(10, Integer.valueOf(R.layout.smartspace_card_weather_forecast)), Map.entry(30, Integer.valueOf(R.layout.smartspace_card_doorbell)), Map.entry(20, Integer.valueOf(R.layout.smartspace_card_doorbell))); + } + + public static String getDimensionRatio(Bundle bundle) { + if (!bundle.containsKey("imageRatioWidth") || !bundle.containsKey("imageRatioHeight")) { + return null; + } + int width = bundle.getInt("imageRatioWidth"); + int height = bundle.getInt("imageRatioHeight"); + if (width <= 0 || height <= 0) { + return null; + } + return width + ":" + height; + } + + public static int getFeatureType(SmartspaceTarget target) { + List actionChips = target.getActionChips(); + int featureType = target.getFeatureType(); + return (actionChips == null || actionChips.isEmpty()) ? featureType : (featureType == 13 && actionChips.size() == 1) ? -2 : -1; + } + + public static Drawable getIconDrawableWithCustomSize(Icon icon, Context context, int size) { + if (icon == null) { + return null; + } + Drawable drawable = (icon.getType() == 1 || icon.getType() == 5) ? new BitmapDrawable(context.getResources(), icon.getBitmap()) : icon.loadDrawable(context); + if (drawable != null) { + drawable.setBounds(0, 0, size, size); + } + return drawable; + } + + public static BcSmartspaceDataPlugin.IntentStarter getIntentStarter(BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, String tag) { + BcSmartspaceDataPlugin.IntentStarter intentStarter = eventNotifier != null ? eventNotifier.getIntentStarter() : null; + if (intentStarter != null) { + return intentStarter; + } + return new DefaultIntentStarter(tag); + } + + public static int getLoggingDisplaySurface(String uiSurface, float dozeAmount) { + if (uiSurface == null) { + return 0; + } + + switch (uiSurface) { + case BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN: + return 1; + case BcSmartspaceDataPlugin.UI_SURFACE_DREAM: + return 5; + case BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD: + if (dozeAmount == 1.0f) { + return 3; + } else if (dozeAmount == 0.0f) { + return 2; + } else { + return -1; + } + default: + return 0; + } + } + + public static Intent getOpenCalendarIntent() { + return new Intent("android.intent.action.VIEW").setData(ContentUris.appendId(CalendarContract.CONTENT_URI.buildUpon().appendPath("time"), System.currentTimeMillis()).build()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + + public static void setOnClickListener(View view, SmartspaceTarget target, SmartspaceAction action, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, String tag, BcSmartspaceCardLoggingInfo loggingInfo, int index) { + if (view == null || action == null) { + Log.e(tag, "No tap action can be set up"); + return; + } + + boolean isNoIntent = action.getIntent() == null && action.getPendingIntent() == null; + boolean showOnLockscreen = action.getExtras() != null && action.getExtras().getBoolean("show_on_lockscreen"); + BcSmartspaceDataPlugin.IntentStarter intentStarter = getIntentStarter(eventNotifier, tag); + + view.setOnClickListener(v -> { + if (sFalsingManager != null && sFalsingManager.isFalseTap(1)) { + return; + } + + if (loggingInfo != null) { + if (loggingInfo.mSubcardInfo != null) { + loggingInfo.mSubcardInfo.mClickedSubcardIndex = index; + } + BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_CLICK, loggingInfo); + } + + if (!isNoIntent) { + intentStarter.startFromAction(action, v, showOnLockscreen); + } + + if (eventNotifier == null) { + Log.w(tag, "Cannot notify target interaction smartspace event: event notifier null."); + } else { + eventNotifier.notifySmartspaceEvent(new SmartspaceTargetEvent.Builder(SmartspaceTargetEvent.EVENT_TARGET_INTERACTION) + .setSmartspaceTarget(target) + .setSmartspaceActionId(action.getId()) + .build()); + } + }); + } + + public static void setOnClickListener(View view, SmartspaceTarget target, TapAction tapAction, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, String tag, BcSmartspaceCardLoggingInfo loggingInfo, int index) { + if (view == null || tapAction == null) { + Log.e(tag, "No tap action can be set up"); + return; + } + + boolean showOnLockscreen = tapAction.shouldShowOnLockscreen(); + + view.setOnClickListener(v -> { + if (sFalsingManager != null && sFalsingManager.isFalseTap(1)) { + return; + } + + if (loggingInfo != null) { + if (loggingInfo.mSubcardInfo != null) { + loggingInfo.mSubcardInfo.mClickedSubcardIndex = index; + } + BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_CLICK, loggingInfo); + } + + BcSmartspaceDataPlugin.IntentStarter intentStarter = getIntentStarter(eventNotifier, tag); + if (tapAction.getIntent() != null || tapAction.getPendingIntent() != null) { + intentStarter.startFromAction(tapAction, v, showOnLockscreen); + } + + if (eventNotifier == null) { + Log.w(tag, "Cannot notify target interaction smartspace event: event notifier null."); + } else { + eventNotifier.notifySmartspaceEvent(new SmartspaceTargetEvent.Builder(SmartspaceTargetEvent.EVENT_TARGET_INTERACTION) + .setSmartspaceTarget(target) + .setSmartspaceActionId(tapAction.getId().toString()) + .build()); + } + }); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCard.java new file mode 100644 index 000000000000..f17af784dd26 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCard.java @@ -0,0 +1,463 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.PathInterpolator; +import android.widget.TextView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import com.android.app.animation.Interpolators; +import com.android.launcher3.icons.GraphicsUtils; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardMetadataLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceSubcardLoggingInfo; +import com.google.android.systemui.smartspace.utils.ContentDescriptionUtil; +import com.android.systemui.res.R; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/* compiled from: go/retraceme af8e0b46c0cb0ee2c99e9b6d0c434e5c0b686fd9230eaab7fb9a40e3a9d0cf6f */ +/* loaded from: classes2.dex */ +public class BcSmartspaceCard extends ConstraintLayout implements SmartspaceCard { + public final DoubleShadowIconDrawable mBaseActionIconDrawable; + public Rect mBaseActionIconSubtitleHitRect; + public DoubleShadowTextView mBaseActionIconSubtitleView; + public float mDozeAmount; + public BcSmartspaceDataPlugin.SmartspaceEventNotifier mEventNotifier; + public final DoubleShadowIconDrawable mIconDrawable; + public int mIconTintColor; + public BcSmartspaceCardLoggingInfo mLoggingInfo; + public BcSmartspaceCardSecondary mSecondaryCard; + public ViewGroup mSecondaryCardGroup; + public TextView mSubtitleTextView; + public SmartspaceTarget mTarget; + public ViewGroup mTextGroup; + public TextView mTitleTextView; + public boolean mTouchDelegateIsDirty; + public String mUiSurface; + public boolean mUsePageIndicatorUi; + public boolean mValidSecondaryCard; + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + public BcSmartspaceCard(Context context) { + this(context, null); + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + public static int getClickedIndex(BcSmartspaceCardLoggingInfo bcSmartspaceCardLoggingInfo, int i) { + List list; + BcSmartspaceSubcardLoggingInfo bcSmartspaceSubcardLoggingInfo = bcSmartspaceCardLoggingInfo.mSubcardInfo; + if (bcSmartspaceSubcardLoggingInfo != null && (list = bcSmartspaceSubcardLoggingInfo.mSubcards) != null) { + int i2 = 0; + while (true) { + ArrayList arrayList = (ArrayList) list; + if (i2 >= arrayList.size()) { + break; + } + BcSmartspaceCardMetadataLoggingInfo bcSmartspaceCardMetadataLoggingInfo = (BcSmartspaceCardMetadataLoggingInfo) arrayList.get(i2); + if (bcSmartspaceCardMetadataLoggingInfo != null && bcSmartspaceCardMetadataLoggingInfo.mCardTypeId == i) { + return i2 + 1; + } + i2++; + } + } + return 0; + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // com.google.android.systemui.smartspace.SmartspaceCard + public final void bindData(SmartspaceTarget smartspaceTarget, BcSmartspaceDataPlugin.SmartspaceEventNotifier smartspaceEventNotifier, BcSmartspaceCardLoggingInfo bcSmartspaceCardLoggingInfo, boolean z) { + SmartspaceAction smartspaceAction; + BcSmartspaceCardLoggingInfo bcSmartspaceCardLoggingInfo2 = bcSmartspaceCardLoggingInfo; + Drawable drawable = null; + this.mLoggingInfo = null; + this.mEventNotifier = null; + int i = 8; + BcSmartspaceTemplateDataUtils.updateVisibility(this.mSecondaryCardGroup, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(this.mBaseActionIconSubtitleView, 8); + this.mIconDrawable.mIconDrawable = null; + this.mBaseActionIconDrawable.mIconDrawable = null; + int i2 = 0; + setTitle(null, null, false); + setSubtitle(null, null, false); + setBaseActionIconSubtitle(null, null, null); + updateIconTint(); + setOnClickListener(null); + TextView textView = this.mTitleTextView; + if (textView != null) { + textView.setOnClickListener(null); + textView.setClickable(false); + } + TextView textView2 = this.mSubtitleTextView; + if (textView2 != null) { + textView2.setOnClickListener(null); + textView2.setClickable(false); + } + DoubleShadowTextView doubleShadowTextView = this.mBaseActionIconSubtitleView; + if (doubleShadowTextView != null) { + doubleShadowTextView.setOnClickListener(null); + doubleShadowTextView.setClickable(false); + } + this.mTarget = smartspaceTarget; + this.mEventNotifier = smartspaceEventNotifier; + SmartspaceAction headerAction = smartspaceTarget.getHeaderAction(); + SmartspaceAction baseAction = smartspaceTarget.getBaseAction(); + this.mLoggingInfo = bcSmartspaceCardLoggingInfo2; + this.mUsePageIndicatorUi = z; + this.mValidSecondaryCard = false; + ViewGroup viewGroup = this.mTextGroup; + if (viewGroup != null) { + viewGroup.setTranslationX(0.0f); + } + if (headerAction != null) { + BcSmartspaceCardSecondary bcSmartspaceCardSecondary = this.mSecondaryCard; + if (bcSmartspaceCardSecondary != null) { + bcSmartspaceCardSecondary.reset(smartspaceTarget.getSmartspaceTargetId()); + this.mValidSecondaryCard = this.mSecondaryCard.setSmartspaceActions(smartspaceTarget, this.mEventNotifier, bcSmartspaceCardLoggingInfo2); + } + ViewGroup viewGroup2 = this.mSecondaryCardGroup; + if (viewGroup2 != null) { + viewGroup2.setAlpha(1.0f); + } + ViewGroup viewGroup3 = this.mSecondaryCardGroup; + if (this.mDozeAmount != 1.0f && this.mValidSecondaryCard) { + i = 0; + } + BcSmartspaceTemplateDataUtils.updateVisibility(viewGroup3, i); + Icon icon = headerAction.getIcon(); + Context context = getContext(); + FalsingManager falsingManager = BcSmartSpaceUtil.sFalsingManager; + Drawable iconDrawableWithCustomSize = BcSmartSpaceUtil.getIconDrawableWithCustomSize(icon, context, context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size)); + boolean z2 = iconDrawableWithCustomSize != null; + this.mIconDrawable.setIcon(iconDrawableWithCustomSize); + CharSequence title = headerAction.getTitle(); + CharSequence subtitle = headerAction.getSubtitle(); + boolean z3 = smartspaceTarget.getFeatureType() == 1 || !TextUtils.isEmpty(title); + boolean isEmpty = TextUtils.isEmpty(subtitle); + boolean z4 = !isEmpty; + if (!z3) { + title = subtitle; + } + setTitle(title, headerAction.getContentDescription(), z3 != z4 && z2); + if (!z3 || isEmpty) { + subtitle = null; + } + setSubtitle(subtitle, headerAction.getContentDescription(), z2); + } + if (baseAction != null) { + int i3 = (baseAction.getExtras() == null || baseAction.getExtras().isEmpty()) ? -1 : baseAction.getExtras().getInt("subcardType", -1); + if (baseAction.getIcon() != null) { + Icon icon2 = baseAction.getIcon(); + Context context2 = getContext(); + FalsingManager falsingManager2 = BcSmartSpaceUtil.sFalsingManager; + drawable = BcSmartSpaceUtil.getIconDrawableWithCustomSize(icon2, context2, context2.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size)); + } + this.mBaseActionIconDrawable.setIcon(drawable); + setBaseActionIconSubtitle(baseAction.getSubtitle(), baseAction.getContentDescription(), this.mBaseActionIconDrawable); + if (i3 != -1) { + i2 = getClickedIndex(bcSmartspaceCardLoggingInfo2, i3); + } else { + Log.d("BcSmartspaceCard", "Subcard expected but missing type. loggingInfo=" + bcSmartspaceCardLoggingInfo2.toString() + ", baseAction=" + baseAction.toString()); + } + BcSmartSpaceUtil.setOnClickListener(this.mBaseActionIconSubtitleView, smartspaceTarget, baseAction, this.mEventNotifier, "BcSmartspaceCard", bcSmartspaceCardLoggingInfo, i2); + smartspaceAction = baseAction; + bcSmartspaceCardLoggingInfo2 = bcSmartspaceCardLoggingInfo; + } else { + smartspaceAction = baseAction; + } + updateIconTint(); + if (headerAction == null || (headerAction.getIntent() == null && headerAction.getPendingIntent() == null)) { + SmartspaceAction smartspaceAction2 = smartspaceAction; + if (smartspaceAction2 == null || (smartspaceAction2.getIntent() == null && smartspaceAction2.getPendingIntent() == null)) { + if (headerAction != null) { + BcSmartSpaceUtil.setOnClickListener(this, smartspaceTarget, headerAction, this.mEventNotifier, "BcSmartspaceCard", bcSmartspaceCardLoggingInfo, 0); + } + } else if (smartspaceAction2 != null) { + BcSmartSpaceUtil.setOnClickListener(this, smartspaceTarget, smartspaceAction2, this.mEventNotifier, "BcSmartspaceCard", bcSmartspaceCardLoggingInfo, 0); + } + } else { + if (smartspaceTarget.getFeatureType() == 1 && bcSmartspaceCardLoggingInfo2.mFeatureType == 39) { + getClickedIndex(bcSmartspaceCardLoggingInfo2, 1); + } + if (headerAction != null) { + BcSmartSpaceUtil.setOnClickListener(this, smartspaceTarget, headerAction, this.mEventNotifier, "BcSmartspaceCard", bcSmartspaceCardLoggingInfo2, 0); + } + } + ViewGroup viewGroup4 = this.mSecondaryCardGroup; + if (viewGroup4 == null) { + return; + } + ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) viewGroup4.getLayoutParams(); + if (BcSmartSpaceUtil.getFeatureType(smartspaceTarget) == -2) { + layoutParams.matchConstraintMaxWidth = (getWidth() * 3) / 4; + } else { + layoutParams.matchConstraintMaxWidth = getWidth() / 2; + } + this.mSecondaryCardGroup.setLayoutParams(layoutParams); + this.mTouchDelegateIsDirty = true; + } + + @Override + public final AccessibilityNodeInfo createAccessibilityNodeInfo() { + AccessibilityNodeInfo info = super.createAccessibilityNodeInfo(); + AccessibilityNodeInfoCompat.wrap(info).setRoleDescription(" "); + return info; + } + + @Override + public final BcSmartspaceCardLoggingInfo getLoggingInfo() { + if (mLoggingInfo != null) { + return mLoggingInfo; + } + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount)) + .setFeatureType(mTarget != null ? mTarget.getFeatureType() : 0) + .setUid(-1); + return new BcSmartspaceCardLoggingInfo(builder); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + setPaddingRelative(getResources().getDimensionPixelSize(R.dimen.non_remoteviews_card_padding_start), getPaddingTop(), getPaddingEnd(), getPaddingBottom()); + mTextGroup = (ViewGroup) findViewById(R.id.text_group); + mSecondaryCardGroup = (ViewGroup) findViewById(R.id.secondary_card_group); + mTitleTextView = (TextView) findViewById(R.id.title_text); + mSubtitleTextView = (TextView) findViewById(R.id.subtitle_text); + mBaseActionIconSubtitleView = findViewById(R.id.base_action_icon_subtitle); + if (mBaseActionIconSubtitleView != null) { + mBaseActionIconSubtitleHitRect = new Rect(); + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // androidx.constraintlayout.widget.ConstraintLayout, android.view.ViewGroup, android.view.View + public final void onLayout(boolean z, int i, int i2, int i3, int i4) { + super.onLayout(z, i, i2, i3, i4); + if (z || this.mTouchDelegateIsDirty) { + this.mTouchDelegateIsDirty = false; + setTouchDelegate(null); + DoubleShadowTextView doubleShadowTextView = this.mBaseActionIconSubtitleView; + if (doubleShadowTextView == null || doubleShadowTextView.getVisibility() != 0) { + return; + } + int dimensionPixelSize = (getResources().getDimensionPixelSize(R.dimen.subtitle_hit_rect_height) - this.mBaseActionIconSubtitleView.getHeight()) / 2; + this.mBaseActionIconSubtitleView.getHitRect(this.mBaseActionIconSubtitleHitRect); + offsetDescendantRectToMyCoords((View) this.mBaseActionIconSubtitleView.getParent(), this.mBaseActionIconSubtitleHitRect); + if (dimensionPixelSize > 0 || this.mBaseActionIconSubtitleHitRect.bottom != getHeight()) { + if (dimensionPixelSize > 0) { + this.mBaseActionIconSubtitleHitRect.top -= dimensionPixelSize; + } + this.mBaseActionIconSubtitleHitRect.bottom = getHeight(); + setTouchDelegate(new TouchDelegate(this.mBaseActionIconSubtitleHitRect, this.mBaseActionIconSubtitleView)); + } + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + public final void setBaseActionIconSubtitle(CharSequence charSequence, CharSequence charSequence2, Drawable drawable) { + if (this.mBaseActionIconSubtitleView == null) { + Log.w("BcSmartspaceCard", "No base action icon subtitle view to update"); + return; + } + if (TextUtils.isEmpty(charSequence)) { + BcSmartspaceTemplateDataUtils.updateVisibility(this.mBaseActionIconSubtitleView, 8); + return; + } + BcSmartspaceTemplateDataUtils.updateVisibility(this.mBaseActionIconSubtitleView, 0); + this.mBaseActionIconSubtitleView.setText(charSequence); + this.mBaseActionIconSubtitleView.setCompoundDrawablesRelative(drawable, null, null, null); + ContentDescriptionUtil.setFormattedContentDescription("BcSmartspaceCard", this.mBaseActionIconSubtitleView, charSequence, charSequence2); + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // com.google.android.systemui.smartspace.SmartspaceCard + public final void setDozeAmount(float f) { + this.mDozeAmount = f; + SmartspaceTarget smartspaceTarget = this.mTarget; + if (smartspaceTarget != null && smartspaceTarget.getBaseAction() != null && this.mTarget.getBaseAction().getExtras() != null) { + Bundle extras = this.mTarget.getBaseAction().getExtras(); + if (this.mTitleTextView != null && extras.getBoolean("hide_title_on_aod")) { + this.mTitleTextView.setAlpha(1.0f - f); + } + if (this.mSubtitleTextView != null && extras.getBoolean("hide_subtitle_on_aod")) { + this.mSubtitleTextView.setAlpha(1.0f - f); + } + } + if (this.mTextGroup == null) { + return; + } + BcSmartspaceTemplateDataUtils.updateVisibility(this.mSecondaryCardGroup, (this.mDozeAmount == 1.0f || !this.mValidSecondaryCard) ? 8 : 0); + SmartspaceTarget smartspaceTarget2 = this.mTarget; + if (smartspaceTarget2 == null || smartspaceTarget2.getFeatureType() != 30) { + ViewGroup viewGroup = this.mSecondaryCardGroup; + if (viewGroup == null || viewGroup.getVisibility() == 8) { + this.mTextGroup.setTranslationX(0.0f); + return; + } + this.mTextGroup.setTranslationX(((PathInterpolator) Interpolators.EMPHASIZED).getInterpolation(this.mDozeAmount) * this.mSecondaryCardGroup.getWidth() * (isRtl() ? 1 : -1)); + this.mSecondaryCardGroup.setAlpha(Math.max(0.0f, Math.min(1.0f, ((1.0f - this.mDozeAmount) * 9.0f) - 6.0f))); + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // com.google.android.systemui.smartspace.SmartspaceCard + public final void setPrimaryTextColor(int i) { + TextView textView = this.mTitleTextView; + if (textView != null) { + textView.setTextColor(i); + } + TextView textView2 = this.mSubtitleTextView; + if (textView2 != null) { + textView2.setTextColor(i); + } + DoubleShadowTextView doubleShadowTextView = this.mBaseActionIconSubtitleView; + if (doubleShadowTextView != null) { + doubleShadowTextView.setTextColor(i); + } + BcSmartspaceCardSecondary bcSmartspaceCardSecondary = this.mSecondaryCard; + if (bcSmartspaceCardSecondary != null) { + bcSmartspaceCardSecondary.setTextColor(i); + } + this.mIconTintColor = i; + updateIconTint(); + } + + public final void setSecondaryCard(BcSmartspaceCardSecondary secondaryCard) { + if (mSecondaryCardGroup == null) { + return; + } + mSecondaryCard = secondaryCard; + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondaryCardGroup, View.GONE); + mSecondaryCardGroup.removeAllViews(); + if (secondaryCard != null) { + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_card_height)); + params.setMarginStart(getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_secondary_card_start_margin)); + params.startToStart = 0; + params.topToTop = 0; + params.bottomToBottom = 0; + mSecondaryCardGroup.addView(secondaryCard, params); + } + } + + public final void setSubtitle(CharSequence text, CharSequence contentDescription, boolean useIcon) { + if (mSubtitleTextView == null) { + Log.w("BcSmartspaceCard", "No subtitle view to update"); + return; + } + mSubtitleTextView.setText(text); + mSubtitleTextView.setCompoundDrawablesRelative((TextUtils.isEmpty(text) || !useIcon) ? null : mIconDrawable, null, null, null); + mSubtitleTextView.setMaxLines((mTarget == null || mTarget.getFeatureType() != 5 || mUsePageIndicatorUi) ? 1 : 2); + ContentDescriptionUtil.setFormattedContentDescription("BcSmartspaceCard", mSubtitleTextView, text, contentDescription); + BcSmartspaceTemplateDataUtils.offsetTextViewForIcon(mSubtitleTextView, useIcon ? mIconDrawable : null, isRtl()); + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + public final void setTitle(CharSequence text, CharSequence contentDescription, boolean useIcon) { + boolean z2; + if (mTitleTextView == null) { + Log.w("BcSmartspaceCard", "No title view to update"); + return; + } + mTitleTextView.setText(text); + SmartspaceAction headerAction = mTarget == null ? null : mTarget.getHeaderAction(); + Bundle extras = headerAction == null ? null : headerAction.getExtras(); + if (extras == null || !extras.containsKey("titleEllipsize")) { + if (mTarget != null && mTarget.getFeatureType() == 2 && Locale.ENGLISH.getLanguage().equals(getResources().getConfiguration().locale.getLanguage())) { + mTitleTextView.setEllipsize(TextUtils.TruncateAt.MIDDLE); + } else { + mTitleTextView.setEllipsize(TextUtils.TruncateAt.END); + } + } else { + try { + mTitleTextView.setEllipsize(TextUtils.TruncateAt.valueOf(extras.getString("titleEllipsize"))); + } catch (IllegalArgumentException unused) { + Log.w( + "BcSmartspaceCard", + "Invalid TruncateAt value: " + extras.getString("titleEllipsize")); + } + } + boolean z3 = false; + if (extras != null) { + if (extras.getInt("titleMaxLines") != 0) { + mTitleTextView.setMaxLines(extras.getInt("titleMaxLines")); + } + z2 = extras.getBoolean("disableTitleIcon"); + } else { + z2 = false; + } + if (useIcon && !z2) { + z3 = true; + } + if (z3) { + ContentDescriptionUtil.setFormattedContentDescription("BcSmartspaceCard", mTitleTextView, text, contentDescription); + } + mTitleTextView.setCompoundDrawablesRelative(z3 ? mIconDrawable : null, null, null, null); + BcSmartspaceTemplateDataUtils.offsetTextViewForIcon(mTitleTextView, z3 ? mIconDrawable : null, isRtl()); + } + + public final void updateIconTint() { + if (mTarget == null) { + return; + } + if (mTarget.getFeatureType() == 1) { + mIconDrawable.setTintList(null); + } else { + mIconDrawable.setTint(mIconTintColor); + } + SmartspaceAction baseAction = mTarget.getBaseAction(); + int subcardType = -1; + if (baseAction != null && baseAction.getExtras() != null && !baseAction.getExtras().isEmpty()) { + subcardType = baseAction.getExtras().getInt("subcardType", -1); + } + if (subcardType == 1) { + mBaseActionIconDrawable.setTintList(null); + } else { + mBaseActionIconDrawable.setTint(mIconTintColor); + } + } + + public BcSmartspaceCard(Context context, AttributeSet attrs) { + super(context, attrs); + mSecondaryCard = null; + mIconTintColor = GraphicsUtils.getAttrColor(context, android.R.attr.textColorPrimary); + mTextGroup = null; + mSecondaryCardGroup = null; + mTitleTextView = null; + mSubtitleTextView = null; + mBaseActionIconSubtitleView = null; + mBaseActionIconSubtitleHitRect = null; + mUiSurface = null; + mTouchDelegateIsDirty = false; + context.getTheme().applyStyle(R.style.Smartspace, false); + mIconDrawable = new DoubleShadowIconDrawable(context); + mBaseActionIconDrawable = new DoubleShadowIconDrawable(context); + setDefaultFocusHighlightEnabled(false); + } + + @Override + public final View getView() { + return this; + } + + @Override + public final void setScreenOn(boolean screenOn) { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombination.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombination.java new file mode 100644 index 000000000000..e309e0a91fe3 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombination.java @@ -0,0 +1,117 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; + +public class BcSmartspaceCardCombination extends BcSmartspaceCardSecondary { + public ConstraintLayout mFirstSubCard; + public ConstraintLayout mSecondSubCard; + + public BcSmartspaceCardCombination(Context context) { + super(context); + } + public final boolean fillSubCard(ConstraintLayout constraintLayout, SmartspaceTarget target, SmartspaceAction action, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + boolean z; + TextView textView = (TextView) constraintLayout.findViewById(R.id.sub_card_text); + ImageView imageView = (ImageView) constraintLayout.findViewById(R.id.sub_card_icon); + if (textView == null) { + Log.w("BcSmartspaceCardCombination", "No sub-card text field to update"); + return false; + } + if (imageView == null) { + Log.w("BcSmartspaceCardCombination", "No sub-card image field to update"); + return false; + } + BcSmartSpaceUtil.setOnClickListener(constraintLayout, target, action, eventNotifier, "BcSmartspaceCardCombination", loggingInfo, 0); + Icon icon = action.getIcon(); + Context context = getContext(); + Drawable iconDrawableWithCustomSize = BcSmartSpaceUtil.getIconDrawableWithCustomSize(icon, context, context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size)); + boolean z2 = true; + if (iconDrawableWithCustomSize == null) { + BcSmartspaceTemplateDataUtils.updateVisibility(imageView, 8); + z = false; + } else { + imageView.setImageDrawable(iconDrawableWithCustomSize); + BcSmartspaceTemplateDataUtils.updateVisibility(imageView, 0); + z = true; + } + CharSequence title = action.getTitle(); + if (TextUtils.isEmpty(title)) { + BcSmartspaceTemplateDataUtils.updateVisibility(textView, 8); + z2 = z; + } else { + textView.setText(title); + BcSmartspaceTemplateDataUtils.updateVisibility(textView, 0); + } + constraintLayout.setContentDescription(z2 ? action.getContentDescription() : null); + if (z2) { + BcSmartspaceTemplateDataUtils.updateVisibility(constraintLayout, 0); + return z2; + } + BcSmartspaceTemplateDataUtils.updateVisibility(constraintLayout, 8); + return z2; + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mFirstSubCard = findViewById(R.id.first_sub_card); + mSecondSubCard = findViewById(R.id.second_sub_card); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstSubCard, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondSubCard, 8); + } + + @Override + public boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + SmartspaceAction action; + List actionChips = target.getActionChips(); + if (actionChips == null || actionChips.size() < 1 || (action = (SmartspaceAction) actionChips.get(0)) == null) { + return false; + } + ConstraintLayout constraintLayout = mFirstSubCard; + boolean z = constraintLayout != null && fillSubCard(constraintLayout, target, action, eventNotifier, loggingInfo); + boolean z2 = actionChips.size() > 1 && actionChips.get(1) != null; + boolean fillSubCard = z2 ? fillSubCard(mSecondSubCard, target, (SmartspaceAction) actionChips.get(1), eventNotifier, loggingInfo) : true; + if (getLayoutParams() instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); + if (z2 && fillSubCard) { + layoutParams.weight = 3.0f; + } else { + layoutParams.weight = 1.0f; + } + setLayoutParams(layoutParams); + } + return z && fillSubCard; + } + + public BcSmartspaceCardCombination(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + @Override + public final void setTextColor(int i) { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombinationAtStore.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombinationAtStore.java new file mode 100644 index 000000000000..a67ac1be5b61 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardCombinationAtStore.java @@ -0,0 +1,43 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; + +public class BcSmartspaceCardCombinationAtStore extends BcSmartspaceCardCombination { + public BcSmartspaceCardCombinationAtStore(Context context) { + super(context); + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + SmartspaceAction action; + List actionChips = target.getActionChips(); + if (actionChips == null || actionChips.isEmpty() || (action = (SmartspaceAction) actionChips.get(0)) == null) { + return false; + } + ConstraintLayout constraintLayout = this.mFirstSubCard; + boolean z = (constraintLayout instanceof BcSmartspaceCardShoppingList) && ((BcSmartspaceCardShoppingList) constraintLayout).setSmartspaceActions(target, eventNotifier, loggingInfo); + ConstraintLayout constraintLayout2 = this.mSecondSubCard; + boolean z2 = constraintLayout2 != null && fillSubCard(constraintLayout2, target, action, eventNotifier, loggingInfo); + if (z) { + this.mFirstSubCard.setBackgroundResource(R.drawable.bg_smartspace_combination_sub_card); + } + return z && z2; + } + + public BcSmartspaceCardCombinationAtStore(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardDoorbell.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardDoorbell.java new file mode 100644 index 000000000000..495912cceb77 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardDoorbell.java @@ -0,0 +1,370 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ImageDecoder; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableWrapper; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; + +import com.android.internal.util.LatencyTracker; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class BcSmartspaceCardDoorbell extends BcSmartspaceCardGenericImage { + public static final String TAG = "BcSmartspaceCardBell"; + public int mGifFrameDurationInMs = 200; + public final LatencyInstrumentContext mLatencyInstrumentContext; + public ImageView mLoadingIcon; + public ViewGroup mLoadingScreenView; + public String mPreviousTargetId; + public ProgressBar mProgressBar; + public final Map mUriToDrawable = new HashMap<>(); + + public BcSmartspaceCardDoorbell(Context context) { + this(context, null); + } + + public BcSmartspaceCardDoorbell(Context context, AttributeSet attrs) { + super(context, attrs); + mLatencyInstrumentContext = new LatencyInstrumentContext(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLoadingScreenView = findViewById(R.id.loading_screen); + mProgressBar = findViewById(R.id.indeterminateBar); + mLoadingIcon = findViewById(R.id.loading_screen_icon); + } + + @Override + public void resetUi() { + super.resetUi(); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoadingScreenView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mProgressBar, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoadingIcon, View.GONE); + } + + public void maybeResetImageView(SmartspaceTarget target) { + String targetId = target.getSmartspaceTargetId(); + boolean sameTarget = targetId.equals(mPreviousTargetId); + mPreviousTargetId = targetId; + + if (!sameTarget) { + ViewGroup.LayoutParams params = mImageView.getLayoutParams(); + params.width = ViewGroup.LayoutParams.WRAP_CONTENT; + mImageView.setImageDrawable(null); + mUriToDrawable.clear(); + } + } + + public void maybeUpdateLayoutHeight(Bundle extras, View view, String key) { + if (extras.containsKey(key)) { + float density = getContext().getResources().getDisplayMetrics().density; + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.height = (int) (extras.getInt(key) * density); + } + } + + public void maybeUpdateLayoutWidth(Bundle extras, View view, String key) { + if (extras.containsKey(key)) { + float density = getContext().getResources().getDisplayMetrics().density; + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.width = (int) (extras.getInt(key) * density); + } + } + + @Override + public boolean setSmartspaceActions( + SmartspaceTarget target, + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, + BcSmartspaceCardLoggingInfo loggingInfo) { + + if (!getContext().getPackageName().equals("com.android.systemui")) { + return false; + } + + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction != null ? baseAction.getExtras() : null; + + List imageUris = target.getIconGrid().stream() + .filter(action -> action.getExtras().containsKey("imageUri")) + .map(action -> Uri.parse(action.getExtras().getString("imageUri"))) + .collect(Collectors.toList()); + + if (!imageUris.isEmpty()) { + if (extras != null && extras.containsKey("frameDurationMs")) { + mGifFrameDurationInMs = extras.getInt("frameDurationMs"); + } + + Set newUris = imageUris.stream() + .filter(uri -> !mUriToDrawable.containsKey(uri)) + .collect(Collectors.toSet()); + + if (!newUris.isEmpty()) { + mLatencyInstrumentContext.mUriSet.addAll(newUris); + mLatencyInstrumentContext.mLatencyTracker.onActionStart(22); + } + + maybeResetImageView(target); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.VISIBLE); + + ContentResolver contentResolver = getContext().getApplicationContext().getContentResolver(); + int height = getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_card_height); + float cornerRadius = getResources().getDimension(R.dimen.enhanced_smartspace_secondary_card_corner_radius); + + WeakReference imageViewRef = new WeakReference<>(mImageView); + WeakReference loadingScreenRef = new WeakReference<>(mLoadingScreenView); + + List drawables = imageUris.stream() + .map(uri -> { + DrawableWithUri drawable = mUriToDrawable.computeIfAbsent(uri, u -> { + DrawableWithUri d = new DrawableWithUri( + u, contentResolver, height, cornerRadius, + imageViewRef, loadingScreenRef); + new LoadUriTask(mLatencyInstrumentContext).execute(d); + return d; + }); + return drawable; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + AnimationDrawable animation = new AnimationDrawable(); + for (DrawableWithUri drawable : drawables) { + animation.addFrame(drawable, mGifFrameDurationInMs); + } + mImageView.setImageDrawable(animation); + animation.start(); + Log.d(TAG, "imageUri is set"); + return true; + } else if (extras != null && extras.containsKey("imageBitmap")) { + Bitmap bitmap = (Bitmap) extras.get("imageBitmap"); + maybeResetImageView(target); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.VISIBLE); + + if (bitmap != null && bitmap.getHeight() != 0) { + int height = (int) getResources().getDimension(R.dimen.enhanced_smartspace_card_height); + float aspectRatio = (float) bitmap.getWidth() / bitmap.getHeight(); + bitmap = Bitmap.createScaledBitmap(bitmap, (int) (height * aspectRatio), height, true); + + RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(getResources(), bitmap); + drawable.setCornerRadius(getResources().getDimension(R.dimen.enhanced_smartspace_secondary_card_corner_radius)); + mImageView.setImageDrawable(drawable); + Log.d(TAG, "imageBitmap is set"); + } + return true; + } else if (extras != null && extras.containsKey("loadingScreenState")) { + int state = extras.getInt("loadingScreenState"); + String dimensionRatio = BcSmartSpaceUtil.getDimensionRatio(extras); + if (dimensionRatio == null) { + return false; + } + + maybeResetImageView(target); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.GONE); + + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mLoadingScreenView.getLayoutParams(); + params.dimensionRatio = dimensionRatio; + mLoadingScreenView.setBackgroundTintList(ColorStateList.valueOf(getContext().getColor(R.color.smartspace_button_background))); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoadingScreenView, View.VISIBLE); + + maybeUpdateLayoutWidth(extras, mProgressBar, "progressBarWidth"); + maybeUpdateLayoutHeight(extras, mProgressBar, "progressBarHeight"); + mProgressBar.setIndeterminateTintList(ColorStateList.valueOf(getContext().getColor(R.color.smartspace_button_text))); + + boolean progressBarVisible = (state == 1 || (state == 4 && extras.getBoolean("progressBarVisible", true))); + BcSmartspaceTemplateDataUtils.updateVisibility(mProgressBar, progressBarVisible ? View.VISIBLE : View.GONE); + + boolean iconVisible = false; + if (state == 2) { + mLoadingIcon.setImageDrawable(getContext().getDrawable(R.drawable.videocam)); + iconVisible = true; + } else if (state == 3) { + mLoadingIcon.setImageDrawable(getContext().getDrawable(R.drawable.videocam_off)); + iconVisible = true; + } else if (state == 4 && extras.containsKey("loadingScreenIcon")) { + mLoadingIcon.setImageBitmap((Bitmap) extras.get("loadingScreenIcon")); + if (extras.getBoolean("tintLoadingIcon", false)) { + mLoadingIcon.setColorFilter(getContext().getColor(R.color.smartspace_button_text)); + } + iconVisible = true; + } + + maybeUpdateLayoutWidth(extras, mLoadingIcon, "loadingIconWidth"); + maybeUpdateLayoutHeight(extras, mLoadingIcon, "loadingIconHeight"); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoadingIcon, iconVisible ? View.VISIBLE : View.GONE); + return true; + } + return false; + } + + public static class DrawableWithUri extends DrawableWrapper { + public final Path mClipPath = new Path(); + public final ContentResolver mContentResolver; + public Drawable mDrawable; + public final int mHeightInPx; + public final WeakReference mImageViewWeakReference; + public final WeakReference mLoadingScreenWeakReference; + public final float mScaledCornerRadius; + public final RectF mTempRect = new RectF(); + public final Uri mUri; + + public DrawableWithUri( + Uri uri, + ContentResolver contentResolver, + int heightInPx, + float scaledCornerRadius, + WeakReference imageViewRef, + WeakReference loadingScreenRef) { + super(new ColorDrawable(0)); + mUri = uri; + mContentResolver = contentResolver; + mHeightInPx = heightInPx; + mScaledCornerRadius = scaledCornerRadius; + mImageViewWeakReference = imageViewRef; + mLoadingScreenWeakReference = loadingScreenRef; + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + canvas.clipPath(mClipPath); + super.draw(canvas); + canvas.restore(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mTempRect.set(bounds); + mClipPath.reset(); + mClipPath.addRoundRect(mTempRect, mScaledCornerRadius, mScaledCornerRadius, Path.Direction.CCW); + super.onBoundsChange(bounds); + } + } + + public static class LatencyInstrumentContext { + public final LatencyTracker mLatencyTracker; + public final Set mUriSet = new HashSet<>(); + + public LatencyInstrumentContext(Context context) { + mLatencyTracker = LatencyTracker.getInstance(context); + } + + public void cancelInstrument() { + if (!mUriSet.isEmpty()) { + mLatencyTracker.onActionCancel(22); + mUriSet.clear(); + } + } + } + + public static class LoadUriTask extends AsyncTask { + public final LatencyInstrumentContext mInstrumentContext; + + public LoadUriTask(LatencyInstrumentContext context) { + mInstrumentContext = context; + } + + @Override + protected DrawableWithUri doInBackground(DrawableWithUri... params) { + if (params.length == 0) return null; + DrawableWithUri drawable = params[0]; + try (InputStream inputStream = drawable.mContentResolver.openInputStream(drawable.mUri)) { + ImageDecoder.Source source = ImageDecoder.createSource((Resources) null, inputStream); + drawable.mDrawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); + int height = drawable.mHeightInPx; + float aspectRatio = info.getSize().getHeight() != 0 + ? (float) info.getSize().getWidth() / info.getSize().getHeight() + : 0; + decoder.setTargetSize((int) (height * aspectRatio), height); + }); + } catch (IOException e) { + Log.e(TAG, "Unable to decode stream: " + e); + } catch (Exception e) { + Log.w(TAG, "open uri:" + drawable.mUri + " got exception:" + e); + } + return drawable; + } + + @Override + protected void onPostExecute(DrawableWithUri result) { + if (result == null) return; + if (result.mDrawable != null) { + result.setDrawable(result.mDrawable); + ImageView imageView = result.mImageViewWeakReference.get(); + if (imageView != null) { + int intrinsicWidth = result.mDrawable.getIntrinsicWidth(); + if (imageView.getLayoutParams().width != intrinsicWidth) { + Log.d(TAG, "imageView requestLayout " + result.mUri); + imageView.getLayoutParams().width = intrinsicWidth; + imageView.requestLayout(); + } + } + if (result.mUri != null + && mInstrumentContext.mUriSet.remove(result.mUri) + && mInstrumentContext.mUriSet.isEmpty()) { + mInstrumentContext.mLatencyTracker.onActionEnd(22); + } else if (result.mUri == null) { + mInstrumentContext.cancelInstrument(); + } + } else { + ImageView imageView = result.mImageViewWeakReference.get(); + if (imageView != null) { + BcSmartspaceTemplateDataUtils.updateVisibility(imageView, View.GONE); + } + mInstrumentContext.cancelInstrument(); + } + View loadingScreen = result.mLoadingScreenWeakReference.get(); + if (loadingScreen != null) { + BcSmartspaceTemplateDataUtils.updateVisibility(loadingScreen, View.GONE); + } + } + + @Override + protected void onCancelled() { + mInstrumentContext.cancelInstrument(); + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardFlight.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardFlight.java new file mode 100644 index 000000000000..90bd7fa0ad6b --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardFlight.java @@ -0,0 +1,60 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class BcSmartspaceCardFlight extends BcSmartspaceCardSecondary { + public ImageView mQrCodeView; + + public BcSmartspaceCardFlight(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mQrCodeView = (ImageView) findViewById(R.id.flight_qr_code); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mQrCodeView, 8); + } + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + if (extras == null || !extras.containsKey("qrCodeBitmap")) { + return false; + } + Bitmap bitmap = (Bitmap) extras.get("qrCodeBitmap"); + ImageView imageView = mQrCodeView; + if (imageView == null) { + Log.w("BcSmartspaceCardFlight", "No flight QR code view to update"); + } else { + imageView.setImageBitmap(bitmap); + } + BcSmartspaceTemplateDataUtils.updateVisibility(mQrCodeView, 0); + return true; + } + + public BcSmartspaceCardFlight(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + @Override + public final void setTextColor(int color) { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardGenericImage.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardGenericImage.java new file mode 100644 index 000000000000..eb770e104238 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardGenericImage.java @@ -0,0 +1,78 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class BcSmartspaceCardGenericImage extends BcSmartspaceCardSecondary { + public ImageView mImageView; + + public BcSmartspaceCardGenericImage(Context context) { + super(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = (ImageView) findViewById(R.id.image_view); + } + + @Override + public void resetUi() { + mImageView.setImageBitmap(null); + } + public void setImageBitmap(Bitmap bitmap) { + mImageView.setImageBitmap(bitmap); + } + + @Override + public boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + if (extras == null || !extras.containsKey("imageBitmap")) { + return false; + } + if (extras.containsKey("imageScaleType")) { + String scaleType = extras.getString("imageScaleType"); + try { + mImageView.setScaleType(ImageView.ScaleType.valueOf(scaleType)); + } catch (IllegalArgumentException unused) { + Log.w("SmartspaceGenericImg", "Invalid imageScaleType value: " + scaleType); + } + } + String dimensionRatio = BcSmartSpaceUtil.getDimensionRatio(extras); + if (dimensionRatio != null) { + ((ConstraintLayout.LayoutParams) mImageView.getLayoutParams()).dimensionRatio = dimensionRatio; + } + if (extras.containsKey("imageLayoutWidth")) { + ((ViewGroup.MarginLayoutParams) ((ConstraintLayout.LayoutParams) mImageView.getLayoutParams())).width = extras.getInt("imageLayoutWidth"); + } + if (extras.containsKey("imageLayoutHeight")) { + ((ViewGroup.MarginLayoutParams) ((ConstraintLayout.LayoutParams) mImageView.getLayoutParams())).height = extras.getInt("imageLayoutHeight"); + } + setImageBitmap((Bitmap) extras.get("imageBitmap")); + return true; + } + + public BcSmartspaceCardGenericImage(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + @Override + public void setTextColor(int i) { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardLoyalty.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardLoyalty.java new file mode 100644 index 000000000000..6bbb1c4946cb --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardLoyalty.java @@ -0,0 +1,98 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class BcSmartspaceCardLoyalty extends BcSmartspaceCardGenericImage { + public TextView mCardPromptView; + public ImageView mLoyaltyProgramLogoView; + public TextView mLoyaltyProgramNameView; + + public BcSmartspaceCardLoyalty(Context context) { + super(context); + } + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mLoyaltyProgramLogoView = findViewById(R.id.loyalty_program_logo); + mLoyaltyProgramNameView = findViewById(R.id.loyalty_program_name); + mCardPromptView = findViewById(R.id.card_prompt); + } + @Override + public final void resetUi() { + super.resetUi(); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoyaltyProgramLogoView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mLoyaltyProgramNameView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptView, 8); + } + @Override + public final void setImageBitmap(Bitmap bitmap) { + super.setImageBitmap(bitmap); + mLoyaltyProgramLogoView.setImageBitmap(bitmap); + } + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + super.setSmartspaceActions(target, eventNotifier, loggingInfo); + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + if (extras == null) { + return false; + } + boolean containsKey = extras.containsKey("imageBitmap"); + if (extras.containsKey("cardPrompt")) { + String string = extras.getString("cardPrompt"); + TextView textView = mCardPromptView; + if (textView == null) { + Log.w("BcSmartspaceCardLoyalty", "No card prompt view to update"); + } else { + textView.setText(string); + } + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptView, 0); + if (containsKey) { + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, 0); + } + return true; + } + if (!extras.containsKey("loyaltyProgramName")) { + if (containsKey) { + BcSmartspaceTemplateDataUtils.updateVisibility(mLoyaltyProgramLogoView, 0); + } + return containsKey; + } + String string2 = extras.getString("loyaltyProgramName"); + TextView textView2 = mLoyaltyProgramNameView; + if (textView2 == null) { + Log.w("BcSmartspaceCardLoyalty", "No loyalty program name view to update"); + } else { + textView2.setText(string2); + } + BcSmartspaceTemplateDataUtils.updateVisibility(mLoyaltyProgramNameView, 0); + if (containsKey) { + BcSmartspaceTemplateDataUtils.updateVisibility(mLoyaltyProgramLogoView, 0); + } + return true; + } + + @Override + public final void setTextColor(int color) { + mLoyaltyProgramNameView.setTextColor(color); + mCardPromptView.setTextColor(color); + } + public BcSmartspaceCardLoyalty(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSecondary.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSecondary.java new file mode 100644 index 000000000000..bdddd011d4f7 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSecondary.java @@ -0,0 +1,40 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +public abstract class BcSmartspaceCardSecondary extends ConstraintLayout { + public String mPrevSmartspaceTargetId; + + public BcSmartspaceCardSecondary(Context context) { + super(context); + mPrevSmartspaceTargetId = ""; + } + + public final void reset(String str) { + if (mPrevSmartspaceTargetId.equals(str)) { + return; + } + mPrevSmartspaceTargetId = str; + resetUi(); + } + + public abstract boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo); + + public abstract void setTextColor(int color); + + public BcSmartspaceCardSecondary(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + mPrevSmartspaceTargetId = ""; + } + + public void resetUi() { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardShoppingList.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardShoppingList.java new file mode 100644 index 000000000000..dba16f42e415 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardShoppingList.java @@ -0,0 +1,148 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.Locale; + +public class BcSmartspaceCardShoppingList extends BcSmartspaceCardSecondary { + public static final int[] LIST_ITEM_TEXT_VIEW_IDS = {R.id.list_item_1, R.id.list_item_2, R.id.list_item_3}; + public ImageView mCardPromptIconView; + public TextView mCardPromptView; + public TextView mEmptyListMessageView; + public ImageView mListIconView; + public final TextView[] mListItems; + + public BcSmartspaceCardShoppingList(Context context) { + super(context); + mListItems = new TextView[3]; + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mCardPromptView = findViewById(R.id.card_prompt); + mEmptyListMessageView = findViewById(R.id.empty_list_message); + mCardPromptIconView = findViewById(R.id.card_prompt_icon); + mListIconView = findViewById(R.id.list_icon); + for (int i = 0; i < 3; i++) { + mListItems[i] = findViewById(LIST_ITEM_TEXT_VIEW_IDS[i]); + } + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mEmptyListMessageView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptIconView, 8); + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptView, 8); + for (int i = 0; i < 3; i++) { + BcSmartspaceTemplateDataUtils.updateVisibility(mListItems[i], 8); + } + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + SmartspaceAction baseAction = target.getBaseAction(); + Bitmap bitmap = null; + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + if (extras != null) { + if (extras.containsKey("appIcon")) { + bitmap = (Bitmap) extras.get("appIcon"); + } else if (extras.containsKey("imageBitmap")) { + bitmap = (Bitmap) extras.get("imageBitmap"); + } + mCardPromptIconView.setImageBitmap(bitmap); + mListIconView.setImageBitmap(bitmap); + if (extras.containsKey("cardPrompt")) { + String string = extras.getString("cardPrompt"); + TextView textView = mCardPromptView; + if (textView == null) { + Log.w("BcSmartspaceCardShoppingList", "No card prompt view to update"); + } else { + textView.setText(string); + } + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptView, 0); + if (bitmap != null) { + BcSmartspaceTemplateDataUtils.updateVisibility(mCardPromptIconView, 0); + return true; + } + } else { + if (extras.containsKey("emptyListString")) { + String string2 = extras.getString("emptyListString"); + TextView textView2 = mEmptyListMessageView; + if (textView2 == null) { + Log.w("BcSmartspaceCardShoppingList", "No empty list message view to update"); + } else { + textView2.setText(string2); + } + BcSmartspaceTemplateDataUtils.updateVisibility(mEmptyListMessageView, 0); + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, 0); + return true; + } + if (extras.containsKey("listItems")) { + String[] stringArray = extras.getStringArray("listItems"); + if (stringArray.length != 0) { + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, 0); + for (int i = 0; i < 3; i++) { + TextView textView3 = mListItems[i]; + if (textView3 == null) { + Log.w( + "BcSmartspaceCardShoppingList", + String.format( + Locale.US, + "Missing list item view to update at row: %d", + i + 1)); + return true; + } + if (i < stringArray.length) { + BcSmartspaceTemplateDataUtils.updateVisibility(textView3, 0); + textView3.setText(stringArray[i]); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(textView3, 8); + textView3.setText(""); + } + } + } + } + } + return true; + } + return false; + } + + @Override + public final void setTextColor(int i) { + mCardPromptView.setTextColor(i); + mEmptyListMessageView.setTextColor(i); + for (int i2 = 0; i2 < 3; i2++) { + TextView textView = mListItems[i2]; + if (textView == null) { + Log.w( + "BcSmartspaceCardShoppingList", + String.format( + Locale.US, "Missing list item view to update at row: %d", i2 + 1)); + return; + } + textView.setTextColor(i); + } + } + + public BcSmartspaceCardShoppingList(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + mListItems = new TextView[3]; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSports.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSports.java new file mode 100644 index 000000000000..971bef26c273 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardSports.java @@ -0,0 +1,127 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class BcSmartspaceCardSports extends BcSmartspaceCardSecondary { + public ImageView mFirstCompetitorLogo; + public TextView mFirstCompetitorScore; + public ImageView mSecondCompetitorLogo; + public TextView mSecondCompetitorScore; + public TextView mSummaryView; + + public BcSmartspaceCardSports(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mSummaryView = findViewById(R.id.match_time_summary); + mFirstCompetitorScore = findViewById(R.id.first_competitor_score); + mSecondCompetitorScore = findViewById(R.id.second_competitor_score); + mFirstCompetitorLogo = findViewById(R.id.first_competitor_logo); + mSecondCompetitorLogo = findViewById(R.id.second_competitor_logo); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mSummaryView, 4); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorScore, 4); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorScore, 4); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorLogo, 4); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorLogo, 4); + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + boolean z; + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + if (extras == null) { + return false; + } + if (extras.containsKey("matchTimeSummary")) { + String string = extras.getString("matchTimeSummary"); + TextView textView = mSummaryView; + if (textView == null) { + Log.w("BcSmartspaceCardSports", "No match time summary view to update"); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(textView, 0); + this.mSummaryView.setText(string); + } + z = true; + } else { + z = false; + } + if (extras.containsKey("firstCompetitorScore")) { + String string2 = extras.getString("firstCompetitorScore"); + TextView textView2 = mFirstCompetitorScore; + if (textView2 == null) { + Log.w("BcSmartspaceCardSports", "No first competitor logo view to update"); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(textView2, 0); + mFirstCompetitorScore.setText(string2); + } + z = true; + } + if (extras.containsKey("secondCompetitorScore")) { + String string3 = extras.getString("secondCompetitorScore"); + TextView textView3 = mSecondCompetitorScore; + if (textView3 == null) { + Log.w("BcSmartspaceCardSports", "No second competitor logo view to update"); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(textView3, 0); + mSecondCompetitorScore.setText(string3); + } + z = true; + } + if (extras.containsKey("firstCompetitorLogo")) { + Bitmap bitmap = (Bitmap) extras.get("firstCompetitorLogo"); + ImageView imageView = mFirstCompetitorLogo; + if (imageView == null) { + Log.w("BcSmartspaceCardSports", "No first competitor logo view to update"); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(imageView, 0); + mFirstCompetitorLogo.setImageBitmap(bitmap); + } + z = true; + } + if (!extras.containsKey("secondCompetitorLogo")) { + return z; + } + Bitmap bitmap2 = (Bitmap) extras.get("secondCompetitorLogo"); + ImageView imageView2 = mSecondCompetitorLogo; + if (imageView2 == null) { + Log.w("BcSmartspaceCardSports", "No second competitor logo view to update"); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(imageView2, 0); + mSecondCompetitorLogo.setImageBitmap(bitmap2); + } + return true; + } + + @Override + public final void setTextColor(int color) { + mSummaryView.setTextColor(color); + mFirstCompetitorScore.setTextColor(color); + mSecondCompetitorScore.setTextColor(color); + } + + public BcSmartspaceCardSports(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardWeatherForecast.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardWeatherForecast.java new file mode 100644 index 000000000000..35544c1524a9 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceCardWeatherForecast.java @@ -0,0 +1,177 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.Constraints; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.Locale; + +public class BcSmartspaceCardWeatherForecast extends BcSmartspaceCardSecondary { + + public interface ItemUpdateFunction { + void update(View view, int i); + } + public BcSmartspaceCardWeatherForecast(Context context) { + super(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + ConstraintLayout[] constraintLayoutArr = new ConstraintLayout[4]; + for (int i = 0; i < 4; i++) { + ConstraintLayout constraintLayout = (ConstraintLayout) ViewGroup.inflate(getContext(), R.layout.smartspace_card_weather_forecast_column, null); + constraintLayout.setId(View.generateViewId()); + constraintLayoutArr[i] = constraintLayout; + } + int i2 = 0; + while (i2 < 4) { + Constraints.LayoutParams layoutParams = new Constraints.LayoutParams(-2, 0); + ConstraintLayout constraintLayout2 = constraintLayoutArr[i2]; + ConstraintLayout constraintLayout3 = i2 > 0 ? constraintLayoutArr[i2 - 1] : null; + ConstraintLayout constraintLayout4 = i2 < 3 ? constraintLayoutArr[i2 + 1] : null; + if (i2 == 0) { + layoutParams.startToStart = 0; + layoutParams.horizontalChainStyle = 1; + } else { + layoutParams.startToEnd = constraintLayout3.getId(); + } + if (i2 == 3) { + layoutParams.endToEnd = 0; + } else { + layoutParams.endToStart = constraintLayout4.getId(); + } + layoutParams.topToTop = 0; + layoutParams.bottomToBottom = 0; + addView(constraintLayout2, layoutParams); + i2++; + } + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + boolean z; + SmartspaceAction baseAction = target.getBaseAction(); + Bundle extras = baseAction == null ? null : baseAction.getExtras(); + + if (extras == null) { + return false; + } + + if (extras.containsKey("temperatureValues")) { + String[] stringArray = extras.getStringArray("temperatureValues"); + if (stringArray == null) { + Log.w("BcSmartspaceCardWeatherForecast", "Temperature values array is null."); + } else { + updateFields((view, i) -> ((TextView) view).setText(stringArray[i]), + stringArray.length, R.id.temperature_value, "temperature value"); + } + z = true; + } else { + z = false; + } + + if (extras.containsKey("weatherIcons")) { + Bitmap[] bitmapArr = (Bitmap[]) extras.get("weatherIcons"); + if (bitmapArr == null) { + Log.w("BcSmartspaceCardWeatherForecast", "Weather icons array is null."); + } else { + updateFields((view, i) -> ((ImageView) view).setImageBitmap(bitmapArr[i]), + bitmapArr.length, R.id.weather_icon, "weather icon"); + } + z = true; + } + + if (!extras.containsKey("timestamps")) { + return z; + } + String[] stringArray2 = extras.getStringArray("timestamps"); + if (stringArray2 == null) { + Log.w("BcSmartspaceCardWeatherForecast", "Timestamps array is null."); + return true; + } + + updateFields((view, i) -> ((TextView) view).setText(stringArray2[i]), + stringArray2.length, R.id.timestamp, "timestamp"); + + return true; + } + + @Override + public final void setTextColor(int color) { + updateFields((view, index) -> ((TextView) view).setTextColor(color), + 4, R.id.temperature_value, "temperature value"); + + updateFields((view, index) -> ((TextView) view).setTextColor(color), + 4, R.id.timestamp, "timestamp"); + } + + public final void updateFields(ItemUpdateFunction itemUpdateFunction, int count, int viewId, String viewName) { + if (getChildCount() < 4) { + Log.w( + "BcSmartspaceCardWeatherForecast", + String.format( + Locale.US, + "Missing %d %s view(s) to update.", + 4 - getChildCount(), + viewName)); + return; + } + if (count < 4) { + int i3 = 4 - count; + Log.w( + "BcSmartspaceCardWeatherForecast", + String.format( + Locale.US, + "Missing %d %s(s). Hiding incomplete columns.", + i3, + viewName)); + if (getChildCount() < 4) { + Log.w("BcSmartspaceCardWeatherForecast", "Missing " + (4 - getChildCount()) + " columns to update."); + } else { + int i4 = 3 - i3; + for (int i = 0; i < 4; i++) { + BcSmartspaceTemplateDataUtils.updateVisibility(getChildAt(i), i <= i4 ? 0 : 8); + } + ((ConstraintLayout.LayoutParams) ((ConstraintLayout) getChildAt(0)).getLayoutParams()).horizontalChainStyle = i3 == 0 ? 1 : 0; + } + } + int min = Math.min(4, count); + for (int i = 0; i < min; i++) { + View findViewById = getChildAt(i).findViewById(viewId); + if (findViewById == null) { + Log.w( + "BcSmartspaceCardWeatherForecast", + String.format( + Locale.US, + "Missing %s view to update at column: %d.", + viewName, + i + 1)); + return; + } + itemUpdateFunction.update(findViewById, i); + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 2 */ + public BcSmartspaceCardWeatherForecast(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java new file mode 100644 index 000000000000..6e53df6fc3ac --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java @@ -0,0 +1,129 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.os.Debug; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.systemui.res.R; + +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; + +public final class BcSmartspaceDataProvider implements BcSmartspaceDataPlugin { + public static final boolean DEBUG = Log.isLoggable("BcSmartspaceDataPlugin", 3); + + public final View.OnAttachStateChangeListener mStateChangeListener; + public final Set mSmartspaceTargetListeners = new CopyOnWriteArraySet<>(); + public List mSmartspaceTargets = new ArrayList<>(); + public final Set mViews = new HashSet<>(); + public final Set mAttachListeners = new HashSet<>(); + public final EventNotifierProxy mEventNotifier = new EventNotifierProxy(); + public BcSmartspaceConfigPlugin mConfigProvider = new DefaultBcSmartspaceConfigProvider(); + + public final class StateChangeListener implements View.OnAttachStateChangeListener { + @Override + public void onViewAttachedToWindow(View view) { + mViews.add(view); + for (View.OnAttachStateChangeListener listener : mAttachListeners) { + listener.onViewAttachedToWindow(view); + } + if (view instanceof BcSmartspaceView) { + ((BcSmartspaceView) view).registerDataProvider(BcSmartspaceDataProvider.this); + } + } + + @Override + public void onViewDetachedFromWindow(View view) { + mViews.remove(view); + for (View.OnAttachStateChangeListener listener : mAttachListeners) { + listener.onViewDetachedFromWindow(view); + } + } + } + + public BcSmartspaceDataProvider() { + mStateChangeListener = new StateChangeListener(); + } + + @Override + public void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) { + mAttachListeners.add(listener); + for (View view : mViews) { + listener.onViewAttachedToWindow(view); + } + } + + @Override + public BcSmartspaceDataPlugin.SmartspaceEventNotifier getEventNotifier() { + return mEventNotifier; + } + + @Override + public BcSmartspaceDataPlugin.SmartspaceView getView(Context context) { + int layoutId = mConfigProvider.isViewPager2Enabled() + ? R.layout.smartspace_enhanced2 + : R.layout.smartspace_enhanced; + + View view = LayoutInflater.from(context).inflate(layoutId, (ViewGroup) null, false); + view.addOnAttachStateChangeListener(mStateChangeListener); + + // Explicitly register data provider. + // Note: The StateChangeListener also attempts this on attach, but doing it here ensures immediate availability. + if (view instanceof BcSmartspaceView) { + ((BcSmartspaceView) view).registerDataProvider(this); + } + + return (BcSmartspaceDataPlugin.SmartspaceView) view; + } + + @Override + public void onTargetsAvailable(List list) { + if (DEBUG) { + Log.d("BcSmartspaceDataPlugin", this + " onTargetsAvailable called. Callers = " + Debug.getCallers(3)); + Log.d("BcSmartspaceDataPlugin", " targets.size() = " + list.size()); + } + + // Filter out feature type 15 (MEDIA?) as seen in reference implementation + mSmartspaceTargets = list.stream() + .filter(target -> target.getFeatureType() != 15) + .collect(Collectors.toList()); + + mSmartspaceTargetListeners.forEach(listener -> listener.onSmartspaceTargetsUpdated(mSmartspaceTargets)); + } + + @Override + public void registerConfigProvider(BcSmartspaceConfigPlugin configPlugin) { + mConfigProvider = configPlugin; + } + + @Override + public void registerListener(BcSmartspaceDataPlugin.SmartspaceTargetListener listener) { + mSmartspaceTargetListeners.add(listener); + listener.onSmartspaceTargetsUpdated(mSmartspaceTargets); + } + + @Override + public void unregisterListener(BcSmartspaceDataPlugin.SmartspaceTargetListener listener) { + mSmartspaceTargetListeners.remove(listener); + } + + @Override + public void setEventDispatcher(BcSmartspaceDataPlugin.SmartspaceEventDispatcher dispatcher) { + mEventNotifier.eventDispatcher = dispatcher; + } + + @Override + public void setIntentStarter(BcSmartspaceDataPlugin.IntentStarter intentStarter) { + mEventNotifier.intentStarterRef = intentStarter; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceEvent.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceEvent.java new file mode 100644 index 000000000000..3c2fb29571aa --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceEvent.java @@ -0,0 +1,21 @@ +package com.google.android.systemui.smartspace; + +public enum BcSmartspaceEvent { + IGNORE(-1), + SMARTSPACE_CARD_RECEIVED(759), + SMARTSPACE_CARD_CLICK(760), + SMARTSPACE_CARD_DISMISS(761), + SMARTSPACE_CARD_SEEN(800), + ENABLED_SMARTSPACE(822), + DISABLED_SMARTSPACE(823), + SMARTSPACE_CARD_SWIPE(1960); + + private final int mId; + + BcSmartspaceEvent(int i) { + mId = i; + } + public final int getId() { + return mId; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceRemoteViewsCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceRemoteViewsCard.java new file mode 100644 index 000000000000..bf81e0959a02 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceRemoteViewsCard.java @@ -0,0 +1,79 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.appwidget.AppWidgetHostView; +import android.content.Context; +import android.view.View; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +public final class BcSmartspaceRemoteViewsCard extends AppWidgetHostView implements SmartspaceCard { + public BcSmartspaceDataPlugin.SmartspaceEventNotifier mEventNotifier; + public BcSmartspaceCardLoggingInfo mLoggingInfo; + public SmartspaceTarget mTarget; + public String mUiSurface; + + public BcSmartspaceRemoteViewsCard(Context context) { + super(context); + setOnLongClickListener(null); + if (BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD.equals(mUiSurface)) { + super.setInteractionHandler(null); + } + } + + @Override + public final void bindData(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo, boolean usePageIndicatorUi) { + mTarget = target; + mLoggingInfo = loggingInfo; + mEventNotifier = eventNotifier; + updateAppWidget(target.getRemoteViews()); + SmartspaceAction headerAction = target.getHeaderAction(); + if (headerAction == null) { + setOnClickListener(null); + super.setInteractionHandler(null); + return; + } + + BcSmartSpaceUtil.setOnClickListener(this, target, headerAction, mEventNotifier, "BcSmartspaceRemoteViewsCard", loggingInfo, 0); + if (BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD.equals(mUiSurface)) { + super.setInteractionHandler( + new BcSmartSpaceUtil.InteractionHandler( + loggingInfo, mEventNotifier, target, headerAction)); + } + } + + @Override + public final BcSmartspaceCardLoggingInfo getLoggingInfo() { + if (mLoggingInfo == null) { + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, 0f)) + .setFeatureType(mTarget != null ? mTarget.getFeatureType() : 0) + .setUid(-1); + return new BcSmartspaceCardLoggingInfo(builder); + } + return mLoggingInfo; + } + + @Override + public final View getView() { + return this; + } + + @Override + public final void setDozeAmount(float dozeAmount) { + } + + @Override + public final void setPrimaryTextColor(int color) { + } + + @Override + public final void setScreenOn(boolean screenOn) { + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceTemplateDataUtils.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceTemplateDataUtils.java new file mode 100644 index 000000000000..c4a38a2db20d --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceTemplateDataUtils.java @@ -0,0 +1,62 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.Text; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.res.R; + +import java.util.Map; + +public abstract class BcSmartspaceTemplateDataUtils { + public static final Map TEMPLATE_TYPE_TO_SECONDARY_CARD_RES = Map.ofEntries(Map.entry(2, R.layout.smartspace_sub_image_template_card), Map.entry(3, R.layout.smartspace_sub_list_template_card), Map.entry(7, R.layout.smartspace_sub_card_template_card), Map.entry(5, R.layout.smartspace_head_to_head_template_card), Map.entry(6, R.layout.smartspace_combined_cards_template_card), Map.entry(4, R.layout.smartspace_carousel_template_card)); + + public static void offsetTextViewForIcon(TextView textView, DoubleShadowIconDrawable iconDrawable, boolean isRtl) { + if (iconDrawable == null) { + textView.setTranslationX(0.0f); + } else { + textView.setTranslationX((isRtl ? 1 : -1) * iconDrawable.mIconInsetSize); + } + } + + public static void setIcon(ImageView imageView, Icon icon) { + if (imageView == null) { + Log.w("BcSmartspaceTemplateDataUtils", "Cannot set. The image view is null"); + return; + } + if (icon == null) { + Log.w("BcSmartspaceTemplateDataUtils", "Cannot set. The given icon is null"); + updateVisibility(imageView, View.GONE); + } + imageView.setImageIcon(icon.getIcon()); + if (icon.getContentDescription() != null) { + imageView.setContentDescription(icon.getContentDescription()); + } + } + + public static void setText(TextView textView, Text text) { + if (textView == null) { + Log.w("BcSmartspaceTemplateDataUtils", "Cannot set. The text view is null"); + return; + } + if (SmartspaceUtils.isEmpty(text)) { + Log.w("BcSmartspaceTemplateDataUtils", "Cannot set. The given text is empty"); + updateVisibility(textView, View.GONE); + } else { + textView.setText(text.getText()); + textView.setEllipsize(text.getTruncateAtType()); + textView.setMaxLines(text.getMaxLines()); + } + } + + public static void updateVisibility(View view, int visibility) { + if (view == null || view.getVisibility() == visibility) { + return; + } + view.setVisibility(visibility); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceView.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceView.java new file mode 100644 index 000000000000..64420c42c098 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceView.java @@ -0,0 +1,769 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceTargetEvent; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Debug; +import android.os.Handler; +import android.os.Parcelable; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.FrameLayout; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; + +import com.android.launcher3.icons.GraphicsUtils; +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; + +import com.google.android.systemui.smartspace.CardPagerAdapter; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLogger; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceSubcardLoggingInfo; +import com.google.android.systemui.smartspace.uitemplate.BaseTemplateCard; + +import com.android.systemui.res.R; + +import java.lang.invoke.VarHandle; +import java.time.DateTimeException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class BcSmartspaceView extends FrameLayout implements BcSmartspaceDataPlugin.SmartspaceTargetListener, BcSmartspaceDataPlugin.SmartspaceView { + public static final boolean DEBUG = Log.isLoggable("BcSmartspaceView", 3); + public CardAdapter mAdapter; + public final ContentObserver mAodObserver; + public Handler mBgHandler; + public int mCardPosition; + public BcSmartspaceConfigPlugin mConfigProvider; + public BcSmartspaceDataPlugin mDataProvider; + public boolean mHasPerformedLongPress; + public boolean mHasPostedLongPress; + public boolean mIsAodEnabled; + public final Set mLastReceivedTargets; + public final Runnable mLongPressCallback; + public PageIndicator mPageIndicator; + public PagerDots mPagerDots; + public List mPendingTargets; + public RecyclerView.ViewHolder mPreInflatedViewHolder; + public float mPreviousDozeAmount; + public final RecyclerView.RecycledViewPool mRecycledViewPool; + public int mScrollState; + public boolean mSplitShadeEnabled; + public Integer mSwipedCardPosition; + public ViewPager mViewPager; + public ViewPager2 mViewPager2; + public final ViewPager2.OnPageChangeCallback mViewPager2OnPageChangeCallback; + public final ViewPager.OnPageChangeListener mViewPagerOnPageChangeListener; + + public final class ViewPager2OnPageChangeCallback extends ViewPager2.OnPageChangeCallback { + @Override + public final void onPageScrollStateChanged(int state) { + Integer num; + SmartspaceCard cardAtPosition; + mScrollState = state; + if (state == ViewPager2.SCROLL_STATE_DRAGGING) { + mSwipedCardPosition = Integer.valueOf(mViewPager2.getCurrentItem()); + } + if (state == ViewPager2.SCROLL_STATE_IDLE) { + if (mConfigProvider.isSwipeEventLoggingEnabled() && (num = mSwipedCardPosition) != null && num.intValue() != mViewPager2.getCurrentItem() && (cardAtPosition = mAdapter.getCardAtPosition(mSwipedCardPosition.intValue())) != null) { + BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_SWIPE, cardAtPosition.getLoggingInfo()); + } + mSwipedCardPosition = null; + } + } + + @Override + public final void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + setSelectedDot(positionOffset, position); + } + + @Override + public final void onPageSelected(int position) { + setSelectedDot(0.0f, position); + onViewPagerPageSelected(BcSmartspaceView.this, position); + } + } + + public final class ViewPagerOnPageChangeListener implements ViewPager.OnPageChangeListener { + @Override + public final void onPageScrollStateChanged(int state) { + Integer num; + SmartspaceCard cardAtPosition; + mScrollState = state; + if (state == ViewPager.SCROLL_STATE_DRAGGING) { + mSwipedCardPosition = mViewPager.getCurrentItem(); + } + if (state == ViewPager.SCROLL_STATE_IDLE) { + if (mConfigProvider.isSwipeEventLoggingEnabled() && (num = mSwipedCardPosition) != null && num.intValue() != mViewPager.getCurrentItem() && (cardAtPosition = mAdapter.getCardAtPosition(mSwipedCardPosition.intValue())) != null) { + BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_SWIPE, cardAtPosition.getLoggingInfo()); + } + mSwipedCardPosition = null; + if (mPendingTargets != null) { + onSmartspaceTargetsUpdated(mPendingTargets); + } + } + } + + @Override + public final void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + setSelectedDot(positionOffset, position); + } + + @Override + public final void onPageSelected(int position) { + setSelectedDot(0.0f, position); + onViewPagerPageSelected(BcSmartspaceView.this, position); + } + } + + public static void onViewPagerPageSelected(BcSmartspaceView view, int position) { + SmartspaceTarget previousTarget = view.mAdapter.getTargetAtPosition(view.mCardPosition); + view.mCardPosition = position; + SmartspaceTarget currentTarget = view.mAdapter.getTargetAtPosition(position); + if (currentTarget != null) { + view.logSmartspaceEvent(currentTarget, view.mCardPosition, BcSmartspaceEvent.SMARTSPACE_CARD_SEEN); + } + if (view.mDataProvider == null) { + Log.w("BcSmartspaceView", "Cannot notify target hidden/shown smartspace events: data provider null"); + return; + } + if (previousTarget == null) { + Log.w("BcSmartspaceView", "Cannot notify target hidden smartspace event: previous target is null."); + } else { + SmartspaceTargetEvent.Builder builder = new SmartspaceTargetEvent.Builder(3); + builder.setSmartspaceTarget(previousTarget); + SmartspaceAction baseAction = previousTarget.getBaseAction(); + if (baseAction != null) { + builder.setSmartspaceActionId(baseAction.getId()); + } + view.mDataProvider.getEventNotifier().notifySmartspaceEvent(builder.build()); + } + if (currentTarget == null) { + Log.w("BcSmartspaceView", "Cannot notify target shown smartspace event: shown card smartspace target null."); + return; + } + SmartspaceTargetEvent.Builder builder = new SmartspaceTargetEvent.Builder(2); + builder.setSmartspaceTarget(currentTarget); + SmartspaceAction baseAction = currentTarget.getBaseAction(); + if (baseAction != null) { + builder.setSmartspaceActionId(baseAction.getId()); + } + view.mDataProvider.getEventNotifier().notifySmartspaceEvent(builder.build()); + } + + public BcSmartspaceView(Context context, AttributeSet attrs) { + super(context, attrs); + mConfigProvider = new DefaultBcSmartspaceConfigProvider(); + mRecycledViewPool = new RecyclerView.RecycledViewPool(); + mPreInflatedViewHolder = null; + mLastReceivedTargets = new ArraySet<>(); + mIsAodEnabled = false; + mCardPosition = 0; + mPreviousDozeAmount = 0.0f; + mScrollState = 0; + mSplitShadeEnabled = false; + mAodObserver = new ContentObserver(new Handler()) { + @Override + public final void onChange(boolean selfChange) { + mIsAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + } + }; + mViewPager2OnPageChangeCallback = new ViewPager2OnPageChangeCallback(); + mViewPagerOnPageChangeListener = new ViewPagerOnPageChangeListener(); + mLongPressCallback = + () -> { + if (mViewPager2 != null && !mHasPerformedLongPress) { + mHasPerformedLongPress = true; + if (mViewPager2.performLongClick()) { + mViewPager2.setPressed(false); + getParent().requestDisallowInterceptTouchEvent(true); + } + } + }; + getContext().getTheme().applyStyle(R.style.DefaultSmartspaceView, false); + } + + public final void cancelScheduledLongPress() { + if (mViewPager2 != null && mHasPostedLongPress) { + mHasPostedLongPress = false; + mViewPager2.removeCallbacks(mLongPressCallback); + } + } + + public int getCurrentCardTopPadding() { + int position = getSelectedPage(); + BcSmartspaceCard legacyCard = mAdapter.getLegacyCardAtPosition(position); + if (legacyCard != null) { + return legacyCard.getPaddingTop(); + } + BaseTemplateCard templateCard = mAdapter.getTemplateCardAtPosition(position); + if (templateCard != null) { + return templateCard.getPaddingTop(); + } + BcSmartspaceRemoteViewsCard remoteViewsCard = + mAdapter.getRemoteViewsCardAtPosition(position); + if (remoteViewsCard != null) { + return remoteViewsCard.getPaddingTop(); + } + return 0; + } + + @Override + public final int getSelectedPage() { + int i = mViewPager != null ? mViewPager.getCurrentItem() : 0; + return mViewPager2 != null ? mViewPager2.getCurrentItem() : i; + } + + public final boolean handleTouchOverride(MotionEvent event, Predicate touchHandler) { + boolean onTouchEvent; + if (mViewPager2 != null) { + int action = event.getAction(); + if (action == 0) { + mHasPerformedLongPress = false; + if (mViewPager2.isLongClickable()) { + cancelScheduledLongPress(); + mHasPostedLongPress = true; + mViewPager2.postDelayed(mLongPressCallback, ViewConfiguration.getLongPressTimeout()); + } + } else if (action == 1 || action == 3) { + cancelScheduledLongPress(); + } + + if (mHasPerformedLongPress) { + cancelScheduledLongPress(); + return true; + } + + if (touchHandler.test(event)) { + cancelScheduledLongPress(); + return true; + } + } + return false; + } + + public final void logSmartspaceEvent(SmartspaceTarget target, int rank, BcSmartspaceEvent event) { + int receivedLatencyMillis; + if (event == BcSmartspaceEvent.SMARTSPACE_CARD_RECEIVED) { + try { + receivedLatencyMillis = (int) Instant.now().minusMillis(target.getCreationTimeMillis()).toEpochMilli(); + } catch (ArithmeticException | DateTimeException e) { + Log.e("BcSmartspaceView", "received_latency_millis will be -1 due to exception ", e); + receivedLatencyMillis = -1; + } + } else { + receivedLatencyMillis = 0; + } + boolean hasValidTemplate = BcSmartspaceCardLoggerUtil.containsValidTemplateType(target.getTemplateData()); + BcSmartspaceCardLoggingInfo.Builder loggingInfoBuilder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create(target)) + .setFeatureType(target.getFeatureType()) + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface( + mAdapter.getUiSurface(), mAdapter.getDozeAmount())) + .setRank(rank) + .setCardinality(mAdapter.getCount()) + .setReceivedLatency(receivedLatencyMillis) + .setUid(-1); + BcSmartspaceSubcardLoggingInfo subcardInfo = + hasValidTemplate + ? BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo( + target.getTemplateData()) + : BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo(target); + loggingInfoBuilder.setSubcardInfo(subcardInfo); + loggingInfoBuilder.setDimensionalInfo( + BcSmartspaceCardLoggerUtil.createDimensionalLoggingInfo(target.getTemplateData())); + BcSmartspaceCardLoggingInfo loggingInfo = + new BcSmartspaceCardLoggingInfo(loggingInfoBuilder); + if (hasValidTemplate) { + BcSmartspaceCardLoggerUtil.tryForcePrimaryFeatureTypeOrUpdateLogInfoFromTemplateData( + loggingInfo, target.getTemplateData()); + } + BcSmartspaceCardLogger.log(event, loggingInfo); + } + + @Override + public final void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mViewPager != null) { + if (mAdapter instanceof CardPagerAdapter) { + mViewPager.setAdapter((CardPagerAdapter) mAdapter); + mViewPager.addOnPageChangeListener(mViewPagerOnPageChangeListener); + if (mPagerDots != null) { + mPagerDots.setNumPages(mAdapter.getCount(), isLayoutRtl()); + } + if (TextUtils.equals(mAdapter.getUiSurface(), BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + try { + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + mBgHandler.post( + () -> { + ContentResolver resolver = getContext().getContentResolver(); + int userId = getContext().getUserId(); + mIsAodEnabled = + Settings.Secure.getIntForUser( + resolver, "doze_always_on", 0, userId) + == 1; + resolver.registerContentObserver( + Settings.Secure.getUriFor("doze_always_on"), + false, + mAodObserver, + -1); + }); + } catch (Exception e) { + Log.w("BcSmartspaceView", "Unable to register Doze Always on content observer.", e); + } + } + if (mDataProvider != null) { + registerDataProvider(mDataProvider); + return; + } + return; + } + } + if (mViewPager2 != null) { + if (mAdapter instanceof CardRecyclerViewAdapter) { + mViewPager2.setAdapter((CardRecyclerViewAdapter) mAdapter); + mViewPager2.registerOnPageChangeCallback(mViewPager2OnPageChangeCallback); + if (mPagerDots != null) { + } + if (TextUtils.equals(mAdapter.getUiSurface(), BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + } + if (mDataProvider != null) { + } + } + } + Log.w("BcSmartspaceView", "Unable to attach the view pager adapter"); + if (mPagerDots != null) { + } + if (TextUtils.equals(mAdapter.getUiSurface(), BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + } + if (mDataProvider != null) { + } + } + + @Override + public final void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + mBgHandler.post( + () -> getContext().getContentResolver().unregisterContentObserver(mAodObserver)); + if (mViewPager != null) { + mViewPager.removeOnPageChangeListener(mViewPagerOnPageChangeListener); + } else if (mViewPager2 != null) { + mViewPager2.unregisterOnPageChangeCallback(mViewPager2OnPageChangeCallback); + } + if (mDataProvider != null) { + mDataProvider.unregisterListener(this); + } + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + View pager = findViewById(R.id.smartspace_card_pager); + if (pager instanceof ViewPager) { + mViewPager = (ViewPager) pager; + mAdapter = new CardPagerAdapter(this, mConfigProvider); + } else { + if (!(pager instanceof ViewPager2)) { + throw new IllegalStateException("smartspace_card_pager is an invalid view type"); + } + mViewPager2 = (ViewPager2) pager; + mAdapter = new CardRecyclerViewAdapter(this, mConfigProvider); + if (mViewPager2 != null) { + CardRecyclerViewAdapter cardRecyclerViewAdapter = new CardRecyclerViewAdapter(this, mConfigProvider); + cardRecyclerViewAdapter.setTargets(Collections.EMPTY_LIST, null); + if (cardRecyclerViewAdapter.smartspaceTargets.size() > 0) { + RecyclerView recyclerView = (RecyclerView) mViewPager2.getChildAt(0); + recyclerView.setRecycledViewPool(mRecycledViewPool); + mPreInflatedViewHolder = cardRecyclerViewAdapter.createViewHolder(recyclerView, cardRecyclerViewAdapter.getItemViewType(0)); + } + } + } + View indicator = findViewById(R.id.smartspace_page_indicator); + if (indicator instanceof PagerDots) { + mPagerDots = (PagerDots) indicator; + } + if (mPagerDots != null) { + int paddingStart = + getResources().getDimensionPixelSize(R.dimen.non_remoteviews_card_padding_start); + mPagerDots.setPaddingRelative(paddingStart, mPagerDots.getPaddingTop(), mPagerDots.getPaddingEnd(), mPagerDots.getPaddingBottom()); + } + } + + @Override + public final boolean onInterceptTouchEvent(MotionEvent event) { + if (mViewPager2 == null) { + return super.onInterceptTouchEvent(event); + } + handleTouchOverride(event, (ev) -> mViewPager2.onInterceptTouchEvent(ev)); + return super.onInterceptTouchEvent(event) || mHasPerformedLongPress; + } + + @Override + public final void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mPreInflatedViewHolder != null) { + mRecycledViewPool.putRecycledView(mPreInflatedViewHolder); + mPreInflatedViewHolder = null; + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + public final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int height = View.MeasureSpec.getSize(heightMeasureSpec); + int desiredHeight = getContext().getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_height); + if (height <= 0 || height >= desiredHeight) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setScaleX(1.0f); + setScaleY(1.0f); + resetPivot(); + return; + } + float scale = (float) height / desiredHeight; + int width = (int) (MeasureSpec.getSize(widthMeasureSpec) / scale); + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(desiredHeight, MeasureSpec.EXACTLY)); + setScaleX(scale); + setScaleY(scale); + setPivotX(0.0f); + setPivotY(desiredHeight / 2.0f); + } + + @Override + public final void onSmartspaceTargetsUpdated(List targets) { + List smartspaceTargets = + targets.stream() + .filter(t -> t instanceof SmartspaceTarget) + .map(t -> (SmartspaceTarget) t) + .collect(Collectors.toList()); + if (DEBUG) { + Log.d("BcSmartspaceView", "@" + Integer.toHexString(hashCode()) + ", onTargetsAvailable called. Callers = " + Debug.getCallers(5)); + StringBuilder sb = new StringBuilder(" targets.size() = "); + sb.append(targets.size()); + Log.d("BcSmartspaceView", sb.toString()); + Log.d("BcSmartspaceView", " targets = " + targets.toString()); + } + if (mViewPager != null && mScrollState != 0 && mAdapter.getCount() > 1 && mViewPager != null) { + mPendingTargets = smartspaceTargets; + return; + } + mPendingTargets = null; + boolean isRtl = isLayoutRtl(); + int selectedPage = getSelectedPage(); + if (isRtl && (mAdapter instanceof CardPagerAdapter)) { + Collections.reverse(smartspaceTargets); + } + View templateCardAtPosition = mAdapter.getTemplateCardAtPosition(selectedPage); + BcSmartspaceCard legacyCardAtPosition = mAdapter.getLegacyCardAtPosition(selectedPage); + BcSmartspaceRemoteViewsCard remoteViewsCardAtPosition = mAdapter.getRemoteViewsCardAtPosition(selectedPage); + if (templateCardAtPosition == null) { + templateCardAtPosition = legacyCardAtPosition != null ? legacyCardAtPosition : remoteViewsCardAtPosition; + } + View cardAtPosition = templateCardAtPosition; + int count = mAdapter.getCount(); + CardAdapter cardAdapter = mAdapter; + if (!(cardAdapter instanceof CardRecyclerViewAdapter)) { + cardAdapter.setTargets(smartspaceTargets); + setTargets(isRtl, selectedPage, cardAtPosition, count); + return; + } + ((CardRecyclerViewAdapter) cardAdapter).setTargets(smartspaceTargets, () -> { + setTargets(isRtl, selectedPage, cardAtPosition, count); + }); + } + + @Override + public final boolean onTouchEvent(MotionEvent event) { + if (mViewPager2 == null) { + return super.onTouchEvent(event); + } + return handleTouchOverride(event, (ev) -> mViewPager2.onTouchEvent(ev)); + } + + @Override + public final void onVisibilityAggregated(boolean isVisible) { + super.onVisibilityAggregated(isVisible); + if (mDataProvider != null) { + mDataProvider.getEventNotifier().notifySmartspaceEvent(new SmartspaceTargetEvent.Builder(isVisible ? 6 : 7).build()); + } + if (mViewPager == null || mScrollState == 0) { + return; + } + mScrollState = 0; + if (mPendingTargets != null) { + onSmartspaceTargetsUpdated(mPendingTargets); + } + } + + @Override + public final void registerConfigProvider(BcSmartspaceConfigPlugin configProvider) { + mConfigProvider = configProvider; + mAdapter.setConfigProvider(configProvider); + } + + @Override + public final void registerDataProvider(BcSmartspaceDataPlugin dataProvider) { + if (mDataProvider != null) { + mDataProvider.unregisterListener(this); + } + mDataProvider = dataProvider; + mDataProvider.registerListener(this); + mAdapter.setDataProvider(mDataProvider); + } + + @Override + public final void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + cancelScheduledLongPress(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public final void setBgHandler(Handler handler) { + mBgHandler = handler; + mAdapter.setBgHandler(handler); + } + + @Override + public final void setDozeAmount(float dozeAmount) { + List previousTargets = mAdapter.getSmartspaceTargets(); + mAdapter.setDozeAmount(dozeAmount); + if (!mAdapter.getSmartspaceTargets().isEmpty()) { + BcSmartspaceTemplateDataUtils.updateVisibility(this, View.VISIBLE); + } + float alpha = 1.0f; + if (mAdapter.getHasAodLockscreenTransition()) { + if (dozeAmount == mPreviousDozeAmount) { + alpha = getAlpha(); + } else if (mPreviousDozeAmount > dozeAmount) { + alpha = 1.0f - dozeAmount; + } else { + alpha = dozeAmount; + } + float threshold = 0.36f; + if (alpha < threshold) { + alpha = (threshold - alpha) / threshold; + } else { + alpha = (alpha - threshold) / 0.64f; + } + } else { + alpha = 1.0f; + } + setAlpha(alpha); + if (mPagerDots != null) { + mPagerDots.setNumPages(mAdapter.getCount(), isLayoutRtl()); + mPagerDots.setAlpha(alpha); + if (mPagerDots.getVisibility() != View.GONE) { + if (dozeAmount == 1.0f) { + BcSmartspaceTemplateDataUtils.updateVisibility(mPagerDots, View.INVISIBLE); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(mPagerDots, View.VISIBLE); + } + } + } + mPreviousDozeAmount = dozeAmount; + if (mAdapter.getHasDifferentTargets() && mAdapter.getSmartspaceTargets() != previousTargets && mAdapter.getCount() > 0) { + if (mAdapter instanceof CardRecyclerViewAdapter) { + setSelectedPage(0); + } else { + setSelectedPage(isLayoutRtl() ? mAdapter.getCount() - 1 : 0); + } + } + int displaySurface = BcSmartSpaceUtil.getLoggingDisplaySurface(mAdapter.getUiSurface(), mAdapter.getDozeAmount()); + if (displaySurface == -1) { + return; + } + if (displaySurface != 3 || mIsAodEnabled) { + if (DEBUG) { + Log.d("BcSmartspaceView", "@" + Integer.toHexString(hashCode()) + ", setDozeAmount: Logging SMARTSPACE_CARD_SEEN, currentSurface = " + displaySurface); + } + SmartspaceTarget target = mAdapter.getTargetAtPosition(mCardPosition); + if (target == null) { + Log.w("BcSmartspaceView", "Current card is not present in the Adapter; cannot log."); + } else { + logSmartspaceEvent(target, mCardPosition, BcSmartspaceEvent.SMARTSPACE_CARD_SEEN); + } + } + } + + @Override + public final void setDozing(boolean dozing) { + if (!dozing && mSplitShadeEnabled && mAdapter.getHasAodLockscreenTransition() && mAdapter.getLockscreenTargets().isEmpty()) { + BcSmartspaceTemplateDataUtils.updateVisibility(this, View.GONE); + } + } + + @Override + public final void setFalsingManager(FalsingManager falsingManager) { + BcSmartSpaceUtil.sFalsingManager = falsingManager; + } + + @Override + public final void setHorizontalPaddings(int padding) { + if (mPagerDots != null) { + mPagerDots.setPaddingRelative(padding, mPagerDots.getPaddingTop(), padding, mPagerDots.getPaddingBottom()); + } + mAdapter.setNonRemoteViewsHorizontalPadding(padding); + } + + @Override + public final void setKeyguardBypassEnabled(boolean enabled) { + mAdapter.setKeyguardBypassEnabled(enabled); + } + + @Override + public final void setMediaTarget(SmartspaceTarget target) { + if (!(mAdapter instanceof CardRecyclerViewAdapter)) { + mAdapter.setMediaTarget(target); + return; + } + CardRecyclerViewAdapter cardRecyclerViewAdapter = (CardRecyclerViewAdapter) mAdapter; + cardRecyclerViewAdapter.mediaTargets.clear(); + if (target != null) { + cardRecyclerViewAdapter.mediaTargets.add(target); + } + cardRecyclerViewAdapter.updateTargetVisibility(null, true); + } + + @Override + public final void setOnLongClickListener(View.OnLongClickListener listener) { + if (mViewPager != null) { + mViewPager.setOnLongClickListener(listener); + return; + } + if (mViewPager2 != null) { + mViewPager2.setOnLongClickListener(listener); + } + } + + @Override + public final void setPrimaryTextColor(int color) { + mAdapter.setPrimaryTextColor(color); + if (mPagerDots != null) { + mPagerDots.primaryColor = color; + mPagerDots.paint.setColor(color); + mPagerDots.invalidate(); + } + } + + @Override + public final void setScreenOn(boolean screenOn) { + if (mViewPager != null && mScrollState != 0) { + mScrollState = 0; + if (mPendingTargets != null) { + onSmartspaceTargetsUpdated(mPendingTargets); + } + } + mAdapter.setScreenOn(screenOn); + } + + public final void setSelectedDot(float f, int i) { + if (mPagerDots != null) { + if (i < 0) { + mPagerDots.getClass(); + return; + } + if (i >= mPagerDots.numPages) { + return; + } + mPagerDots.currentPositionIndex = i; + mPagerDots.currentPositionOffset = f; + mPagerDots.invalidate(); + if (f >= 0.5d) { + i++; + } + mPagerDots.updateCurrentPageIndex(i); + } + } + + public final void setSelectedPage(int i) { + if (mViewPager != null) { + mViewPager.setCurrentItem(i, false); + } else if (mViewPager2 != null) { + mViewPager2.post(() -> { + mViewPager2.setCurrentItem(i, false); + }); + } + setSelectedDot(0.0f, i); + } + + @Override + public final void setSplitShadeEnabled(boolean enabled) { + mSplitShadeEnabled = enabled; + } + + public final void setTargets(boolean z, int i, View view, int i2) { + int count = mAdapter.getCount(); + if (mPagerDots != null) { + mPagerDots.setNumPages(count, z); + } + if (z && (mAdapter instanceof CardPagerAdapter)) { + setSelectedPage(Math.max(0, Math.min(count - 1, count - (i2 - i)))); + } else if (mAdapter instanceof CardRecyclerViewAdapter) { + setSelectedPage(Math.max(0, Math.min(i, count - 1))); + } + for (int i3 = 0; i3 < count; i3++) { + SmartspaceTarget targetAtPosition = mAdapter.getTargetAtPosition(i3); + if (!mLastReceivedTargets.contains(targetAtPosition.getSmartspaceTargetId())) { + logSmartspaceEvent(targetAtPosition, i3, BcSmartspaceEvent.SMARTSPACE_CARD_RECEIVED); + SmartspaceTargetEvent.Builder builder = new SmartspaceTargetEvent.Builder(8); + builder.setSmartspaceTarget(targetAtPosition); + SmartspaceAction baseAction = targetAtPosition.getBaseAction(); + if (baseAction != null) { + builder.setSmartspaceActionId(baseAction.getId()); + } + mDataProvider.getEventNotifier().notifySmartspaceEvent(builder.build()); + } + } + mLastReceivedTargets.clear(); + mLastReceivedTargets.addAll((Collection) mAdapter.getSmartspaceTargets().stream() + .map(target -> ((SmartspaceTarget) target).getSmartspaceTargetId()) + .collect(Collectors.toList())); + } + + @Override + public final void setTimeChangedDelegate(BcSmartspaceDataPlugin.TimeChangedDelegate delegate) { + mAdapter.setTimeChangedDelegate(delegate); + } + + @Override + public final void setUiSurface(String uiSurface) { + if (isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + if (uiSurface == BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN) { + getContext().getTheme().applyStyle(R.style.LauncherSmartspaceView, true); + } + mAdapter.setUiSurface(uiSurface); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardAdapter.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardAdapter.java new file mode 100644 index 000000000000..214053a3c6e1 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardAdapter.java @@ -0,0 +1,61 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import android.os.Handler; + +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.uitemplate.BaseTemplateCard; + +import java.util.List; + +public interface CardAdapter { + SmartspaceCard getCardAtPosition(int i); + + int getCount(); + + float getDozeAmount(); + + boolean getHasAodLockscreenTransition(); + + boolean getHasDifferentTargets(); + + BcSmartspaceCard getLegacyCardAtPosition(int i); + + List getLockscreenTargets(); + + BcSmartspaceRemoteViewsCard getRemoteViewsCardAtPosition(int i); + + List getSmartspaceTargets(); + + SmartspaceTarget getTargetAtPosition(int i); + + BaseTemplateCard getTemplateCardAtPosition(int i); + + String getUiSurface(); + + void setBgHandler(Handler handler); + + void setConfigProvider(BcSmartspaceConfigPlugin bcSmartspaceConfigPlugin); + + void setDataProvider(BcSmartspaceDataPlugin bcSmartspaceDataPlugin); + + void setDozeAmount(float f); + + void setKeyguardBypassEnabled(boolean z); + + void setMediaTarget(SmartspaceTarget smartspaceTarget); + + void setNonRemoteViewsHorizontalPadding(Integer num); + + void setPrimaryTextColor(int i); + + void setScreenOn(boolean z); + + void setTargets(List list); + + void setTimeChangedDelegate(BcSmartspaceDataPlugin.TimeChangedDelegate timeChangedDelegate); + + void setUiSurface(String str); +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardPagerAdapter.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardPagerAdapter.java new file mode 100644 index 000000000000..98562f56e098 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardPagerAdapter.java @@ -0,0 +1,672 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.viewpager.widget.PagerAdapter; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.GraphicsUtils; +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceSubcardLoggingInfo; +import com.google.android.systemui.smartspace.uitemplate.BaseTemplateCard; + +import com.android.systemui.res.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.IntStream; + +public final class CardPagerAdapter extends PagerAdapter implements CardAdapter { + public static final Companion Companion = new Companion(); + private final List _aodTargets = new ArrayList<>(); + private final List _lockscreenTargets = new ArrayList<>(); + private Handler bgHandler; + private BcSmartspaceConfigPlugin configProvider; + private int currentTextColor; + private BcSmartspaceDataPlugin dataProvider; + private float dozeAmount; + private int dozeColor = -1; + private final LazyServerFlagLoader enableCardRecycling = + new LazyServerFlagLoader("enable_card_recycling"); + private final LazyServerFlagLoader enableReducedCardRecycling = + new LazyServerFlagLoader("enable_reduced_card_recycling"); + private boolean hasAodLockscreenTransition; + private boolean hasDifferentTargets; + private boolean keyguardBypassEnabled; + private final List mediaTargets = new ArrayList<>(); + private Integer nonRemoteViewsHorizontalPadding; + private float previousDozeAmount; + private int primaryTextColor; + private final SparseArray recycledCards = new SparseArray<>(); + private final SparseArray recycledLegacyCards = new SparseArray<>(); + private final SparseArray recycledRemoteViewsCards = + new SparseArray<>(); + private BcSmartspaceView root; + private List smartspaceTargets = new ArrayList<>(); + private BcSmartspaceDataPlugin.TimeChangedDelegate timeChangedDelegate; + private TransitionType transitioningTo = TransitionType.NOT_IN_TRANSITION; + private String uiSurface; + private final SparseArray viewHolders = new SparseArray<>(); + + public CardPagerAdapter(BcSmartspaceView root, BcSmartspaceConfigPlugin configProvider) { + this.root = root; + this.configProvider = configProvider; + int color = GraphicsUtils.getAttrColor(root.getContext(), android.R.attr.textColorPrimary); + primaryTextColor = color; + currentTextColor = color; + } + + public static class Companion { + public static boolean useRecycledViewForAction(SmartspaceAction newAction, SmartspaceAction recycledAction) { + Map map = BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES; + if (newAction == null && recycledAction == null) { + return true; + } + if (newAction == null || recycledAction == null) { + return false; + } + Bundle newExtras = newAction.getExtras(); + Bundle recycledExtras = recycledAction.getExtras(); + if (newExtras == null && recycledExtras == null) { + return true; + } + if (newExtras == null || recycledExtras == null) { + return false; + } + Set newKeys = newExtras.keySet(); + Set recycledKeys = recycledExtras.keySet(); + return Objects.equals(newKeys, recycledKeys); + } + + public static boolean useRecycledViewForActionsList(List newActions, List recycledActions) { + Map map = BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES; + if (newActions == null && recycledActions == null) { + return true; + } + + if (newActions == null || recycledActions == null || newActions.size() != recycledActions.size()) { + return false; + } + + return IntStream.range(0, newActions.size()) + .allMatch(i -> useRecycledViewForAction(newActions.get(i), recycledActions.get(i))); + } + + public int getBaseLegacyCardRes(int featureType) { + switch (featureType) { + case -2: + case -1: + case 1: + case 2: + case 3: + case 4: + case 6: + case 9: + case 10: + case 18: + case 20: + case 30: + case 13: + case 14: + case 15: + return R.layout.smartspace_card; + default: + return R.layout.smartspace_card; + } + } + + public final boolean useRecycledViewForNewTarget(SmartspaceTarget newTarget, SmartspaceTarget recycledTarget) { + if (recycledTarget == null) { + return false; + } + if (!newTarget.getSmartspaceTargetId().equals(recycledTarget.getSmartspaceTargetId())) { + return false; + } + if (!useRecycledViewForAction( + newTarget.getHeaderAction(), recycledTarget.getHeaderAction())) { + return false; + } + if (!useRecycledViewForAction( + newTarget.getBaseAction(), recycledTarget.getBaseAction())) { + return false; + } + if (!useRecycledViewForActionsList( + newTarget.getActionChips(), recycledTarget.getActionChips())) { + return false; + } + if (!useRecycledViewForActionsList( + newTarget.getIconGrid(), recycledTarget.getIconGrid())) { + return false; + } + BaseTemplateData newTemplateData = newTarget.getTemplateData(); + BaseTemplateData recycledTemplateData = recycledTarget.getTemplateData(); + if (newTemplateData == null && recycledTemplateData == null) { + return true; + } + return (newTemplateData == null || recycledTemplateData == null || !newTemplateData.equals(recycledTemplateData)) ? false : true; + } + } + + public enum TransitionType { + NOT_IN_TRANSITION, + TO_LOCKSCREEN, + TO_AOD + } + + public final class ViewHolder { + public final BaseTemplateCard card; + public final BcSmartspaceCard legacyCard; + public final int position; + public final BcSmartspaceRemoteViewsCard remoteViewsCard; + public SmartspaceTarget target; + + public ViewHolder(int position, BcSmartspaceCard legacyCard, SmartspaceTarget target, BaseTemplateCard card, BcSmartspaceRemoteViewsCard remoteViewsCard) { + this.position = position; + this.legacyCard = legacyCard; + this.target = target; + this.card = card; + this.remoteViewsCard = remoteViewsCard; + } + + public void setTarget(SmartspaceTarget target) { + this.target = target; + } + } + + public static final int getBaseLegacyCardRes(int featureType) { + return Companion.getBaseLegacyCardRes(featureType); + } + + public static final boolean useRecycledViewForNewTarget(SmartspaceTarget newTarget, SmartspaceTarget recycledTarget) { + return Companion.useRecycledViewForNewTarget(newTarget, recycledTarget); + } + + public final void addDefaultDateCardIfEmpty(List targets) { + if (targets.isEmpty()) { + targets.add(new SmartspaceTarget.Builder("date_card_794317_92634", new ComponentName(root.getContext(), (Class) CardPagerAdapter.class), root.getContext().getUser()).setFeatureType(1).setTemplateData(new BaseTemplateData.Builder(1).build()).build()); + } + } + + @Override + public final void destroyItem(ViewGroup container, int position, Object object) { + ViewHolder holder = (ViewHolder) object; + if (holder.legacyCard != null) { + SmartspaceTarget smartspaceTarget = holder.legacyCard.mTarget; + if (smartspaceTarget != null && enableCardRecycling.get()) { + recycledLegacyCards.put(BcSmartSpaceUtil.getFeatureType(smartspaceTarget), holder.legacyCard); + } + container.removeView(holder.legacyCard); + } + if (holder.card != null) { + SmartspaceTarget smartspaceTarget2 = holder.card.mTarget; + if (smartspaceTarget2 != null && enableCardRecycling.get()) { + recycledCards.put(smartspaceTarget2.getFeatureType(), holder.card); + } + container.removeView(holder.card); + } + if (holder.remoteViewsCard != null) { + if (enableCardRecycling.get()) { + Log.d("SsCardPagerAdapter", "[rmv] Caching RemoteViews card"); + recycledRemoteViewsCards.put(BcSmartSpaceUtil.getFeatureType(holder.target), holder.remoteViewsCard); + } + Log.d("SsCardPagerAdapter", "[rmv] Removing RemoteViews card"); + container.removeView(holder.remoteViewsCard); + } + if (viewHolders.get(position) == holder) { + viewHolders.remove(position); + } + } + + @Override + public final SmartspaceCard getCardAtPosition(int position) { + ViewHolder holder = viewHolders.get(position); + if (holder != null) { + if (holder.card != null) { + return holder.card; + } + if (holder.legacyCard != null) { + return holder.legacyCard; + } + return holder.remoteViewsCard; + } + return null; + } + + @Override + public final int getCount() { + return smartspaceTargets.size(); + } + + @Override + public final float getDozeAmount() { + return dozeAmount; + } + + @Override + public final boolean getHasAodLockscreenTransition() { + return hasAodLockscreenTransition; + } + + @Override + public final boolean getHasDifferentTargets() { + return hasDifferentTargets; + } + + @Override + public int getItemPosition(Object object) { + ViewHolder holder = (ViewHolder) object; + SmartspaceTarget currentTarget = getTargetAtPosition(holder.position); + if (holder.target == currentTarget) { + return POSITION_UNCHANGED; + } + if (currentTarget != null + && BcSmartSpaceUtil.getFeatureType(currentTarget) + == BcSmartSpaceUtil.getFeatureType(holder.target) + && currentTarget + .getSmartspaceTargetId() + .equals(holder.target.getSmartspaceTargetId())) { + holder.setTarget(currentTarget); + onBindViewHolder(holder); + return POSITION_UNCHANGED; + } + return POSITION_NONE; + } + + @Override + public final BcSmartspaceCard getLegacyCardAtPosition(int position) { + ViewHolder holder = viewHolders.get(position); + if (holder != null) { + return holder.legacyCard; + } + return null; + } + + @Override + public final List getLockscreenTargets() { + return (mediaTargets.isEmpty() || !keyguardBypassEnabled) ? _lockscreenTargets : mediaTargets; + } + + @Override + public final BcSmartspaceRemoteViewsCard getRemoteViewsCardAtPosition(int position) { + ViewHolder holder = viewHolders.get(position); + if (holder != null) { + return holder.remoteViewsCard; + } + return null; + } + + @Override + public final List getSmartspaceTargets() { + return smartspaceTargets; + } + + @Override + public final SmartspaceTarget getTargetAtPosition(int position) { + if (smartspaceTargets.isEmpty() || position < 0 || position >= smartspaceTargets.size()) { + return null; + } + return smartspaceTargets.get(position); + } + + @Override + public final BaseTemplateCard getTemplateCardAtPosition(int position) { + ViewHolder holder = viewHolders.get(position); + if (holder != null) { + return holder.card; + } + return null; + } + + @Override + public final String getUiSurface() { + return uiSurface; + } + + @Override + public final Object instantiateItem(ViewGroup container, int position) { + BcSmartspaceCard bcSmartspaceCard; + ViewHolder holder; + BaseTemplateData.SubItemLoggingInfo loggingInfo; + SmartspaceTarget target = smartspaceTargets.get(position); + BcSmartspaceCard bcSmartspaceCard2 = null; + if (target.getRemoteViews() != null) { + Log.i("SsCardPagerAdapter", "[rmv] Use RemoteViews for the feature: " + target.getFeatureType()); + BcSmartspaceRemoteViewsCard remoteViewsCard = enableCardRecycling.get() ? recycledRemoteViewsCards.removeReturnOld(BcSmartSpaceUtil.getFeatureType(target)) : null; + if (remoteViewsCard == null) { + remoteViewsCard = new BcSmartspaceRemoteViewsCard(container.getContext()); + remoteViewsCard.mUiSurface = uiSurface; + remoteViewsCard.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + holder = new ViewHolder(position, null, target, null, remoteViewsCard); + container.addView(remoteViewsCard); + } else { + boolean containsValidTemplateType = BcSmartspaceCardLoggerUtil.containsValidTemplateType(target.getTemplateData()); + if (containsValidTemplateType) { + Log.i("SsCardPagerAdapter", "Use UI template for the feature: " + target.getFeatureType()); + BaseTemplateCard templateCard = enableCardRecycling.get() ? (BaseTemplateCard) recycledCards.removeReturnOld(target.getFeatureType()) : null; + if (templateCard == null || (enableReducedCardRecycling.get() && !Companion.useRecycledViewForNewTarget(target, templateCard.mTarget))) { + BaseTemplateData templateData = target.getTemplateData(); + BaseTemplateData.SubItemInfo primaryItem = templateData != null ? templateData.getPrimaryItem() : null; + int layoutRes = (primaryItem == null || (SmartspaceUtils.isEmpty(primaryItem.getText()) && primaryItem.getIcon() == null) || ((loggingInfo = primaryItem.getLoggingInfo()) != null && loggingInfo.getFeatureType() == 1)) ? R.layout.smartspace_base_template_card_with_date : R.layout.smartspace_base_template_card; + LayoutInflater inflater = LayoutInflater.from(container.getContext()); + templateCard = (BaseTemplateCard) inflater.inflate(layoutRes, (ViewGroup) container, false); + templateCard.mUiSurface = uiSurface; + if (templateCard.mDateView != null && TextUtils.equals(uiSurface, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + if (templateCard.mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + templateCard.mDateView.mUpdatesOnAod = true; + } + if (nonRemoteViewsHorizontalPadding != null) { + templateCard.setPaddingRelative(nonRemoteViewsHorizontalPadding, templateCard.getPaddingTop(), nonRemoteViewsHorizontalPadding, templateCard.getPaddingBottom()); + } + templateCard.mBgHandler = bgHandler != null ? bgHandler : null; + if (templateCard.mDateView != null) { + templateCard.mDateView.mBgHandler = templateCard.mBgHandler; + } + if (templateCard.mDateView != null) { + if (templateCard.mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + templateCard.mDateView.mTimeChangedDelegate = timeChangedDelegate; + } + Map templateTypeToSecondaryCardRes = + BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES; + Integer secondaryRes = + templateTypeToSecondaryCardRes.get( + target.getTemplateData().getTemplateType()); + if (templateData != null && secondaryRes != null) { + BcSmartspaceCardSecondary secondaryCard = (BcSmartspaceCardSecondary) inflater.inflate(secondaryRes, (ViewGroup) templateCard, false); + Log.i("SsCardPagerAdapter", "Secondary card is found"); + templateCard.setSecondaryCard(secondaryCard); + } + } + holder = new ViewHolder(position, null, target, templateCard, null); + container.addView(templateCard); + } else { + BcSmartspaceCard legacyCard = enableCardRecycling.get() ? recycledLegacyCards.removeReturnOld(BcSmartSpaceUtil.getFeatureType(target)) : null; + if (legacyCard == null || (enableReducedCardRecycling.get() && !Companion.useRecycledViewForNewTarget(target, legacyCard.mTarget))) { + int featureType = BcSmartSpaceUtil.getFeatureType(target); + LayoutInflater inflater = LayoutInflater.from(container.getContext()); + int layoutRes = Companion.getBaseLegacyCardRes(featureType); + if (layoutRes == 0) { + Log.w( + "SsCardPagerAdapter", + "No legacy card can be created for feature type: " + featureType); + } else { + legacyCard = (BcSmartspaceCard) inflater.inflate(layoutRes, (ViewGroup) container, false); + legacyCard.mUiSurface = uiSurface; + if (nonRemoteViewsHorizontalPadding != null) { + legacyCard.setPaddingRelative(nonRemoteViewsHorizontalPadding, legacyCard.getPaddingTop(), nonRemoteViewsHorizontalPadding, legacyCard.getPaddingBottom()); + } + Integer secondaryRes = (Integer) BcSmartSpaceUtil.FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP.get(featureType); + if (secondaryRes != null) { + legacyCard.setSecondaryCard((BcSmartspaceCardSecondary) inflater.inflate(secondaryRes, (ViewGroup) legacyCard, false)); + } + } + bcSmartspaceCard = legacyCard; + } else { + bcSmartspaceCard = legacyCard; + } + holder = new ViewHolder(position, bcSmartspaceCard, target, null, null); + if (bcSmartspaceCard != null) { + container.addView(bcSmartspaceCard); + } + } + } + onBindViewHolder(holder); + viewHolders.put(position, holder); + return holder; + } + + @Override + public final boolean isViewFromObject(View view, Object object) { + ViewHolder holder = (ViewHolder) object; + return view == holder.legacyCard || view == holder.card || view == holder.remoteViewsCard; + } + + public final void onBindViewHolder(ViewHolder holder) { + SmartspaceTarget target = smartspaceTargets.get(holder.position); + boolean hasValidTemplate = BcSmartspaceCardLoggerUtil.containsValidTemplateType(target.getTemplateData()); + BcSmartspaceCardLoggingInfo.Builder loggingInfoBuilder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create(target)) + .setFeatureType(target.getFeatureType()) + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(uiSurface, dozeAmount)) + .setRank(holder.position) + .setCardinality(smartspaceTargets.size()) + .setUid(-1); + BcSmartspaceSubcardLoggingInfo subcardInfo = + hasValidTemplate + ? BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo( + target.getTemplateData()) + : BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo(target); + loggingInfoBuilder.setSubcardInfo(subcardInfo); + loggingInfoBuilder.setDimensionalInfo( + BcSmartspaceCardLoggerUtil.createDimensionalLoggingInfo(target.getTemplateData())); + BcSmartspaceCardLoggingInfo loggingInfo = + new BcSmartspaceCardLoggingInfo(loggingInfoBuilder); + if (target.getRemoteViews() != null) { + if (holder.remoteViewsCard == null) { + Log.w("SsCardPagerAdapter", "[rmv] No RemoteViews card view can be binded"); + return; + } + Log.d("SsCardPagerAdapter", "[rmv] Refreshing RemoteViews card"); + holder.remoteViewsCard.bindData(target, dataProvider != null ? dataProvider.getEventNotifier() : null, loggingInfo, smartspaceTargets.size() > 1); + return; + } + if (!hasValidTemplate) { + if (holder.legacyCard == null) { + Log.w("SsCardPagerAdapter", "No legacy card view can be binded"); + return; + } + holder.legacyCard.bindData(target, dataProvider != null ? dataProvider.getEventNotifier() : null, loggingInfo, smartspaceTargets.size() > 1); + holder.legacyCard.setPrimaryTextColor(currentTextColor); + holder.legacyCard.setDozeAmount(dozeAmount); + return; + } + if (target.getTemplateData() == null) { + throw new IllegalStateException("Required value was null."); + } + BcSmartspaceCardLoggerUtil.tryForcePrimaryFeatureTypeOrUpdateLogInfoFromTemplateData(loggingInfo, target.getTemplateData()); + if (holder.card == null) { + Log.w("SsCardPagerAdapter", "No ui-template card view can be binded"); + return; + } + holder.card.bindData(target, dataProvider != null ? dataProvider.getEventNotifier() : null, loggingInfo, smartspaceTargets.size() > 1); + holder.card.setPrimaryTextColor(currentTextColor); + holder.card.setDozeAmount(dozeAmount); + } + + @Override + public final void setBgHandler(Handler handler) { + bgHandler = handler; + } + + @Override + public final void setConfigProvider(BcSmartspaceConfigPlugin configProvider) { + this.configProvider = configProvider; + } + + @Override + public final void setDataProvider(BcSmartspaceDataPlugin dataProvider) { + this.dataProvider = dataProvider; + } + + @Override + public void setDozeAmount(float dozeAmount) { + this.dozeAmount = dozeAmount; + TransitionType newTransition = + previousDozeAmount > dozeAmount + ? TransitionType.TO_LOCKSCREEN + : previousDozeAmount < dozeAmount + ? TransitionType.TO_AOD + : TransitionType.NOT_IN_TRANSITION; + transitioningTo = newTransition; + previousDozeAmount = dozeAmount; + updateTargetVisibility(); + updateCurrentTextColor(); + } + + @Override + public final void setKeyguardBypassEnabled(boolean enabled) { + keyguardBypassEnabled = enabled; + updateTargetVisibility(); + } + + @Override + public void setMediaTarget(SmartspaceTarget target) { + mediaTargets.clear(); + if (target != null) { + mediaTargets.add(target); + } + updateTargetVisibility(); + notifyDataSetChanged(); + } + + @Override + public final void setNonRemoteViewsHorizontalPadding(Integer padding) { + nonRemoteViewsHorizontalPadding = padding; + for (int i = 0; i < viewHolders.size(); i++) { + int keyAt = viewHolders.keyAt(i); + BcSmartspaceCard legacyCardAtPosition = getLegacyCardAtPosition(keyAt); + if (legacyCardAtPosition != null) { + legacyCardAtPosition.setPaddingRelative(padding, legacyCardAtPosition.getPaddingTop(), padding, legacyCardAtPosition.getPaddingBottom()); + } + BaseTemplateCard templateCardAtPosition = getTemplateCardAtPosition(keyAt); + if (templateCardAtPosition != null) { + templateCardAtPosition.setPaddingRelative(padding, templateCardAtPosition.getPaddingTop(), padding, templateCardAtPosition.getPaddingBottom()); + } + } + for (int i = 0; i < recycledCards.size(); i++) { + BaseTemplateCard baseTemplateCard = (BaseTemplateCard) recycledCards.valueAt(i); + baseTemplateCard.setPaddingRelative(padding, baseTemplateCard.getPaddingTop(), padding, baseTemplateCard.getPaddingBottom()); + } + for (int i = 0; i < recycledLegacyCards.size(); i++) { + BcSmartspaceCard bcSmartspaceCard = (BcSmartspaceCard) recycledLegacyCards.valueAt(i); + bcSmartspaceCard.setPaddingRelative(padding, bcSmartspaceCard.getPaddingTop(), padding, bcSmartspaceCard.getPaddingBottom()); + } + } + + @Override + public final void setPrimaryTextColor(int color) { + primaryTextColor = color; + updateCurrentTextColor(); + } + + @Override + public final void setScreenOn(boolean screenOn) { + for (int i = 0; i < viewHolders.size(); i++) { + ViewHolder holder = viewHolders.valueAt(i); + if (holder != null && holder.card != null) { + holder.card.setScreenOn(screenOn); + } + } + } + + @Override + public void setTargets(List targets) { + _aodTargets.clear(); + _lockscreenTargets.clear(); + hasDifferentTargets = false; + for (SmartspaceTarget target : targets) { + if (target.getFeatureType() == 34) { + continue; + } + int screenExtra = + target.getBaseAction() != null && target.getBaseAction().getExtras() != null + ? target.getBaseAction().getExtras().getInt("SCREEN_EXTRA", 3) + : 3; + if ((screenExtra & 2) != 0) { + _aodTargets.add(target); + } + if ((screenExtra & 1) != 0) { + _lockscreenTargets.add(target); + } + if (screenExtra != 3) { + hasDifferentTargets = true; + } + } + if (!configProvider.isDefaultDateWeatherDisabled()) { + addDefaultDateCardIfEmpty(_aodTargets); + addDefaultDateCardIfEmpty(_lockscreenTargets); + } + updateTargetVisibility(); + notifyDataSetChanged(); + } + + @Override + public final void setTimeChangedDelegate(BcSmartspaceDataPlugin.TimeChangedDelegate delegate) { + timeChangedDelegate = delegate; + } + + @Override + public final void setUiSurface(String uiSurface) { + this.uiSurface = uiSurface; + } + + public final void updateCurrentTextColor() { + currentTextColor = ColorUtils.blendARGB(primaryTextColor, dozeColor, dozeAmount); + for (int i = 0; i < viewHolders.size(); i++) { + ViewHolder holder = viewHolders.valueAt(i); + if (holder != null) { + if (holder.legacyCard != null) { + holder.legacyCard.setPrimaryTextColor(currentTextColor); + holder.legacyCard.setDozeAmount(dozeAmount); + } + if (holder.card != null) { + holder.card.setPrimaryTextColor(currentTextColor); + holder.card.setDozeAmount(dozeAmount); + } + } + } + } + + public void updateTargetVisibility() { + List targetList = + !mediaTargets.isEmpty() + ? mediaTargets + : hasDifferentTargets ? _aodTargets : getLockscreenTargets(); + List lockscreenTargets = getLockscreenTargets(); + boolean shouldUpdate = + smartspaceTargets != targetList + && (dozeAmount == 1f + || (dozeAmount >= 0.36f + && transitioningTo == TransitionType.TO_AOD)); + boolean shouldUpdateLockscreen = + smartspaceTargets != lockscreenTargets + && (dozeAmount == 0f + || (1f - dozeAmount >= 0.36f + && transitioningTo == TransitionType.TO_LOCKSCREEN)); + if (shouldUpdate || shouldUpdateLockscreen) { + smartspaceTargets = shouldUpdate ? targetList : lockscreenTargets; + notifyDataSetChanged(); + } + hasAodLockscreenTransition = targetList != lockscreenTargets; + if (configProvider.isDefaultDateWeatherDisabled() && !BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN.equals(uiSurface)) { + BcSmartspaceTemplateDataUtils.updateVisibility( + root, smartspaceTargets.isEmpty() ? View.GONE : View.VISIBLE); + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java new file mode 100644 index 000000000000..3ce9a5a7be68 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java @@ -0,0 +1,566 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews; + +import androidx.recyclerview.widget.AdapterListUpdateCallback; +import androidx.recyclerview.widget.AsyncDifferConfig; +import androidx.recyclerview.widget.AsyncListDiffer; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.GraphicsUtils; +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.IcuDateTextView; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceSubcardLoggingInfo; +import com.google.android.systemui.smartspace.uitemplate.BaseTemplateCard; + +import com.android.systemui.res.R; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/* compiled from: go/retraceme af8e0b46c0cb0ee2c99e9b6d0c434e5c0b686fd9230eaab7fb9a40e3a9d0cf6f */ +/* loaded from: classes2.dex */ +public final class CardRecyclerViewAdapter extends RecyclerView.Adapter implements CardAdapter { + public static final Set legacySecondaryCardResourceIdSet = + BcSmartSpaceUtil.FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP.values().stream() + .collect(Collectors.toSet()); + public static final Set templateSecondaryCardResourceIdSet = + BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES.values().stream() + .collect(Collectors.toSet()); + public final List _aodTargets; + public float dozeAmount; + public final List _lockscreenTargets; + public Handler bgHandler; + public BcSmartspaceConfigPlugin configProvider; + public int currentTextColor; + public BcSmartspaceDataPlugin dataProvider; + public final int dozeColor; + public boolean hasAodLockscreenTransition; + public boolean hasDifferentTargets; + public boolean keyguardBypassEnabled; + public final AsyncListDiffer mDiffer; + public final List mediaTargets; + public Integer nonRemoteViewsHorizontalPadding; + public float previousDozeAmount; + public int primaryTextColor; + public final BcSmartspaceView root; + public List smartspaceTargets; + public BcSmartspaceDataPlugin.TimeChangedDelegate timeChangedDelegate; + public TransitionType transitioningTo; + public String uiSurface; + public final SparseArray viewHolders; + + /* compiled from: go/retraceme af8e0b46c0cb0ee2c99e9b6d0c434e5c0b686fd9230eaab7fb9a40e3a9d0cf6f */ + public final class DiffUtilItemCallback extends DiffUtil.ItemCallback { + @Override // androidx.recyclerview.widget.DiffUtil.Callback + public final boolean areItemsTheSame(SmartspaceTarget oldItem, SmartspaceTarget newItem) { + return oldItem.getSmartspaceTargetId().equals(newItem.getSmartspaceTargetId()); + } + + @Override // androidx.recyclerview.widget.DiffUtil.Callback + public final boolean areContentsTheSame(SmartspaceTarget oldItem, SmartspaceTarget newItem) { + return false; + } + } + + public enum TransitionType { + NOT_IN_TRANSITION, + TO_LOCKSCREEN, + TO_AOD + } + + /* compiled from: go/retraceme af8e0b46c0cb0ee2c99e9b6d0c434e5c0b686fd9230eaab7fb9a40e3a9d0cf6f */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public SmartspaceCard card; + + public ViewHolder(SmartspaceCard card) { + super(card.getView()); + this.card = card; + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + public CardRecyclerViewAdapter(BcSmartspaceView root, BcSmartspaceConfigPlugin configProvider) { + DiffUtilItemCallback diffUtilItemCallback = new DiffUtilItemCallback(); + AsyncDifferConfig asyncDifferConfig = + new AsyncDifferConfig.Builder(diffUtilItemCallback) + .build(); + mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), asyncDifferConfig); + mDiffer.addListListener((previousList, currentList) -> { + }); + this.root = root; + viewHolders = new SparseArray<>(); + smartspaceTargets = new ArrayList<>(); + _aodTargets = new ArrayList<>(); + _lockscreenTargets = new ArrayList<>(); + mediaTargets = new ArrayList<>(); + dozeColor = -1; + int attrColor = GraphicsUtils.getAttrColor(root.getContext(), android.R.attr.textColorPrimary); + primaryTextColor = attrColor; + currentTextColor = attrColor; + configProvider = configProvider; + transitioningTo = TransitionType.NOT_IN_TRANSITION; + } + + public static boolean isTemplateCard(SmartspaceTarget target) { + return target.getTemplateData() != null && BcSmartspaceCardLoggerUtil.containsValidTemplateType(target.getTemplateData()); + } + + public final void addDefaultDateCardIfEmpty(List targets) { + if (targets.isEmpty()) { + targets.add(new SmartspaceTarget.Builder("date_card_794317_92634", new ComponentName(root.getContext(), CardRecyclerViewAdapter.class), root.getContext().getUser()).setFeatureType(1).setTemplateData(new BaseTemplateData.Builder(1).build()).build()); + } + } + + @Override + public final SmartspaceCard getCardAtPosition(int position) { + ViewHolder holder = viewHolders.get(position); + if (holder != null) { + return holder.card; + } + return null; + } + + @Override + public final int getCount() { + return smartspaceTargets.size(); + } + + @Override + public final float getDozeAmount() { + return dozeAmount; + } + + @Override + public final boolean getHasAodLockscreenTransition() { + return hasAodLockscreenTransition; + } + + @Override + public final boolean getHasDifferentTargets() { + return hasDifferentTargets; + } + + @Override + public final int getItemCount() { + return mDiffer.getCurrentList().size(); + } + + @Override + public final int getItemViewType(int position) { + SmartspaceTarget target = mDiffer.getCurrentList().get(position); + BaseTemplateData templateData = target.getTemplateData(); + if (target.getRemoteViews() != null) { + return target.getRemoteViews().getLayoutId(); + } + if (!isTemplateCard(target)) { + Integer layoutId = (Integer) BcSmartSpaceUtil.FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP.get(BcSmartSpaceUtil.getFeatureType(target)); + return layoutId != null ? layoutId : R.layout.smartspace_card; + } + BaseTemplateData.SubItemInfo primaryItem = templateData.getPrimaryItem(); + if (primaryItem == null) { + return R.layout.smartspace_base_template_card_with_date; + } + if (SmartspaceUtils.isEmpty(primaryItem.getText()) && primaryItem.getIcon() == null) { + return R.layout.smartspace_base_template_card_with_date; + } + BaseTemplateData.SubItemLoggingInfo loggingInfo = primaryItem.getLoggingInfo(); + if (loggingInfo != null && loggingInfo.getFeatureType() == 1) { + return R.layout.smartspace_base_template_card_with_date; + } + Integer layoutId = (Integer) BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES.get(templateData.getTemplateType()); + return layoutId != null ? layoutId : R.layout.smartspace_base_template_card; + } + + @Override + public BcSmartspaceCard getLegacyCardAtPosition(int position) { + SmartspaceCard card = getCardAtPosition(position); + return card instanceof BcSmartspaceCard ? (BcSmartspaceCard) card : null; + } + + @Override + public List getLockscreenTargets() { + return (mediaTargets.isEmpty() || !keyguardBypassEnabled) ? _lockscreenTargets : mediaTargets; + } + + @Override + public BcSmartspaceRemoteViewsCard getRemoteViewsCardAtPosition(int position) { + SmartspaceCard card = getCardAtPosition(position); + return card instanceof BcSmartspaceRemoteViewsCard + ? (BcSmartspaceRemoteViewsCard) card + : null; + } + + @Override + public List getSmartspaceTargets() { + return smartspaceTargets; + } + + @Override + public SmartspaceTarget getTargetAtPosition(int position) { + if (position < 0 || position >= getItemCount()) { + return null; + } + return mDiffer.getCurrentList().get(position); + } + + @Override + public BaseTemplateCard getTemplateCardAtPosition(int position) { + SmartspaceCard card = getCardAtPosition(position); + return card instanceof BaseTemplateCard ? (BaseTemplateCard) card : null; + } + + @Override + public String getUiSurface() { + return uiSurface; + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // androidx.recyclerview.widget.RecyclerView.Adapter + public final void onBindViewHolder(ViewHolder holder, int position) { + SmartspaceTarget target = mDiffer.getCurrentList().get(position); + boolean isTemplateCard = isTemplateCard(target); + BcSmartspaceCardLoggingInfo.Builder loggingInfoBuilder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create(target)) + .setFeatureType(target.getFeatureType()) + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(uiSurface, dozeAmount)) + .setRank(position) + .setCardinality(smartspaceTargets.size()) + .setUid(-1); + if (isTemplateCard) { + loggingInfoBuilder.setSubcardInfo( + BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo(target.getTemplateData())); + } else { + loggingInfoBuilder.setSubcardInfo( + BcSmartspaceCardLoggerUtil.createSubcardLoggingInfo(target)); + } + loggingInfoBuilder.setDimensionalInfo( + BcSmartspaceCardLoggerUtil.createDimensionalLoggingInfo(target.getTemplateData())); + BcSmartspaceCardLoggingInfo loggingInfo = + new BcSmartspaceCardLoggingInfo(loggingInfoBuilder); + SmartspaceCard card = holder.card; + if (target.getRemoteViews() != null) { + if (!(card instanceof BcSmartspaceRemoteViewsCard)) { + Log.w("SsCardRecyclerViewAdapter", "[rmv] No RemoteViews card view can be binded"); + return; + } + Log.d("SsCardRecyclerViewAdapter", "[rmv] Refreshing RemoteViews card"); + } else if (isTemplateCard) { + if (target.getTemplateData() == null) { + throw new IllegalStateException("Required value was null."); + } + BcSmartspaceCardLoggerUtil.tryForcePrimaryFeatureTypeOrUpdateLogInfoFromTemplateData(loggingInfo, target.getTemplateData()); + if (!(card instanceof BaseTemplateCard)) { + Log.w("SsCardRecyclerViewAdapter", "No ui-template card view can be binded"); + return; + } + ((BaseTemplateCard) card).mBgHandler = bgHandler; + IcuDateTextView icuDateTextView = ((BaseTemplateCard) card).mDateView; + if (icuDateTextView != null) { + icuDateTextView.mBgHandler = bgHandler; + } + } else if (!(card instanceof BcSmartspaceCard)) { + Log.w("SsCardRecyclerViewAdapter", "No legacy card view can be binded"); + return; + } + card.bindData(target, dataProvider != null ? dataProvider.getEventNotifier() : null, loggingInfo, smartspaceTargets.size() > 1); + card.setPrimaryTextColor(currentTextColor); + card.setDozeAmount(dozeAmount); + viewHolders.put(position, holder); + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + SmartspaceCard card; + Integer secondaryCardResId = null; + if (templateSecondaryCardResourceIdSet.contains(viewType) || viewType == R.layout.smartspace_base_template_card_with_date || viewType == R.layout.smartspace_base_template_card) { + if (templateSecondaryCardResourceIdSet.contains(viewType)) { + secondaryCardResId = viewType; + viewType = R.layout.smartspace_base_template_card; + } + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + BaseTemplateCard templateCard = (BaseTemplateCard) inflater.inflate(viewType, parent, false); + templateCard.mUiSurface = uiSurface; + if (templateCard.mDateView != null && TextUtils.equals(uiSurface, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + if (templateCard.mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + templateCard.mDateView.mUpdatesOnAod = true; + } + if (nonRemoteViewsHorizontalPadding != null) { + templateCard.setPaddingRelative(nonRemoteViewsHorizontalPadding, templateCard.getPaddingTop(), nonRemoteViewsHorizontalPadding, templateCard.getPaddingBottom()); + } + if (templateCard.mDateView != null) { + if (templateCard.mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + templateCard.mDateView.mTimeChangedDelegate = timeChangedDelegate; + } + if (secondaryCardResId != null) { + BcSmartspaceCardSecondary secondaryCard = (BcSmartspaceCardSecondary) inflater.inflate(secondaryCardResId, (ViewGroup) templateCard, false); + Log.i("SsCardRecyclerViewAdapter", "Secondary card is found"); + templateCard.setSecondaryCard(secondaryCard); + } + card = templateCard; + } else { + if (legacySecondaryCardResourceIdSet.contains(viewType) || viewType == R.layout.smartspace_card) { + if (legacySecondaryCardResourceIdSet.contains(viewType)) { + secondaryCardResId = viewType; + viewType = R.layout.smartspace_card; + } + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + BcSmartspaceCard legacyCard = (BcSmartspaceCard) inflater.inflate(viewType, parent, false); + legacyCard.mUiSurface = uiSurface; + if (nonRemoteViewsHorizontalPadding != null) { + legacyCard.setPaddingRelative(nonRemoteViewsHorizontalPadding, legacyCard.getPaddingTop(), nonRemoteViewsHorizontalPadding, legacyCard.getPaddingBottom()); + } + if (secondaryCardResId != null) { + legacyCard.setSecondaryCard((BcSmartspaceCardSecondary) inflater.inflate(secondaryCardResId, (ViewGroup) legacyCard, false)); + } + card = legacyCard; + } else { + BcSmartspaceRemoteViewsCard remoteViewsCard = new BcSmartspaceRemoteViewsCard(parent.getContext()); + remoteViewsCard.mUiSurface = uiSurface; + card = remoteViewsCard; + } + } + card.getView() + .setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + return new ViewHolder(card); + } + + @Override + public final void setBgHandler(Handler handler) { + bgHandler = handler; + } + @Override + public final void setConfigProvider(BcSmartspaceConfigPlugin configProvider) { + this.configProvider = configProvider; + } + + @Override + public final void setDataProvider(BcSmartspaceDataPlugin dataProvider) { + this.dataProvider = dataProvider; + } + + @Override + public final void setDozeAmount(float dozeAmount) { + this.dozeAmount = dozeAmount; + transitioningTo = previousDozeAmount > dozeAmount ? TransitionType.TO_LOCKSCREEN : previousDozeAmount < dozeAmount ? TransitionType.TO_AOD : TransitionType.NOT_IN_TRANSITION; + previousDozeAmount = dozeAmount; + updateTargetVisibility(null, false); + updateCurrentTextColor(); + } + + @Override + public final void setKeyguardBypassEnabled(boolean enabled) { + keyguardBypassEnabled = enabled; + updateTargetVisibility(null, false); + } + + @Override + public void setMediaTarget(SmartspaceTarget target) { + mediaTargets.clear(); + if (target != null) { + mediaTargets.add(target); + } + updateTargetVisibility(null, true); + } + + @Override + public final void setNonRemoteViewsHorizontalPadding(Integer padding) { + nonRemoteViewsHorizontalPadding = padding; + for (int i = 0; i < viewHolders.size(); i++) { + int key = viewHolders.keyAt(i); + BcSmartspaceCard legacyCard = getLegacyCardAtPosition(key); + if (legacyCard != null) { + legacyCard.setPaddingRelative(padding, legacyCard.getPaddingTop(), padding, legacyCard.getPaddingBottom()); + } + BaseTemplateCard templateCard = getTemplateCardAtPosition(key); + if (templateCard != null) { + templateCard.setPaddingRelative(padding, templateCard.getPaddingTop(), padding, templateCard.getPaddingBottom()); + } + } + } + + @Override + public final void setPrimaryTextColor(int color) { + primaryTextColor = color; + updateCurrentTextColor(); + } + + @Override + public void setScreenOn(boolean screenOn) { + for (int i = 0; i < viewHolders.size(); i++) { + ViewHolder holder = viewHolders.get(viewHolders.keyAt(i)); + if (holder != null) { + holder.card.setScreenOn(screenOn); + } + } + } + + @Override + public final void setTargets(List targets) { + setTargets(targets, null); + } + + @Override + public final void setTimeChangedDelegate(BcSmartspaceDataPlugin.TimeChangedDelegate delegate) { + timeChangedDelegate = delegate; + } + + @Override + public final void setUiSurface(String uiSurface) { + this.uiSurface = uiSurface; + } + + public void updateCurrentTextColor() { + currentTextColor = ColorUtils.blendARGB(primaryTextColor, dozeColor, dozeAmount); + for (int i = 0; i < viewHolders.size(); i++) { + ViewHolder holder = viewHolders.get(viewHolders.keyAt(i)); + if (holder != null) { + holder.card.setPrimaryTextColor(currentTextColor); + holder.card.setDozeAmount(dozeAmount); + } + } + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + /* JADX WARN: Removed duplicated region for block: B:14:0x003b */ + /* JADX WARN: Removed duplicated region for block: B:23:0x0055 */ + /* JADX WARN: Removed duplicated region for block: B:25:0x0068 A[ADDED_TO_REGION] */ + /* JADX WARN: Removed duplicated region for block: B:28:0x007f */ + /* JADX WARN: Removed duplicated region for block: B:31:0x008a */ + /* JADX WARN: Removed duplicated region for block: B:40:? A[ADDED_TO_REGION, RETURN, SYNTHETIC] */ + /* JADX WARN: Removed duplicated region for block: B:42:0x005d */ + /* + Code decompiled incorrectly, please refer to instructions dump. + */ + public final void updateTargetVisibility(Runnable runnable, boolean z) { + boolean z2; + boolean z3; + List targets = !mediaTargets.isEmpty() ? mediaTargets : hasDifferentTargets ? _aodTargets : getLockscreenTargets(); + List lockscreenTargets = getLockscreenTargets(); + if (smartspaceTargets != targets) { + if (dozeAmount == 1.0f || (dozeAmount >= 0.36f && transitioningTo == TransitionType.TO_AOD)) { + z2 = true; + if (smartspaceTargets != lockscreenTargets) { + if (dozeAmount == 0.0f || (1.0f - dozeAmount >= 0.36f && transitioningTo == TransitionType.TO_LOCKSCREEN)) { + z3 = true; + if (z2) { + Log.d("SsCardRecyclerViewAdapter", "Updating Smartspace targets to targets for AOD"); + smartspaceTargets = targets; + } else if (z3) { + Log.d("SsCardRecyclerViewAdapter", "Updating Smartspace targets to targets for Lockscreen"); + smartspaceTargets = lockscreenTargets; + } + if (!z || z2 || z3) { + viewHolders.clear(); + mDiffer.submitList(new ArrayList<>(smartspaceTargets), runnable); + } + hasAodLockscreenTransition = targets != lockscreenTargets; + if (!configProvider.isDefaultDateWeatherDisabled() || !BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN.equalsIgnoreCase(uiSurface)) { + return; + } + BcSmartspaceTemplateDataUtils.updateVisibility(root, smartspaceTargets.isEmpty() ? View.GONE : View.VISIBLE); + return; + } + } + z3 = false; + if (z2) { + } + if (!z) { + } + viewHolders.clear(); + mDiffer.submitList(new ArrayList<>(smartspaceTargets), runnable); + hasAodLockscreenTransition = targets != lockscreenTargets; + if (configProvider.isDefaultDateWeatherDisabled()) { + return; + } else { + return; + } + } + } + z2 = false; + if (smartspaceTargets != lockscreenTargets) { + } + z3 = false; + if (z2) { + } + if (!z) { + } + viewHolders.clear(); + mDiffer.submitList(new ArrayList<>(smartspaceTargets), runnable); + hasAodLockscreenTransition = targets != lockscreenTargets; + if (configProvider.isDefaultDateWeatherDisabled()) { + } + } + + public final void setTargets(List list, Runnable runnable) { + Bundle extras; + _aodTargets.clear(); + _lockscreenTargets.clear(); + hasDifferentTargets = false; + Iterator it = list.iterator(); + while (it.hasNext()) { + SmartspaceTarget smartspaceTarget = it.next(); + if (smartspaceTarget.getFeatureType() == 34 || + (smartspaceTarget.getRemoteViews() == null && !isTemplateCard(smartspaceTarget) && smartspaceTarget.getFeatureType() == 1)) { + Log.e("SsCardRecyclerViewAdapter", "No card can be created for target: " + smartspaceTarget.getFeatureType()); + } else { + SmartspaceAction baseAction = smartspaceTarget.getBaseAction(); + + int screenExtra = (baseAction == null || (extras = baseAction.getExtras()) == null) + ? 3 + : extras.getInt("SCREEN_EXTRA", 3); + + if ((screenExtra & 2) != 0) { + _aodTargets.add(smartspaceTarget); + } + if ((screenExtra & 1) != 0) { + _lockscreenTargets.add(smartspaceTarget); + } + if (screenExtra != 3) { + hasDifferentTargets = true; + } + } + } + + if (!configProvider.isDefaultDateWeatherDisabled()) { + addDefaultDateCardIfEmpty(_aodTargets); + addDefaultDateCardIfEmpty(_lockscreenTargets); + } + + updateTargetVisibility(runnable, true); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceDataProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceDataProvider.java new file mode 100644 index 000000000000..c363be028927 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceDataProvider.java @@ -0,0 +1,93 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.systemui.res.R; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public final class DateSmartspaceDataProvider + implements BcSmartspaceDataPlugin { + public Set mAttachListeners = + new HashSet<>(); + public EventNotifierProxy mEventNotifier = new EventNotifierProxy(); + public final View.OnAttachStateChangeListener mStateChangeListener = + new StateChangeListener(); + public Set mViews = new HashSet<>(); + + public final class StateChangeListener + implements View.OnAttachStateChangeListener { + @Override + public final void onViewAttachedToWindow(View view) { + mViews.add(view); + Iterator iterator = + mAttachListeners.iterator(); + while (iterator.hasNext()) { + View.OnAttachStateChangeListener listener = iterator.next(); + listener.onViewAttachedToWindow(view); + } + } + + @Override + public final void onViewDetachedFromWindow(View view) { + mViews.remove(view); + Iterator iterator = + mAttachListeners.iterator(); + while (iterator.hasNext()) { + View.OnAttachStateChangeListener listener = iterator.next(); + listener.onViewDetachedFromWindow(view); + } + } + } + + @Override + public final void + addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) { + mAttachListeners.add(listener); + Iterator iterator = mViews.iterator(); + while (iterator.hasNext()) { + View view = iterator.next(); + listener.onViewAttachedToWindow(view); + } + } + + @Override + public final BcSmartspaceDataPlugin.SmartspaceEventNotifier + getEventNotifier() { + return mEventNotifier; + } + + @Override + public final BcSmartspaceDataPlugin.SmartspaceView + getLargeClockView(Context context) { + View view = LayoutInflater.from(context).inflate( + R.layout.date_plus_extras_large, (ViewGroup)null, false); + view.setId(R.id.date_smartspace_view_large); + view.addOnAttachStateChangeListener(this.mStateChangeListener); + return (BcSmartspaceDataPlugin.SmartspaceView)view; + } + + @Override + public final BcSmartspaceDataPlugin.SmartspaceView getView(Context context) { + View view = LayoutInflater.from(context).inflate(R.layout.date_plus_extras, + (ViewGroup)null, false); + view.addOnAttachStateChangeListener(this.mStateChangeListener); + return (BcSmartspaceDataPlugin.SmartspaceView)view; + } + + @Override + public final void setEventDispatcher( + BcSmartspaceDataPlugin.SmartspaceEventDispatcher eventDispatcher) { + mEventNotifier.eventDispatcher = eventDispatcher; + } + + @Override + public final void + setIntentStarter(BcSmartspaceDataPlugin.IntentStarter intentStarter) { + mEventNotifier.intentStarterRef = intentStarter; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceView.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceView.java new file mode 100644 index 000000000000..dd6854428eed --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/DateSmartspaceView.java @@ -0,0 +1,282 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.content.ComponentName; +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.internal.graphics.ColorUtils; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLogger; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.lang.invoke.VarHandle; + +/* compiled from: go/retraceme af8e0b46c0cb0ee2c99e9b6d0c434e5c0b686fd9230eaab7fb9a40e3a9d0cf6f */ +/* loaded from: classes2.dex */ +public class DateSmartspaceView extends LinearLayout implements BcSmartspaceDataPlugin.SmartspaceView { + public static final boolean DEBUG = Log.isLoggable("DateSmartspaceView", Log.DEBUG); + public final ContentObserver mAodSettingsObserver; + public Handler mBgHandler; + public int mCurrentTextColor; + public BcSmartspaceDataPlugin mDataProvider; + public final SmartspaceAction mDateAction; + public final SmartspaceTarget mDateTarget; + public IcuDateTextView mDateView; + public final DoubleShadowIconDrawable mDndIconDrawable; + public ImageView mDndImageView; + public float mDozeAmount; + public boolean mIsAodEnabled; + public BcSmartspaceCardLoggingInfo mLoggingInfo; + public final BcNextAlarmData mNextAlarmData; + public final DoubleShadowIconDrawable mNextAlarmIconDrawable; + public DoubleShadowTextView mNextAlarmTextView; + public int mPrimaryTextColor; + public String mUiSurface; + + public DateSmartspaceView(Context context) { + this(context, null); + } + + @Override + public final void onAttachedToWindow() { + Handler handler; + super.onAttachedToWindow(); + if (TextUtils.equals(mUiSurface, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + try { + handler = mBgHandler; + } catch (Exception e) { + Log.w("DateSmartspaceView", "Unable to register DOZE_ALWAYS_ON content observer: ", e); + } + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + mBgHandler.post(() -> { + getContext().getContentResolver().registerContentObserver( + Settings.Secure.getUriFor("doze_always_on"), + false, + mAodSettingsObserver, + -1); + }); + mIsAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + } + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create(mDateTarget)) + .setFeatureType(mDateTarget.getFeatureType()) + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount)) + .setUid(-1); + mLoggingInfo = new BcSmartspaceCardLoggingInfo(builder); + BcSmartSpaceUtil.setOnClickListener(mDateView, mDateTarget, mDateAction, mDataProvider != null ? mDataProvider.getEventNotifier() : null, "DateSmartspaceView", mLoggingInfo, 0); + } + + @Override + public final void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + mBgHandler.post(() -> { + getContext().getContentResolver().unregisterContentObserver(mAodSettingsObserver); + }); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mDateView = findViewById(R.id.date); + mNextAlarmTextView = findViewById(R.id.alarm_text_view); + mDndImageView = findViewById(R.id.dnd_icon); + } + + @Override + public final void registerDataProvider(BcSmartspaceDataPlugin dataProvider) { + mDataProvider = dataProvider; + } + + @Override + public final void setBgHandler(Handler handler) { + mBgHandler = handler; + mDateView.mBgHandler = handler; + } + + @Override + public final void setDnd(Drawable image, String description) { + if (image == null) { + BcSmartspaceTemplateDataUtils.updateVisibility(mDndImageView, View.GONE); + } else { + mDndIconDrawable.setIcon(image.mutate()); + mDndImageView.setImageDrawable(mDndIconDrawable); + mDndImageView.setContentDescription(description); + BcSmartspaceTemplateDataUtils.updateVisibility(mDndImageView, View.VISIBLE); + } + updateColorForExtras(); + } + + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView + public final void setDozeAmount(float dozeAmount) { + int loggingSurface; + mDozeAmount = dozeAmount; + mCurrentTextColor = ColorUtils.blendARGB(mPrimaryTextColor, -1, dozeAmount); + mDateView.setTextColor(mCurrentTextColor); + updateColorForExtras(); + if (mLoggingInfo == null || (loggingSurface = BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount)) == -1) { + return; + } + if (loggingSurface != 3 || mIsAodEnabled) { + if (DEBUG) { + Log.d("DateSmartspaceView", "@" + Integer.toHexString(hashCode()) + ", setDozeAmount: Logging SMARTSPACE_CARD_SEEN, loggingSurface = " + loggingSurface); + } + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(mLoggingInfo.mInstanceId) + .setFeatureType(mLoggingInfo.mFeatureType) + .setDisplaySurface(loggingSurface) + .setUid(mLoggingInfo.mUid); + BcSmartspaceCardLogger.log( + BcSmartspaceEvent.SMARTSPACE_CARD_SEEN, new BcSmartspaceCardLoggingInfo(builder)); + if (mNextAlarmData.mImage != null) { + BcSmartspaceCardLoggingInfo.Builder alarmBuilder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create("upcoming_alarm_card_94510_12684")) + .setFeatureType(23) + .setDisplaySurface(loggingSurface) + .setUid(mLoggingInfo.mUid); + BcSmartspaceCardLogger.log( + BcSmartspaceEvent.SMARTSPACE_CARD_SEEN, + new BcSmartspaceCardLoggingInfo(alarmBuilder)); + } + } + } + + @Override + public final void setFalsingManager(FalsingManager falsingManager) { + BcSmartSpaceUtil.sFalsingManager = falsingManager; + } + + @Override + public final void setNextAlarm(Drawable image, String description) { + mNextAlarmData.mImage = image; + if (image != null) { + image.mutate(); + } + mNextAlarmData.mDescription = description; + if (mNextAlarmData.mImage == null) { + BcSmartspaceTemplateDataUtils.updateVisibility(mNextAlarmTextView, View.GONE); + } else { + mNextAlarmTextView.setContentDescription(getContext().getString(R.string.accessibility_next_alarm, description)); + String displayText = TextUtils.isEmpty(null) ? mNextAlarmData.mDescription : mNextAlarmData.mDescription + " · null"; + mNextAlarmTextView.setText(displayText); + int iconSize = getContext().getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size); + mNextAlarmData.mImage.setBounds(0, 0, iconSize, iconSize); + mNextAlarmIconDrawable.setIcon(mNextAlarmData.mImage); + mNextAlarmTextView.setCompoundDrawablesRelative(mNextAlarmIconDrawable, null, null, null); + BcSmartspaceTemplateDataUtils.updateVisibility(mNextAlarmTextView, View.VISIBLE); + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier = mDataProvider == null ? null : mDataProvider.getEventNotifier(); + int loggingSurface = BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount); + BcSmartspaceCardLoggingInfo loggingInfo = new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create("upcoming_alarm_card_94510_12684")) + .setFeatureType(23) + .setDisplaySurface(loggingSurface) + .build(); + BcSmartSpaceUtil.setOnClickListener(mNextAlarmTextView, null, BcNextAlarmData.SHOW_ALARMS_ACTION, eventNotifier, "BcNextAlarmData", loggingInfo, 0); + } + updateColorForExtras(); + } + + @Override + public final void setPrimaryTextColor(int color) { + mPrimaryTextColor = color; + mCurrentTextColor = ColorUtils.blendARGB(color, -1, mDozeAmount); + mDateView.setTextColor(mCurrentTextColor); + updateColorForExtras(); + } + + @Override + public final void setScreenOn(boolean screenOn) { + if (mDateView != null) { + mDateView.mIsInteractive = screenOn; + mDateView.rescheduleTicker(); + } + } + + @Override + public final void setTimeChangedDelegate(BcSmartspaceDataPlugin.TimeChangedDelegate delegate) { + if (mDateView != null) { + if (mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + mDateView.mTimeChangedDelegate = delegate; + } + } + + @Override + public final void setUiSurface(String uiSurface) { + if (isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + mUiSurface = uiSurface; + if (TextUtils.equals(uiSurface, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + if (mDateView.isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + mDateView.mUpdatesOnAod = true; + } + } + + public final void updateColorForExtras() { + if (mNextAlarmTextView != null) { + mNextAlarmTextView.setTextColor(mCurrentTextColor); + mNextAlarmIconDrawable.setTint(mCurrentTextColor); + } + if (mDndImageView == null || mDndImageView.getDrawable() == null) { + return; + } + mDndImageView.getDrawable().setTint(mCurrentTextColor); + mDndImageView.invalidate(); + } + + public DateSmartspaceView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /* JADX WARN: Type inference failed for: r4v10, types: [com.google.android.systemui.smartspace.DateSmartspaceView$1] */ + public DateSmartspaceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mUiSurface = null; + mDozeAmount = 0.0f; + mDateTarget = new SmartspaceTarget.Builder("date_card_794317_92634", new ComponentName(getContext(), getClass()), getContext().getUser()).setFeatureType(1).build(); + mDateAction = new SmartspaceAction.Builder("dateId", "Date").setIntent(BcSmartSpaceUtil.getOpenCalendarIntent()).build(); + mNextAlarmData = new BcNextAlarmData(); + mAodSettingsObserver = new ContentObserver(new Handler()) { // from class: com.google.android.systemui.smartspace.DateSmartspaceView.1 + /* JADX DEBUG: Don't trust debug lines info. Lines numbers was adjusted: min line is 1 */ + @Override // android.database.ContentObserver + public final void onChange(boolean selfChange) { + boolean isAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + if (mIsAodEnabled == isAodEnabled) { + return; + } + mIsAodEnabled = isAodEnabled; + } + }; + context.getTheme().applyStyle(R.style.Smartspace, false); + mNextAlarmIconDrawable = new DoubleShadowIconDrawable(context); + mDndIconDrawable = new DoubleShadowIconDrawable(context); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/DefaultBcSmartspaceConfigProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/DefaultBcSmartspaceConfigProvider.java new file mode 100644 index 000000000000..c49ee0e9fd66 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/DefaultBcSmartspaceConfigProvider.java @@ -0,0 +1,20 @@ +package com.google.android.systemui.smartspace; + +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; + +public final class DefaultBcSmartspaceConfigProvider implements BcSmartspaceConfigPlugin { + @Override + public final boolean isDefaultDateWeatherDisabled() { + return false; + } + + @Override + public final boolean isSwipeEventLoggingEnabled() { + return false; + } + + @Override + public final boolean isViewPager2Enabled() { + return false; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowIconDrawable.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowIconDrawable.java new file mode 100644 index 000000000000..96e4fb21e027 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowIconDrawable.java @@ -0,0 +1,121 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.RecordingCanvas; +import android.graphics.RenderEffect; +import android.graphics.RenderNode; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; + +import com.android.internal.graphics.ColorUtils; + +import com.android.systemui.res.R; + +public final class DoubleShadowIconDrawable extends Drawable { + public final int mAmbientShadowRadius; + public final int mCanvasSize; + public RenderNode mDoubleShadowNode; + public InsetDrawable mIconDrawable; + public final int mIconInsetSize; + public final int mKeyShadowOffsetX; + public final int mKeyShadowOffsetY; + public final int mKeyShadowRadius; + public boolean mShowShadow; + + public DoubleShadowIconDrawable(Context context) { + this(context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size), context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_inset), context); + } + + @Override + public final void draw(Canvas canvas) { + if (canvas.isHardwareAccelerated() && mDoubleShadowNode != null && mShowShadow) { + if (!mDoubleShadowNode.hasDisplayList()) { + RecordingCanvas beginRecording = mDoubleShadowNode.beginRecording(); + if (mIconDrawable != null) { + mIconDrawable.draw(beginRecording); + } + mDoubleShadowNode.endRecording(); + } + canvas.drawRenderNode(mDoubleShadowNode); + } + if (mIconDrawable != null) { + mIconDrawable.draw(canvas); + } + } + + @Override + public final int getIntrinsicHeight() { + return mCanvasSize; + } + + @Override + public final int getIntrinsicWidth() { + return mCanvasSize; + } + + @Override + public final int getOpacity() { + return -2; + } + + @Override + public final void setAlpha(int alpha) { + if (mIconDrawable != null) { + mIconDrawable.setAlpha(alpha); + } + } + + @Override + public final void setColorFilter(ColorFilter colorFilter) { + if (mIconDrawable != null) { + mIconDrawable.setColorFilter(colorFilter); + } + } + + public final void setIcon(Drawable drawable) { + RenderNode renderNode = null; + if (drawable == null) { + mIconDrawable = null; + return; + } + mIconDrawable = new InsetDrawable(drawable, mIconInsetSize); + mIconDrawable.setBounds(0, 0, mCanvasSize, mCanvasSize); + if (mIconDrawable != null) { + RenderNode shadowNode = new RenderNode("DoubleShadowNode"); + shadowNode.setPosition(0, 0, mCanvasSize, mCanvasSize); + RenderEffect ambientShadowEffect = RenderEffect.createColorFilterEffect(new PorterDuffColorFilter(Color.argb(48, 0, 0, 0), PorterDuff.Mode.MULTIPLY), RenderEffect.createOffsetEffect(0f, 0f, RenderEffect.createBlurEffect(mAmbientShadowRadius, mAmbientShadowRadius, Shader.TileMode.CLAMP))); + RenderEffect keyShadowEffect = RenderEffect.createColorFilterEffect(new PorterDuffColorFilter(Color.argb(72, 0, 0, 0), PorterDuff.Mode.MULTIPLY), RenderEffect.createOffsetEffect(mKeyShadowOffsetX, mKeyShadowOffsetY, RenderEffect.createBlurEffect(mKeyShadowRadius, mKeyShadowRadius, Shader.TileMode.CLAMP))); + if (ambientShadowEffect != null && keyShadowEffect != null) { + shadowNode.setRenderEffect(RenderEffect.createBlendModeEffect(ambientShadowEffect, keyShadowEffect, BlendMode.DARKEN)); + renderNode = shadowNode; + } + } + mDoubleShadowNode = renderNode; + } + + @Override + public final void setTint(int color) { + if (mIconDrawable != null) { + mIconDrawable.setTint(color); + } + mShowShadow = ColorUtils.calculateLuminance(color) > 0.5d; + } + + public DoubleShadowIconDrawable(int iconSize, int insetSize, Context context) { + mShowShadow = true; + mIconInsetSize = insetSize; + mCanvasSize = insetSize * 2 + iconSize; + mAmbientShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.ambient_text_shadow_radius); + mKeyShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_radius); + mKeyShadowOffsetX = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_dx); + mKeyShadowOffsetY = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_dy); + setBounds(0, 0, mCanvasSize, mCanvasSize); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowTextView.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowTextView.java new file mode 100644 index 000000000000..fc7dac260da1 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/DoubleShadowTextView.java @@ -0,0 +1,61 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.core.graphics.ColorUtils; + +import com.android.systemui.res.R; + +public class DoubleShadowTextView extends TextView { + public final float mAmbientShadowBlur; + public final int mAmbientShadowColor; + public boolean mDrawShadow; + public final float mKeyShadowBlur; + public final int mKeyShadowColor; + public final float mKeyShadowOffsetX; + public final float mKeyShadowOffsetY; + + public DoubleShadowTextView(Context context) { + this(context, null); + } + + @Override + public final void onDraw(Canvas canvas) { + if (!mDrawShadow) { + getPaint().clearShadowLayer(); + super.onDraw(canvas); + return; + } + getPaint().setShadowLayer(mAmbientShadowBlur, 0.0f, 0.0f, mAmbientShadowColor); + super.onDraw(canvas); + canvas.save(); + canvas.clipRect(getScrollX(), getExtendedPaddingTop() + getScrollY(), getWidth() + getScrollX(), getHeight() + getScrollY()); + getPaint().setShadowLayer(mKeyShadowBlur, mKeyShadowOffsetX, mKeyShadowOffsetY, mKeyShadowColor); + super.onDraw(canvas); + canvas.restore(); + } + + @Override + public final void setTextColor(int color) { + super.setTextColor(color); + mDrawShadow = ColorUtils.calculateLuminance(color) > 0.5d; + } + + public DoubleShadowTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DoubleShadowTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDrawShadow = ColorUtils.calculateLuminance(getCurrentTextColor()) > 0.5d; + mKeyShadowBlur = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_radius); + mKeyShadowOffsetX = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_dx); + mKeyShadowOffsetY = context.getResources().getDimensionPixelSize(R.dimen.key_text_shadow_dy); + mKeyShadowColor = context.getResources().getColor(R.color.key_text_shadow_color); + mAmbientShadowBlur = context.getResources().getDimensionPixelSize(R.dimen.ambient_text_shadow_radius); + mAmbientShadowColor = context.getResources().getColor(R.color.ambient_text_shadow_color); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/EventNotifierProxy.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/EventNotifierProxy.java new file mode 100644 index 000000000000..a92c3abbbb61 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/EventNotifierProxy.java @@ -0,0 +1,22 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTargetEvent; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +public final class EventNotifierProxy implements BcSmartspaceDataPlugin.SmartspaceEventNotifier { + public BcSmartspaceDataPlugin.SmartspaceEventDispatcher eventDispatcher; + public BcSmartspaceDataPlugin.IntentStarter intentStarterRef; + + @Override + public final BcSmartspaceDataPlugin.IntentStarter getIntentStarter() { + return intentStarterRef; + } + + @Override + public final void notifySmartspaceEvent(SmartspaceTargetEvent smartspaceTargetEvent) { + if (eventDispatcher != null) { + eventDispatcher.notifySmartspaceEvent(smartspaceTargetEvent); + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/IcuDateTextView.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/IcuDateTextView.java new file mode 100644 index 000000000000..bde2fbf7f5a0 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/IcuDateTextView.java @@ -0,0 +1,194 @@ +package com.google.android.systemui.smartspace; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.icu.text.DateFormat; +import android.icu.text.DisplayContext; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.android.systemui.res.R; + +import java.util.Locale; +import java.util.Objects; + +public class IcuDateTextView extends DoubleShadowTextView { + private static final String TAG = "IcuDateTextView"; + public final ContentObserver mAodSettingsObserver; + public Handler mBgHandler; + public DateFormat mFormatter; + public Handler mHandler; + public final BroadcastReceiver mIntentReceiver; + public boolean mIsAodEnabled; + public Boolean mIsInteractive; + public String mText; + public final Runnable mTimeChangedCallback; + public BcSmartspaceDataPlugin.TimeChangedDelegate mTimeChangedDelegate; + public boolean mUpdatesOnAod; + + public final class DefaultTimeChangedDelegate implements BcSmartspaceDataPlugin.TimeChangedDelegate, Runnable { + public Handler mHandler; + public Runnable mTimeChangedCallback; + + @Override + public final void register(Runnable callback) { + if (mTimeChangedCallback != null) { + unregister(); + } + mTimeChangedCallback = callback; + run(); + } + + @Override + public final void run() { + if (mTimeChangedCallback != null) { + mTimeChangedCallback.run(); + if (mHandler != null) { + long now = SystemClock.uptimeMillis(); + long delay = 60000 - (now % 60000); + mHandler.postAtTime(this, now + delay); + } + } + } + + @Override + public final void unregister() { + mHandler.removeCallbacks(this); + mTimeChangedCallback = null; + } + } + + public IcuDateTextView(Context context) { + this(context, null); + } + + @Override + public final void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mUpdatesOnAod) { + try { + if (mBgHandler == null) { + Log.wtf(TAG, "Must set background handler when mUpdatesOnAod is set to avoid making binder calls on main thread"); + getContext().getContentResolver().registerContentObserver(Settings.Secure.getUriFor("doze_always_on"), false, mAodSettingsObserver, -1); + } else { + mBgHandler.post(() -> getContext().getContentResolver().registerContentObserver( + Settings.Secure.getUriFor("doze_always_on"), false, mAodSettingsObserver, -1)); + } + } catch (Exception e) { + Log.w(TAG, "Unable to register DOZE_ALWAYS_ON content observer: ", e); + } + mIsAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + } + + mHandler = new Handler(); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction("Intent.ACTION_TIME_CHANGED"); + intentFilter.addAction("Intent.ACTION_TIMEZONE_CHANGED"); + + if (mBgHandler == null) { + Log.w(TAG, "mBgHandler is not set! Fallback to make binder calls on main thread."); + getContext().registerReceiver(mIntentReceiver, intentFilter, Context.RECEIVER_EXPORTED); + } else { + mBgHandler.post(() -> getContext().registerReceiver(mIntentReceiver, intentFilter, Context.RECEIVER_EXPORTED)); + } + + if (mTimeChangedDelegate == null) { + DefaultTimeChangedDelegate delegate = new DefaultTimeChangedDelegate(); + delegate.mHandler = mHandler; + mTimeChangedDelegate = delegate; + } + onTimeChanged(true); + } + + @Override + public final void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mHandler != null) { + if (mBgHandler == null) { + Log.w(TAG, "mBgHandler is not set! Fallback to make binder calls on main thread."); + getContext().unregisterReceiver(mIntentReceiver); + } else { + mBgHandler.post(() -> { + try { + getContext().unregisterReceiver(mIntentReceiver); + } catch (IllegalArgumentException ignored) {} + }); + } + mTimeChangedDelegate.unregister(); + mHandler = null; + } + if (mUpdatesOnAod) { + if (mBgHandler == null) { + Log.wtf(TAG, "Must set background handler when mUpdatesOnAod is set to avoid making binder calls on main thread"); + getContext().getContentResolver().unregisterContentObserver(mAodSettingsObserver); + } else { + mBgHandler.post(() -> getContext().getContentResolver().unregisterContentObserver(mAodSettingsObserver)); + } + } + } + + public final void onTimeChanged(boolean forceUpdateFormatter) { + if (mFormatter == null || forceUpdateFormatter) { + mFormatter = DateFormat.getInstanceForSkeleton(getContext().getString(R.string.smartspace_icu_date_pattern), Locale.getDefault()); + mFormatter.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE); + } + String newText = mFormatter.format(Long.valueOf(System.currentTimeMillis())); + if (Objects.equals(mText, newText)) { + return; + } + mText = newText; + setText(newText); + setContentDescription(newText); + } + + @Override + public final void onVisibilityAggregated(boolean isVisible) { + super.onVisibilityAggregated(isVisible); + rescheduleTicker(); + } + + public final void rescheduleTicker() { + if (mHandler == null) { + return; + } + mTimeChangedDelegate.unregister(); + if ((mIsInteractive == null || mIsInteractive || (mUpdatesOnAod && mIsAodEnabled)) && isAggregatedVisible()) { + mTimeChangedDelegate.register(mTimeChangedCallback); + } + } + + public IcuDateTextView(Context context, AttributeSet attrs) { + super(context, attrs, 0); + + mAodSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + boolean isAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + if (mIsAodEnabled == isAodEnabled) { + return; + } + mIsAodEnabled = isAodEnabled; + rescheduleTicker(); + } + }; + + mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + boolean updateFormatter = "android.intent.action.TIMEZONE_CHANGED".equals(intent.getAction()) || "android.intent.action.TIME_SET".equals(intent.getAction()); + onTimeChanged(updateFormatter); + } + }; + + mTimeChangedCallback = () -> onTimeChanged(false); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/InstanceId.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/InstanceId.java new file mode 100644 index 000000000000..8154c3e01f9b --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/InstanceId.java @@ -0,0 +1,21 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import java.util.UUID; + +public abstract class InstanceId { + public static int create(SmartspaceTarget smartspaceTarget) { + if (smartspaceTarget == null) { + return SmallHash.hash(UUID.randomUUID().toString()); + } + String smartspaceTargetId = smartspaceTarget.getSmartspaceTargetId(); + return (smartspaceTargetId == null || smartspaceTargetId.isEmpty()) ? SmallHash.hash(String.valueOf(smartspaceTarget.getCreationTimeMillis())) : SmallHash.hash(smartspaceTargetId); + } + + public static int create(String str) { + if (str != null && !str.isEmpty()) { + return SmallHash.hash(str); + } + return SmallHash.hash(UUID.randomUUID().toString()); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/InterceptingViewPager.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/InterceptingViewPager.java new file mode 100644 index 000000000000..18efe6161b2d --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/InterceptingViewPager.java @@ -0,0 +1,99 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.viewpager.widget.ViewPager; + +import com.android.systemui.res.R; + +import java.util.function.Predicate; + +public class InterceptingViewPager extends ViewPager { + public boolean mHasPerformedLongPress; + public boolean mHasPostedLongPress; + public final Runnable mLongPressCallback; + public final Predicate mSuperOnIntercept; + public final Predicate mSuperOnTouch; + + public InterceptingViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + + mSuperOnTouch = super::onTouchEvent; + mSuperOnIntercept = super::onInterceptTouchEvent; + mLongPressCallback = () -> { + mHasPerformedLongPress = true; + if (performLongClick()) { + getParent().requestDisallowInterceptTouchEvent(true); + } + }; + } + + public final void cancelScheduledLongPress() { + if (mHasPostedLongPress) { + mHasPostedLongPress = false; + removeCallbacks(mLongPressCallback); + } + } + + @Override + public final AccessibilityNodeInfo createAccessibilityNodeInfo() { + AccessibilityNodeInfo info = super.createAccessibilityNodeInfo(); + AccessibilityNodeInfoCompat.wrap(info).setRoleDescription(getContext().getString(R.string.smartspace_role_desc)); + return info; + } + + public final boolean handleTouchOverride(MotionEvent event, Predicate superMethod) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + mHasPerformedLongPress = false; + if (isLongClickable()) { + cancelScheduledLongPress(); + mHasPostedLongPress = true; + postDelayed(mLongPressCallback, ViewConfiguration.getLongPressTimeout()); + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + cancelScheduledLongPress(); + } + + if (mHasPerformedLongPress) { + cancelScheduledLongPress(); + return true; + } + + if (!superMethod.test(event)) { + return false; + } + + cancelScheduledLongPress(); + return true; + } + + @Override + public final boolean onInterceptTouchEvent(MotionEvent event) { + return handleTouchOverride(event, mSuperOnIntercept); + } + + @Override + public final boolean onTouchEvent(MotionEvent event) { + return handleTouchOverride(event, mSuperOnTouch); + } + + public InterceptingViewPager(Context context) { + super(context); + + mSuperOnTouch = super::onTouchEvent; + mSuperOnIntercept = super::onInterceptTouchEvent; + mLongPressCallback = () -> { + mHasPerformedLongPress = true; + if (performLongClick()) { + getParent().requestDisallowInterceptTouchEvent(true); + } + }; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt new file mode 100644 index 000000000000..8a22ecd2f8c9 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt @@ -0,0 +1,101 @@ +package com.google.android.systemui.smartspace + +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceTarget +import android.content.ComponentName +import android.content.Context +import android.media.MediaMetadata +import android.os.UserHandle +import android.text.TextUtils +import android.view.View +import com.android.systemui.media.NotificationMediaManager +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.res.R; +import javax.inject.Inject + +class KeyguardMediaViewController +@Inject +constructor( + val context: Context, + val mediaManager: NotificationMediaManager, + val plugin: BcSmartspaceDataPlugin, + val userTracker: UserTracker, + val uiExecutor: DelayableExecutor, +) { + var title: CharSequence? = null + var artist: CharSequence? = null + var smartspaceView: BcSmartspaceDataPlugin.SmartspaceView? = null + lateinit var mediaComponent: ComponentName + + val mediaListener = + object : NotificationMediaManager.MediaListener { + override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { + uiExecutor.execute { updateMediaInfo(metadata, state) } + } + } + + val attachStateChangeListener = + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + smartspaceView = view as? BcSmartspaceDataPlugin.SmartspaceView + mediaManager.addCallback(mediaListener) + } + + override fun onViewDetachedFromWindow(view: View) { + smartspaceView = null + mediaManager.removeCallback(mediaListener) + } + } + + private fun updateMediaInfo(metadata: MediaMetadata?, state: Int) { + if (!NotificationMediaManager.isPlayingState(state)) { + clearMedia() + return + } + + val newTitle = + metadata?.let { + it.getText(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + ?: it.getText(MediaMetadata.METADATA_KEY_TITLE) + ?: context.resources.getString(R.string.music_controls_no_title) + } + + val newArtist = metadata?.getText(MediaMetadata.METADATA_KEY_ARTIST) + + if (TextUtils.equals(title, newTitle) && TextUtils.equals(artist, newArtist)) { + return + } + + title = newTitle + artist = newArtist + + if (newTitle != null) { + val target = + SmartspaceTarget.Builder( + "deviceMedia", + mediaComponent, + UserHandle.of(userTracker.userId), + ) + .setFeatureType(SmartspaceTarget.FEATURE_MEDIA) + .setHeaderAction( + SmartspaceAction.Builder("deviceMediaTitle", newTitle.toString()) + .setSubtitle(artist) + .setIcon(mediaManager.getMediaIcon()) + .build() + ) + .build() + + smartspaceView?.setMediaTarget(target) ?: clearMedia() + } else { + clearMedia() + } + } + + private fun clearMedia() { + title = null + artist = null + smartspaceView?.setMediaTarget(null) + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardSmartspaceStartable.kt b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardSmartspaceStartable.kt new file mode 100644 index 000000000000..ef31c0121840 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardSmartspaceStartable.kt @@ -0,0 +1,33 @@ +package com.google.android.systemui.smartspace + +import com.android.systemui.CoreStartable +import com.android.systemui.util.InitializationChecker + +import javax.inject.Inject + +import kotlinx.coroutines.launch + +class KeyguardSmartspaceStartable +@Inject +constructor( + private val zenController: KeyguardZenAlarmViewController, + private val mediaController: KeyguardMediaViewController, + private val initializationChecker: InitializationChecker +) : CoreStartable { + + override fun start() { + if (initializationChecker.initializeComponents()) { + zenController.datePlugin.addOnAttachStateChangeListener( + zenController.attachStateChangeListener + ) + + zenController.applicationScope.launch { + zenController.updateNextAlarm() + } + + mediaController.plugin.addOnAttachStateChangeListener( + mediaController.attachStateChangeListener + ) + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardZenAlarmViewController.kt b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardZenAlarmViewController.kt new file mode 100644 index 000000000000..3c957d9a2066 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardZenAlarmViewController.kt @@ -0,0 +1,140 @@ +package com.google.android.systemui.smartspace + +import android.app.ActivityManager +import android.app.AlarmManager +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Handler +import android.text.format.DateFormat +import android.view.View +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.statusbar.policy.NextAlarmController +import com.android.systemui.statusbar.policy.NextAlarmControllerImpl +import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo +import com.android.systemui.res.R; +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class KeyguardZenAlarmViewController +@Inject +constructor( + val context: Context, + val datePlugin: BcSmartspaceDataPlugin, + val zenModeController: ZenModeController, + val zenModeInteractor: ZenModeInteractor, + val alarmManager: AlarmManager, + val nextAlarmController: NextAlarmControllerImpl, + val handler: Handler, + val applicationScope: CoroutineScope, + val bgDispatcher: CoroutineDispatcher, +) { + lateinit var alarmImage: Drawable + val smartspaceViews = mutableSetOf() + + private val nextAlarmCallback = + NextAlarmController.NextAlarmChangeCallback { + applicationScope.launch { updateNextAlarm() } + } + + private val showNextAlarm = AlarmManager.OnAlarmListener { showAlarm(null) } + + val attachStateChangeListener = + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + val smartspaceView = view as? BcSmartspaceDataPlugin.SmartspaceView ?: return + if (smartspaceViews.add(smartspaceView)) { + view.repeatWhenAttached { + zenModeInteractor.mainActiveMode.collect { modeInfo -> + updateModeIcon(smartspaceView, modeInfo) + } + } + } + if (smartspaceViews.size == 1) { + nextAlarmController.addCallback(nextAlarmCallback) + } + applicationScope.launch { updateNextAlarm() } + } + + override fun onViewDetachedFromWindow(view: View) { + val smartspaceView = view as? BcSmartspaceDataPlugin.SmartspaceView ?: return + smartspaceViews.remove(smartspaceView) + if (smartspaceViews.isEmpty()) { + nextAlarmController.removeCallback(nextAlarmCallback) + } + } + } + + suspend fun updateNextAlarm() { + applicationScope.launch { + alarmManager.cancel(showNextAlarm) + val nextAlarmTime = getNextAlarmTime() + if (nextAlarmTime > 0) { + val triggerTime = nextAlarmTime - TimeUnit.HOURS.toMillis(12L) + if (triggerTime > 0) { + alarmManager.setExact( + AlarmManager.RTC, + triggerTime, + "lock_screen_next_alarm", + showNextAlarm, + handler, + ) + } + } + showAlarm(nextAlarmTime) + } + } + + fun showAlarm(alarmTime: Long?): Job = + applicationScope.launch { + val time = alarmTime ?: getNextAlarmTime() + val alarmString = + if (time > 0 && time <= System.currentTimeMillis() + TimeUnit.HOURS.toMillis(12L)) { + val pattern = + if (DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser())) + "HH:mm" + else "h:mm" + DateFormat.format(pattern, time).toString() + } else null + + smartspaceViews.forEach { view -> + if (alarmString != null) { + view.setNextAlarm(alarmImage, alarmString) + } else { + view.setNextAlarm(null, null) + } + } + } + + // private suspend fun getNextAlarmTime(): Long = withContext(bgDispatcher) { + // val zenControllerImpl = zenModeController as? ZenModeControllerImpl ?: return@withContext + // 0L + // zenControllerImpl.mAlarmManager.getNextAlarmClock(zenControllerImpl.mUserId)?.triggerTime + // ?: 0L + // } + + private suspend fun getNextAlarmTime(): Long = + withContext(bgDispatcher) { + val nextAlarm = alarmManager.getNextAlarmClock(ActivityManager.getCurrentUser()) + nextAlarm?.triggerTime ?: 0L + } + + fun updateModeIcon(view: BcSmartspaceDataPlugin.SmartspaceView, zenModeInfo: ZenModeInfo?) { + applicationScope.launch { + if (zenModeInfo != null) { + val description = + context.getString(R.string.active_mode_content_description, zenModeInfo.name) + view.setDnd(zenModeInfo.icon.drawable, description) + } else { + view.setDnd(null, null) + } + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/LazyServerFlagLoader.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/LazyServerFlagLoader.java new file mode 100644 index 000000000000..e57867cd1482 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/LazyServerFlagLoader.java @@ -0,0 +1,44 @@ +package com.google.android.systemui.smartspace; + +import android.provider.DeviceConfig; + +import java.util.Set; +import java.util.concurrent.Executors; + +public final class LazyServerFlagLoader { + public final String mPropertyKey; + public Boolean mValue = null; + + public LazyServerFlagLoader(String str) { + mPropertyKey = str; + } + + public boolean get() { + if (mValue == null) { + mValue = Boolean.valueOf(DeviceConfig.getBoolean("launcher", mPropertyKey, true)); + DeviceConfig.addOnPropertiesChangedListener( + "launcher", + Executors.newSingleThreadExecutor(), + new OnPropertiesChangedListener(this)); + } + return mValue.booleanValue(); + } + + private static class OnPropertiesChangedListener + implements DeviceConfig.OnPropertiesChangedListener { + private final LazyServerFlagLoader lazyServerFlagLoader; + + OnPropertiesChangedListener(LazyServerFlagLoader loader) { + lazyServerFlagLoader = loader; + } + + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + Set keyset = properties.getKeyset(); + String str = lazyServerFlagLoader.mPropertyKey; + if (keyset.contains(str)) { + lazyServerFlagLoader.mValue = Boolean.valueOf(properties.getBoolean(str, true)); + } + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/PageIndicator.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/PageIndicator.java new file mode 100644 index 000000000000..d669910607d3 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/PageIndicator.java @@ -0,0 +1,19 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +public abstract class PageIndicator extends LinearLayout { + public PageIndicator(Context context) { + super(context); + } + + public PageIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PageIndicator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/PagerDots.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/PagerDots.java new file mode 100644 index 000000000000..dbfa89ce4589 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/PagerDots.java @@ -0,0 +1,122 @@ +package com.google.android.systemui.smartspace; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.systemui.res.R; + +public final class PagerDots extends View { + public final float activeDotSize; + public int currentPageIndex; + public int currentPositionIndex; + public float currentPositionOffset; + public final float dotMargin; + public final float dotRadius; + public final float dotSize; + public int numPages; + public final Paint paint; + public int primaryColor; + public final RectF tempRectF; + + public PagerDots(Context context, AttributeSet attrs) { + super(context, attrs); + numPages = -1; + currentPageIndex = -1; + currentPositionIndex = -1; + dotSize = getResources().getDimension(R.dimen.page_indicator_dot_size); + dotMargin = getResources().getDimension(R.dimen.page_indicator_dot_margin); + activeDotSize = dotSize * 2; + dotRadius = dotSize / 2; + tempRectF = new RectF(); + TypedArray obtainStyledAttributes = getContext().obtainStyledAttributes(new int[]{android.R.attr.textColorPrimary}); + primaryColor = obtainStyledAttributes.getColor(0, 0); + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(primaryColor); + obtainStyledAttributes.recycle(); + this.paint = paint; + } + + @Override + public final void onDraw(Canvas canvas) { + if (numPages < 2) { + return; + } + int save = canvas.save(); + try { + canvas.scale(isLayoutRtl() ? -1.0f : 1.0f, 1.0f, canvas.getWidth() * 0.5f, 0.0f); + canvas.translate(getPaddingStart(), getPaddingTop()); + float f4 = (activeDotSize - dotSize) * currentPositionOffset; + int i = (primaryColor >> 24) & 255; + int i2 = (int) (i * 0.4f); + int i3 = (int) ((i - i2) * currentPositionOffset); + tempRectF.top = 0.0f; + tempRectF.bottom = dotSize; + tempRectF.left = 0.0f; + for (int i5 = 0; i5 < numPages; i5++) { + int i6 = currentPositionIndex; + float f5 = i5 == i6 ? activeDotSize - f4 : i5 == i6 + 1 ? dotSize + f4 : dotSize; + int i7 = i5 == i6 ? i - i3 : i5 == i6 + 1 ? i2 + i3 : i2; + tempRectF.right = tempRectF.left + f5; + paint.setAlpha(i7); + canvas.drawRoundRect(tempRectF, dotRadius, dotRadius, paint); + tempRectF.left = tempRectF.right + dotMargin; + } + canvas.restoreToCount(save); + } catch (Throwable th) { + canvas.restoreToCount(save); + throw th; + } + } + + @Override + public final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(getPaddingRight() + getPaddingLeft() + (View.MeasureSpec.getMode(widthMeasureSpec) == View.MeasureSpec.EXACTLY ? View.MeasureSpec.getSize(widthMeasureSpec) : (int) (((dotMargin + dotSize) * (numPages - 1)) + activeDotSize)), getPaddingBottom() + getPaddingTop() + (View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.EXACTLY ? View.MeasureSpec.getSize(heightMeasureSpec) : (int) dotSize)); + } + + public final void setNumPages(int i, boolean z) { + if (i == numPages) { + return; + } + if (i <= 0) { + Log.w("SsPagerDots", "Total number of pages invalid: " + i + ". Assuming 1 page."); + numPages = 1; + } else { + numPages = i; + } + if (currentPageIndex < 0) { + updateCurrentPageIndex(z ? numPages - 1 : 0); + currentPositionIndex = currentPageIndex; + } else { + if (currentPageIndex >= numPages) { + updateCurrentPageIndex(z ? 0 : numPages - 1); + currentPositionIndex = currentPageIndex; + } + } + if (numPages < 2) { + BcSmartspaceTemplateDataUtils.updateVisibility(this, View.GONE); + } else if (getVisibility() != View.INVISIBLE) { + BcSmartspaceTemplateDataUtils.updateVisibility(this, View.VISIBLE); + } + requestLayout(); + invalidate(); + } + + public final void updateCurrentPageIndex(int i) { + if (i == currentPageIndex) { + return; + } + currentPageIndex = i; + setContentDescription(getContext().getString(R.string.accessibility_smartspace_page, currentPageIndex + 1, numPages)); + } + + public PagerDots(Context context) { + this(context, null); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/SmallHash.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/SmallHash.java new file mode 100644 index 000000000000..6975c387c6f9 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/SmallHash.java @@ -0,0 +1,9 @@ +package com.google.android.systemui.smartspace; + +import java.util.Objects; + +public abstract class SmallHash { + public static int hash(String str) { + return Math.abs(Math.floorMod(Objects.hashCode(str), 8192)); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/SmartspaceCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/SmartspaceCard.java new file mode 100644 index 000000000000..1e09ce1e73a8 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/SmartspaceCard.java @@ -0,0 +1,22 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import android.view.View; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +public interface SmartspaceCard { + void bindData(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo, boolean usePageIndicatorUi); + + BcSmartspaceCardLoggingInfo getLoggingInfo(); + + View getView(); + + void setDozeAmount(float dozeAmount); + + void setPrimaryTextColor(int color); + + void setScreenOn(boolean screenOn); +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/TouchDelegateComposite.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/TouchDelegateComposite.java new file mode 100644 index 000000000000..90c5b51037db --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/TouchDelegateComposite.java @@ -0,0 +1,34 @@ +package com.google.android.systemui.smartspace; + +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.TouchDelegate; + +import com.google.android.systemui.smartspace.uitemplate.BaseTemplateCard; + +import java.util.ArrayList; +import java.util.Iterator; + +public final class TouchDelegateComposite extends TouchDelegate { + public ArrayList mDelegates; + + public TouchDelegateComposite(Rect rect, BaseTemplateCard baseTemplateCard) { + super(rect, baseTemplateCard); + mDelegates = new ArrayList(); + } + + @Override + public final boolean onTouchEvent(MotionEvent motionEvent) { + float x = motionEvent.getX(); + float y = motionEvent.getY(); + Iterator it = mDelegates.iterator(); + while (it.hasNext()) { + TouchDelegate touchDelegate = (TouchDelegate) it.next(); + motionEvent.setLocation(x, y); + if (touchDelegate.onTouchEvent(motionEvent)) { + return true; + } + } + return false; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceDataProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceDataProvider.java new file mode 100644 index 000000000000..e2cd81cb3af5 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceDataProvider.java @@ -0,0 +1,86 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceTarget; +import android.content.Context; +import android.os.Debug; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.android.systemui.res.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class WeatherSmartspaceDataProvider implements BcSmartspaceDataPlugin { + public static final boolean DEBUG = Log.isLoggable("WeatherSSDataProvider", Log.DEBUG); + public final Set mSmartspaceTargetListeners = new HashSet<>(); + public final List mSmartspaceTargets = new ArrayList<>(); + public final EventNotifierProxy mEventNotifier = new EventNotifierProxy(); + + @Override + public final BcSmartspaceDataPlugin.SmartspaceEventNotifier getEventNotifier() { + return mEventNotifier; + } + + @Override + public final BcSmartspaceDataPlugin.SmartspaceView getLargeClockView(Context context) { + View view = LayoutInflater.from(context).inflate(R.layout.weather_large, (ViewGroup) null, false); + view.setId(R.id.weather_smartspace_view_large); + return (BcSmartspaceDataPlugin.SmartspaceView) view; + } + + @Override + public final BcSmartspaceDataPlugin.SmartspaceView getView(Context context) { + return (BcSmartspaceDataPlugin.SmartspaceView) LayoutInflater.from(context).inflate(R.layout.weather, (ViewGroup) null, false); + } + + @Override + public void onTargetsAvailable(List targets) { + if (DEBUG) { + Log.d( + "WeatherSSDataProvider", + this + + " onTargetsAvailable called. Callers = " + + android.os.Debug.getCallers(3)); + Log.d("WeatherSSDataProvider", " targets.size() = " + targets.size()); + Log.d("WeatherSSDataProvider", " targets = " + targets.toString()); + } + + mSmartspaceTargets.clear(); + for (SmartspaceTarget target : targets) { + if (target.getFeatureType() == 1) { + mSmartspaceTargets.add(target); + } + } + + mSmartspaceTargetListeners.forEach( + listener -> listener.onSmartspaceTargetsUpdated(mSmartspaceTargets)); + } + + @Override + public final void registerListener(BcSmartspaceDataPlugin.SmartspaceTargetListener listener) { + mSmartspaceTargetListeners.add(listener); + listener.onSmartspaceTargetsUpdated(mSmartspaceTargets); + } + + @Override + public final void setEventDispatcher(BcSmartspaceDataPlugin.SmartspaceEventDispatcher eventDispatcher) { + mEventNotifier.eventDispatcher = eventDispatcher; + } + + @Override + public final void setIntentStarter(BcSmartspaceDataPlugin.IntentStarter intentStarter) { + mEventNotifier.intentStarterRef = intentStarter; + } + + @Override + public final void unregisterListener(BcSmartspaceDataPlugin.SmartspaceTargetListener listener) { + mSmartspaceTargetListeners.remove(listener); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceView.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceView.java new file mode 100644 index 000000000000..a29b010befc2 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/WeatherSmartspaceView.java @@ -0,0 +1,273 @@ +package com.google.android.systemui.smartspace; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.TapAction; +import android.app.smartspace.uitemplatedata.Text; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Parcelable; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.internal.graphics.ColorUtils; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; + +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLogger; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.utils.ContentDescriptionUtil; + +import com.android.systemui.res.R; + +import java.util.List; + +public class WeatherSmartspaceView extends LinearLayout implements BcSmartspaceDataPlugin.SmartspaceTargetListener, BcSmartspaceDataPlugin.SmartspaceView { + public static final boolean DEBUG = Log.isLoggable("WeatherSmartspaceView", Log.DEBUG); + public final ContentObserver mAodSettingsObserver; + public Handler mBgHandler; + public BcSmartspaceDataPlugin mDataProvider; + public float mDozeAmount; + public final DoubleShadowIconDrawable mIconDrawable; + public final int mIconSize; + public boolean mIsAodEnabled; + public BcSmartspaceCardLoggingInfo mLoggingInfo; + public int mPrimaryTextColor; + public final boolean mRemoveTextDescent; + public final int mTextDescentExtraPadding; + public String mUiSurface; + public DoubleShadowTextView mView; + + public WeatherSmartspaceView(Context context) { + this(context, null); + } + + @Override + public final void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (TextUtils.equals(mUiSurface, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)) { + try { + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + + mBgHandler.post(() -> { + getContext().getContentResolver().registerContentObserver( + Settings.Secure.getUriFor("doze_always_on"), + false, + mAodSettingsObserver, + -1 + ); + }); + } catch (Exception e) { + Log.w("WeatherSmartspaceView", "Unable to register DOZE_ALWAYS_ON content observer: ", e); + } + + mIsAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + } + + if (mDataProvider != null) { + mDataProvider.registerListener(this); + } + } + + @Override + public final void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mBgHandler == null) { + throw new IllegalStateException("Must set background handler to avoid making binder calls on main thread"); + } + + mBgHandler.post(() -> { + getContext().getContentResolver().unregisterContentObserver(mAodSettingsObserver); + }); + + if (mDataProvider != null) { + mDataProvider.unregisterListener(this); + } + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mView = findViewById(R.id.weather_text_view); + } + + @Override + public final void onSmartspaceTargetsUpdated(List targets) { + List smartspaceTargets = + targets.stream() + .filter(t -> t instanceof SmartspaceTarget) + .map(t -> (SmartspaceTarget) t) + .collect(java.util.stream.Collectors.toList()); + if (smartspaceTargets.size() > 1) { + return; + } + if (smartspaceTargets.isEmpty() && TextUtils.equals(mUiSurface, BcSmartspaceDataPlugin.UI_SURFACE_DREAM)) { + return; + } + if (smartspaceTargets.isEmpty()) { + BcSmartspaceTemplateDataUtils.updateVisibility(mView, View.GONE); + return; + } + BcSmartspaceTemplateDataUtils.updateVisibility(mView, View.VISIBLE); + SmartspaceTarget target = smartspaceTargets.get(0); + if (target.getFeatureType() != 1) { + return; + } + boolean hasValidTemplate = BcSmartspaceCardLoggerUtil.containsValidTemplateType(target.getTemplateData()); + if (hasValidTemplate || target.getHeaderAction() != null) { + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(InstanceId.create(target)) + .setFeatureType(target.getFeatureType()) + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount)) + .setUid(-1) + .setDimensionalInfo( + BcSmartspaceCardLoggerUtil.createDimensionalLoggingInfo( + target.getTemplateData())); + mLoggingInfo = new BcSmartspaceCardLoggingInfo(builder); + if (!hasValidTemplate) { + SmartspaceAction headerAction = target.getHeaderAction(); + if (headerAction == null) { + Log.d("WeatherSmartspaceView", "Passed-in header action is null"); + } else { + mView.setText(headerAction.getTitle().toString()); + mView.setCompoundDrawablesRelative(null, null, null, null); + mIconDrawable.setIcon(BcSmartSpaceUtil.getIconDrawableWithCustomSize(headerAction.getIcon(), getContext(), mIconSize)); + mView.setCompoundDrawablesRelative(mIconDrawable, null, null, null); + ContentDescriptionUtil.setFormattedContentDescription("WeatherSmartspaceView", mView, headerAction.getTitle(), headerAction.getContentDescription()); + if (!TextUtils.equals(mUiSurface, BcSmartspaceDataPlugin.UI_SURFACE_DREAM)) { + BcSmartSpaceUtil.setOnClickListener(mView, target, headerAction, mDataProvider != null ? mDataProvider.getEventNotifier() : null, "WeatherSmartspaceView", mLoggingInfo, 0); + } + } + } else if (target.getTemplateData() != null) { + BaseTemplateData.SubItemInfo subItemInfo = target.getTemplateData().getSubtitleItem(); + if (subItemInfo == null) { + Log.d("WeatherSmartspaceView", "Passed-in item info is null"); + } else { + BcSmartspaceTemplateDataUtils.setText(mView, subItemInfo.getText()); + mView.setCompoundDrawablesRelative(null, null, null, null); + if (subItemInfo.getIcon() != null) { + mIconDrawable.setIcon(BcSmartSpaceUtil.getIconDrawableWithCustomSize(subItemInfo.getIcon().getIcon(), getContext(), mIconSize)); + mView.setCompoundDrawablesRelative(mIconDrawable, null, null, null); + } + ContentDescriptionUtil.setFormattedContentDescription("WeatherSmartspaceView", mView, SmartspaceUtils.isEmpty(subItemInfo.getText()) ? "" : subItemInfo.getText().getText(), subItemInfo.getIcon() != null ? subItemInfo.getIcon().getContentDescription() : ""); + if (subItemInfo.getTapAction() != null && !TextUtils.equals(mUiSurface, BcSmartspaceDataPlugin.UI_SURFACE_DREAM)) { + BcSmartSpaceUtil.setOnClickListener(mView, target, subItemInfo.getTapAction(), mDataProvider != null ? mDataProvider.getEventNotifier() : null, "WeatherSmartspaceView", mLoggingInfo, 0); + } + } + } + if (mRemoveTextDescent) { + mView.setPaddingRelative(0, 0, 0, mTextDescentExtraPadding - ((int) Math.floor(mView.getPaint().getFontMetrics().descent))); + } + } + } + + @Override + public final void registerDataProvider(BcSmartspaceDataPlugin dataProvider) { + if (mDataProvider != null) { + mDataProvider.unregisterListener(this); + } + mDataProvider = dataProvider; + if (isAttachedToWindow()) { + mDataProvider.registerListener(this); + } + } + + @Override + public final void setBgHandler(Handler handler) { + mBgHandler = handler; + } + + @Override + public final void setDozeAmount(float dozeAmount) { + mDozeAmount = dozeAmount; + mView.setTextColor(ColorUtils.blendARGB(mPrimaryTextColor, -1, dozeAmount)); + int loggingSurface = BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount); + if (mLoggingInfo == null || loggingSurface == -1) { + return; + } + if (loggingSurface != 3 || mIsAodEnabled) { + if (DEBUG) { + Log.d("WeatherSmartspaceView", "@" + Integer.toHexString(hashCode()) + ", setDozeAmount: Logging SMARTSPACE_CARD_SEEN, loggingSurface = " + loggingSurface); + } + + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setInstanceId(mLoggingInfo.mInstanceId) + .setFeatureType(mLoggingInfo.mFeatureType) + .setDisplaySurface(loggingSurface) + .setUid(mLoggingInfo.mUid); + BcSmartspaceCardLogger.log( + BcSmartspaceEvent.SMARTSPACE_CARD_SEEN, new BcSmartspaceCardLoggingInfo(builder)); + } + } + + @Override + public final void setFalsingManager(FalsingManager falsingManager) { + BcSmartSpaceUtil.sFalsingManager = falsingManager; + } + + @Override + public final void setPrimaryTextColor(int color) { + mPrimaryTextColor = color; + mView.setTextColor(ColorUtils.blendARGB(color, -1, mDozeAmount)); + } + + @Override + public final void setUiSurface(String uiSurface) { + if (isAttachedToWindow()) { + throw new IllegalStateException("Must call before attaching view to window."); + } + mUiSurface = uiSurface; + } + + public WeatherSmartspaceView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WeatherSmartspaceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mUiSurface = null; + mDozeAmount = 0.0f; + mLoggingInfo = null; + mAodSettingsObserver = new ContentObserver(new Handler()) { + @Override + public final void onChange(boolean selfChange) { + boolean isAodEnabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), "doze_always_on", 0, getContext().getUserId()) == 1; + if (mIsAodEnabled == isAodEnabled) { + return; + } + mIsAodEnabled = isAodEnabled; + } + }; + context.getTheme().applyStyle(R.style.Smartspace, false); + TypedArray obtainStyledAttributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WeatherSmartspaceView, 0, 0); + try { + int iconSize = obtainStyledAttributes.getDimensionPixelSize(1, context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size)); + int iconInset = obtainStyledAttributes.getDimensionPixelSize(0, context.getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_inset)); + mRemoveTextDescent = obtainStyledAttributes.getBoolean(2, false); + mTextDescentExtraPadding = obtainStyledAttributes.getDimensionPixelSize(3, 0); + obtainStyledAttributes.recycle(); + mIconSize = iconSize; + mIconDrawable = new DoubleShadowIconDrawable(iconSize, iconInset, context); + } catch (Throwable th) { + obtainStyledAttributes.recycle(); + throw th; + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/dagger/SmartspaceStartableModule.kt b/packages/SystemUI/src/com/google/android/systemui/smartspace/dagger/SmartspaceStartableModule.kt new file mode 100644 index 000000000000..d705344b0c61 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/dagger/SmartspaceStartableModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 auroraOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.systemui.smartspace.dagger + +import com.android.systemui.CoreStartable +import com.google.android.systemui.smartspace.KeyguardSmartspaceStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface SmartspaceStartableModule { + @Binds + @IntoMap + @ClassKey(KeyguardSmartspaceStartable::class) + fun bindKeyguardSmartspaceStartable(impl: KeyguardSmartspaceStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLogger.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLogger.java new file mode 100644 index 000000000000..e97ab7600d53 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLogger.java @@ -0,0 +1,80 @@ +package com.google.android.systemui.smartspace.logging; + +import android.util.StatsEvent; +import android.util.StatsLog; + +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.smartspace.SmartspaceProtoLite; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceEvent; +import com.google.protobuf.nano.MessageNano; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BcSmartspaceCardLogger { + static { + FalsingManager falsingManager = BcSmartSpaceUtil.sFalsingManager; + } + + public static void log(BcSmartspaceEvent event, BcSmartspaceCardLoggingInfo loggingInfo) { + byte[] subcardsData = null; + byte[] dimensionalInfoData = null; + + BcSmartspaceSubcardLoggingInfo subcardInfo = loggingInfo.mSubcardInfo; + if (subcardInfo != null + && subcardInfo.mSubcards != null + && !subcardInfo.mSubcards.isEmpty()) { + List subcardMetadataList = + new ArrayList<>(); + for (BcSmartspaceCardMetadataLoggingInfo metadata : subcardInfo.mSubcards) { + SmartspaceProtoLite.SmartSpaceCardMetadata.Builder builder = + SmartspaceProtoLite.SmartSpaceCardMetadata.newBuilder(); + builder.setInstanceId(metadata.mInstanceId); + builder.setCardTypeId(metadata.mCardTypeId); + subcardMetadataList.add(builder.build()); + } + + SmartspaceProtoLite.SmartSpaceSubcards.Builder subcardsBuilder = + SmartspaceProtoLite.SmartSpaceSubcards.newBuilder(); + subcardsBuilder.setClickedSubcardIndex(subcardInfo.mClickedSubcardIndex); + subcardsBuilder.addAllSubcards(subcardMetadataList); + SmartspaceProtoLite.SmartSpaceSubcards subcards = subcardsBuilder.build(); + subcardsData = subcards.toByteArray(); + } + + if (loggingInfo.mDimensionalInfo != null) { + dimensionalInfoData = MessageNano.toByteArray(loggingInfo.mDimensionalInfo); + } + + StatsEvent.Builder statsBuilder = StatsEvent.newBuilder(); + statsBuilder.setAtomId(0x160); + statsBuilder.writeInt(event.getId()); + statsBuilder.writeInt(loggingInfo.mInstanceId); + statsBuilder.writeInt(0); + statsBuilder.writeInt(loggingInfo.mDisplaySurface); + statsBuilder.writeInt(loggingInfo.mRank); + statsBuilder.writeInt(loggingInfo.mCardinality); + statsBuilder.writeInt(loggingInfo.mFeatureType); + statsBuilder.writeInt(loggingInfo.mUid); + statsBuilder.addBooleanAnnotation((byte) 1, true); + statsBuilder.writeInt(0); + statsBuilder.writeInt(0); + statsBuilder.writeInt(loggingInfo.mReceivedLatency); + + if (subcardsData == null) { + subcardsData = new byte[0]; + } + statsBuilder.writeByteArray(subcardsData); + + if (dimensionalInfoData == null) { + dimensionalInfoData = new byte[0]; + } + statsBuilder.writeByteArray(dimensionalInfoData); + + statsBuilder.usePooledBuffer(); + StatsEvent statsEvent = statsBuilder.build(); + StatsLog.write(statsEvent); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggerUtil.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggerUtil.java new file mode 100644 index 000000000000..35160fcaf5c8 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggerUtil.java @@ -0,0 +1,130 @@ +package com.google.android.systemui.smartspace.logging; + +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.os.Bundle; + +import com.android.systemui.smartspace.nano.SmartspaceProto; + +import com.google.android.systemui.smartspace.InstanceId; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BcSmartspaceCardLoggerUtil { + public static boolean containsValidTemplateType(BaseTemplateData data) { + return (data == null || data.getTemplateType() == 0 || data.getTemplateType() == 8) ? false : true; + } + + public static SmartspaceProto.SmartspaceCardDimensionalInfo createDimensionalLoggingInfo(BaseTemplateData data) { + if (data == null || data.getPrimaryItem() == null || data.getPrimaryItem().getTapAction() == null) { + return null; + } + + Bundle extras = data.getPrimaryItem().getTapAction().getExtras(); + List dimensions = new ArrayList<>(); + + if (extras != null && !extras.isEmpty()) { + ArrayList ids = extras.getIntegerArrayList("ss_card_dimension_ids"); + ArrayList values = extras.getIntegerArrayList("ss_card_dimension_values"); + if (ids != null && values != null && ids.size() == values.size()) { + for (int i = 0; i < ids.size(); i++) { + SmartspaceProto.SmartspaceFeatureDimension dimension = new SmartspaceProto.SmartspaceFeatureDimension(); + dimension.featureDimensionId = ids.get(i); + dimension.featureDimensionValue = values.get(i); + dimensions.add(dimension); + } + } + } + + if (dimensions.isEmpty()) { + return null; + } + + SmartspaceProto.SmartspaceCardDimensionalInfo info = new SmartspaceProto.SmartspaceCardDimensionalInfo(); + info.featureDimensions = dimensions.toArray(new SmartspaceProto.SmartspaceFeatureDimension[dimensions.size()]); + return info; + } + + public static BcSmartspaceSubcardLoggingInfo createSubcardLoggingInfo(SmartspaceTarget target) { + if (target.getBaseAction() == null || target.getBaseAction().getExtras() == null || target.getBaseAction().getExtras().isEmpty() || target.getBaseAction().getExtras().getInt("subcardType", -1) == -1) { + return null; + } + + int instanceId = InstanceId.create(target.getBaseAction().getExtras().getString("subcardId")); + int cardTypeId = target.getBaseAction().getExtras().getInt("subcardType"); + + BcSmartspaceCardMetadataLoggingInfo.Builder builder = + new BcSmartspaceCardMetadataLoggingInfo.Builder(); + builder.mInstanceId = instanceId; + builder.mCardTypeId = cardTypeId; + + BcSmartspaceCardMetadataLoggingInfo metadata = + new BcSmartspaceCardMetadataLoggingInfo(builder); + List subcards = new ArrayList<>(); + subcards.add(metadata); + + BcSmartspaceSubcardLoggingInfo subcardInfo = new BcSmartspaceSubcardLoggingInfo(); + subcardInfo.mSubcards = subcards; + subcardInfo.mClickedSubcardIndex = 0; + return subcardInfo; + } + + public static void createSubcardLoggingInfoHelper( + List subcards, + BaseTemplateData.SubItemInfo subItemInfo) { + if (subItemInfo != null && subItemInfo.getLoggingInfo() != null) { + BaseTemplateData.SubItemLoggingInfo loggingInfo = subItemInfo.getLoggingInfo(); + BcSmartspaceCardMetadataLoggingInfo.Builder builder = + new BcSmartspaceCardMetadataLoggingInfo.Builder(); + builder.mCardTypeId = loggingInfo.getFeatureType(); + builder.mInstanceId = loggingInfo.getInstanceId(); + subcards.add(new BcSmartspaceCardMetadataLoggingInfo(builder)); + } + } + + public static void tryForcePrimaryFeatureTypeOrUpdateLogInfoFromTemplateData(BcSmartspaceCardLoggingInfo loggingInfo, BaseTemplateData data) { + if (loggingInfo.mFeatureType == 1) { + loggingInfo.mFeatureType = 39; + loggingInfo.mInstanceId = InstanceId.create("date_card_794317_92634"); + return; + } + if (data == null || data.getPrimaryItem() == null || data.getPrimaryItem().getLoggingInfo() == null) { + return; + } + int featureType = data.getPrimaryItem().getLoggingInfo().getFeatureType(); + if (featureType > 0) { + loggingInfo.mFeatureType = featureType; + } + int instanceId = data.getPrimaryItem().getLoggingInfo().getInstanceId(); + if (instanceId > 0) { + loggingInfo.mInstanceId = instanceId; + } + } + + public static BcSmartspaceSubcardLoggingInfo createSubcardLoggingInfo(BaseTemplateData data) { + if (data == null) { + return null; + } + + List subcards = new ArrayList<>(); + + if (data.getPrimaryItem() != null && data.getPrimaryItem().getLoggingInfo() != null && data.getPrimaryItem().getLoggingInfo().getFeatureType() == 1) { + createSubcardLoggingInfoHelper(subcards, data.getPrimaryItem()); + } + + createSubcardLoggingInfoHelper(subcards, data.getSubtitleItem()); + createSubcardLoggingInfoHelper(subcards, data.getSubtitleSupplementalItem()); + createSubcardLoggingInfoHelper(subcards, data.getSupplementalLineItem()); + + if (subcards.isEmpty()) { + return null; + } + + BcSmartspaceSubcardLoggingInfo subcardInfo = new BcSmartspaceSubcardLoggingInfo(); + subcardInfo.mSubcards = subcards; + subcardInfo.mClickedSubcardIndex = 0; + return subcardInfo; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggingInfo.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggingInfo.java new file mode 100644 index 000000000000..0ee3d6cb87a6 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardLoggingInfo.java @@ -0,0 +1,112 @@ +package com.google.android.systemui.smartspace.logging; + +import com.android.systemui.smartspace.nano.SmartspaceProto; + +import java.util.Objects; + +public final class BcSmartspaceCardLoggingInfo { + public int mInstanceId; + public int mDisplaySurface; + public int mRank; + public int mCardinality; + public int mFeatureType; + public int mReceivedLatency; + public int mUid; + public BcSmartspaceSubcardLoggingInfo mSubcardInfo; + public SmartspaceProto.SmartspaceCardDimensionalInfo mDimensionalInfo; + + public static final class Builder { + public int mInstanceId; + public int mDisplaySurface = 1; + public int mRank; + public int mCardinality; + public int mFeatureType; + public int mReceivedLatency; + public int mUid; + public BcSmartspaceSubcardLoggingInfo mSubcardInfo; + public SmartspaceProto.SmartspaceCardDimensionalInfo mDimensionalInfo; + + public Builder() {} + + public Builder setInstanceId(int instanceId) { + this.mInstanceId = instanceId; + return this; + } + + public Builder setDisplaySurface(int displaySurface) { + this.mDisplaySurface = displaySurface; + return this; + } + + public Builder setRank(int rank) { + this.mRank = rank; + return this; + } + + public Builder setCardinality(int cardinality) { + this.mCardinality = cardinality; + return this; + } + + public Builder setFeatureType(int featureType) { + this.mFeatureType = featureType; + return this; + } + + public Builder setReceivedLatency(int receivedLatency) { + this.mReceivedLatency = receivedLatency; + return this; + } + + public Builder setUid(int uid) { + this.mUid = uid; + return this; + } + + public Builder setSubcardInfo(BcSmartspaceSubcardLoggingInfo subcardInfo) { + this.mSubcardInfo = subcardInfo; + return this; + } + + public Builder setDimensionalInfo( + SmartspaceProto.SmartspaceCardDimensionalInfo dimensionalInfo) { + this.mDimensionalInfo = dimensionalInfo; + return this; + } + + public BcSmartspaceCardLoggingInfo build() { + return new BcSmartspaceCardLoggingInfo(this); + } + } + + public BcSmartspaceCardLoggingInfo(Builder builder) { + this.mInstanceId = builder.mInstanceId; + this.mDisplaySurface = builder.mDisplaySurface; + this.mRank = builder.mRank; + this.mCardinality = builder.mCardinality; + this.mFeatureType = builder.mFeatureType; + this.mReceivedLatency = builder.mReceivedLatency; + this.mUid = builder.mUid; + this.mSubcardInfo = builder.mSubcardInfo; + this.mDimensionalInfo = builder.mDimensionalInfo; + } + + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BcSmartspaceCardLoggingInfo)) { + return false; + } + BcSmartspaceCardLoggingInfo other = (BcSmartspaceCardLoggingInfo) obj; + return mInstanceId == other.mInstanceId && mDisplaySurface == other.mDisplaySurface && mRank == other.mRank && mCardinality == other.mCardinality && mFeatureType == other.mFeatureType && mReceivedLatency == other.mReceivedLatency && mUid == other.mUid && Objects.equals(mSubcardInfo, other.mSubcardInfo) && Objects.equals(mDimensionalInfo, other.mDimensionalInfo); + } + + public final int hashCode() { + return Objects.hash(mInstanceId, mDisplaySurface, mRank, mCardinality, mFeatureType, mReceivedLatency, mUid, mSubcardInfo); + } + + public final String toString() { + return "instance_id = " + mInstanceId + ", feature type = " + mFeatureType + ", display surface = " + mDisplaySurface + ", rank = " + mRank + ", cardinality = " + mCardinality + ", receivedLatencyMillis = " + mReceivedLatency + ", uid = " + mUid + ", subcardInfo = " + mSubcardInfo + ", dimensionalInfo = " + mDimensionalInfo; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardMetadataLoggingInfo.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardMetadataLoggingInfo.java new file mode 100644 index 000000000000..6a16e1800542 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceCardMetadataLoggingInfo.java @@ -0,0 +1,55 @@ +package com.google.android.systemui.smartspace.logging; + +import java.util.Objects; + +public final class BcSmartspaceCardMetadataLoggingInfo { + public int mCardTypeId; + public int mInstanceId; + + public BcSmartspaceCardMetadataLoggingInfo(Builder builder) { + this.mInstanceId = builder.mInstanceId; + this.mCardTypeId = builder.mCardTypeId; + } + + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BcSmartspaceCardMetadataLoggingInfo)) { + return false; + } + BcSmartspaceCardMetadataLoggingInfo bcSmartspaceCardMetadataLoggingInfo = (BcSmartspaceCardMetadataLoggingInfo) obj; + return mInstanceId == bcSmartspaceCardMetadataLoggingInfo.mInstanceId && mCardTypeId == bcSmartspaceCardMetadataLoggingInfo.mCardTypeId; + } + + public final int hashCode() { + return Objects.hash(mInstanceId, mCardTypeId); + } + + public String toString() { + return "BcSmartspaceCardMetadataLoggingInfo{mInstanceId=" + + mInstanceId + + ", mCardTypeId=" + + mCardTypeId + + "}"; + } + + public static final class Builder { + public int mCardTypeId; + public int mInstanceId; + + public Builder setCardTypeId(int cardTypeId) { + this.mCardTypeId = cardTypeId; + return this; + } + + public Builder setInstanceId(int instanceId) { + this.mInstanceId = instanceId; + return this; + } + + public BcSmartspaceCardMetadataLoggingInfo build() { + return new BcSmartspaceCardMetadataLoggingInfo(this); + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceSubcardLoggingInfo.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceSubcardLoggingInfo.java new file mode 100644 index 000000000000..01a6df58e925 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/logging/BcSmartspaceSubcardLoggingInfo.java @@ -0,0 +1,32 @@ +package com.google.android.systemui.smartspace.logging; + +import java.util.List; +import java.util.Objects; + +public final class BcSmartspaceSubcardLoggingInfo { + public int mClickedSubcardIndex; + public List mSubcards; + + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BcSmartspaceSubcardLoggingInfo)) { + return false; + } + BcSmartspaceSubcardLoggingInfo other = (BcSmartspaceSubcardLoggingInfo) obj; + return mClickedSubcardIndex == other.mClickedSubcardIndex && Objects.equals(mSubcards, other.mSubcards); + } + + public final int hashCode() { + return Objects.hash(mSubcards, mClickedSubcardIndex); + } + + public final String toString() { + return "BcSmartspaceSubcardLoggingInfo{mSubcards=" + + mSubcards + + ", mClickedSubcardIndex=" + + mClickedSubcardIndex + + "}"; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/BaseTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/BaseTemplateCard.java new file mode 100644 index 000000000000..f6be76215363 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/BaseTemplateCard.java @@ -0,0 +1,473 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.TapAction; +import android.app.smartspace.uitemplatedata.Text; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.PathInterpolator; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.app.animation.Interpolators; +import com.android.launcher3.icons.GraphicsUtils; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.smartspace.nano.SmartspaceProto; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.DoubleShadowIconDrawable; +import com.google.android.systemui.smartspace.DoubleShadowTextView; +import com.google.android.systemui.smartspace.IcuDateTextView; +import com.google.android.systemui.smartspace.SmartspaceCard; +import com.google.android.systemui.smartspace.TouchDelegateComposite; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardMetadataLoggingInfo; +import com.google.android.systemui.smartspace.logging.BcSmartspaceSubcardLoggingInfo; +import com.google.android.systemui.smartspace.utils.ContentDescriptionUtil; + +import com.android.systemui.res.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class BaseTemplateCard extends ConstraintLayout implements SmartspaceCard { + public Handler mBgHandler; + public IcuDateTextView mDateView; + public float mDozeAmount; + public ViewGroup mExtrasGroup; + public int mFeatureType; + public int mIconTintColor; + public BcSmartspaceCardLoggingInfo mLoggingInfo; + public BcSmartspaceCardSecondary mSecondaryCard; + public ViewGroup mSecondaryCardPane; + public boolean mShouldShowPageIndicator; + public ViewGroup mSubtitleGroup; + public Rect mSubtitleHitRect; + public Rect mSubtitleSupplementalHitRect; + public DoubleShadowTextView mSubtitleSupplementalView; + public DoubleShadowTextView mSubtitleTextView; + public DoubleShadowTextView mSupplementalLineTextView; + public SmartspaceTarget mTarget; + public BaseTemplateData mTemplateData; + public ViewGroup mTextGroup; + public DoubleShadowTextView mTitleTextView; + public final TouchDelegateComposite mTouchDelegateComposite; + public boolean mTouchDelegateIsDirty; + public String mUiSurface; + public boolean mValidSecondaryCard; + + public BaseTemplateCard(Context context) { + this(context, null); + } + + public static boolean shouldTint(BaseTemplateData.SubItemInfo subItemInfo) { + if (subItemInfo == null || subItemInfo.getIcon() == null) { + return false; + } + return subItemInfo.getIcon().shouldTint(); + } + + @Override + public final void bindData(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo, boolean shouldShowPageIndicator) { + mTarget = null; + mTemplateData = null; + mFeatureType = 0; + mLoggingInfo = null; + setOnClickListener(null); + setClickable(false); + if (mDateView != null) { + mDateView.setOnClickListener(null); + mDateView.setClickable(false); + } + resetTextView(mTitleTextView); + resetTextView(mSubtitleTextView); + resetTextView(mSubtitleSupplementalView); + resetTextView(mSupplementalLineTextView); + BcSmartspaceTemplateDataUtils.updateVisibility(mTitleTextView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSubtitleGroup, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSubtitleTextView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSubtitleSupplementalView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondaryCardPane, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mExtrasGroup, View.GONE); + mTarget = target; + mTemplateData = target.getTemplateData(); + mFeatureType = target.getFeatureType(); + mLoggingInfo = loggingInfo; + mShouldShowPageIndicator = shouldShowPageIndicator; + mValidSecondaryCard = false; + if (mTextGroup != null) { + mTextGroup.setTranslationX(0.0f); + } + if (mTemplateData == null) { + return; + } + mLoggingInfo = getLoggingInfo(); + if (mSecondaryCard != null) { + Log.i("SsBaseTemplateCard", "Secondary card is not null"); + mSecondaryCard.reset(target.getSmartspaceTargetId()); + mValidSecondaryCard = mSecondaryCard.setSmartspaceActions(target, eventNotifier, mLoggingInfo); + } + if (mSecondaryCardPane != null) { + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondaryCardPane, (mDozeAmount == 1.0f || !mValidSecondaryCard) ? View.GONE : View.VISIBLE); + } + if (mDateView == null) { + Log.d("SsBaseTemplateCard", "No date view can be set up"); + } else { + if (TextUtils.isEmpty(mDateView.getText())) { + Log.d("SsBaseTemplateCard", "Date view text is empty"); + } + TapAction tapAction = new TapAction.Builder((mTemplateData.getPrimaryItem() == null || mTemplateData.getPrimaryItem().getTapAction() == null) ? UUID.randomUUID().toString() : mTemplateData.getPrimaryItem().getTapAction().getId().toString()).setIntent(BcSmartSpaceUtil.getOpenCalendarIntent()).build(); + BcSmartSpaceUtil.setOnClickListener(this, mTarget, tapAction, eventNotifier, "SsBaseTemplateCard", loggingInfo, 0); + BcSmartSpaceUtil.setOnClickListener(mDateView, mTarget, tapAction, eventNotifier, "SsBaseTemplateCard", loggingInfo, 0); + } + setUpTextView(mTitleTextView, mTemplateData.getPrimaryItem(), eventNotifier, mDateView == null); + setUpTextView(mSubtitleTextView, mTemplateData.getSubtitleItem(), eventNotifier, true); + setUpTextView(mSubtitleSupplementalView, mTemplateData.getSubtitleSupplementalItem(), eventNotifier, true); + setUpTextView(mSupplementalLineTextView, mTemplateData.getSupplementalLineItem(), eventNotifier, true); + if (mExtrasGroup != null) { + if (mSupplementalLineTextView == null || mSupplementalLineTextView.getVisibility() != View.VISIBLE || (mShouldShowPageIndicator && mDateView == null)) { + BcSmartspaceTemplateDataUtils.updateVisibility(mExtrasGroup, View.GONE); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(mExtrasGroup, View.VISIBLE); + updateZenColors(); + } + } + BcSmartspaceTemplateDataUtils.updateVisibility(mSubtitleGroup, mSubtitleTextView.getVisibility() != View.GONE || mSubtitleSupplementalView.getVisibility() != View.GONE ? View.VISIBLE : View.GONE); + if (target.getFeatureType() == 1 && mSubtitleSupplementalView != null && mSubtitleSupplementalView.getVisibility() == View.VISIBLE) { + mSubtitleTextView.setEllipsize(null); + } + if (mDateView == null && mTemplateData.getPrimaryItem() != null && mTemplateData.getPrimaryItem().getTapAction() != null) { + BcSmartSpaceUtil.setOnClickListener(this, target, mTemplateData.getPrimaryItem().getTapAction(), eventNotifier, "SsBaseTemplateCard", mLoggingInfo, 0); + if (mDateView == null && mTitleTextView != null && mTitleTextView.getVisibility() == View.VISIBLE && mSubtitleTextView != null && mSubtitleTextView.getVisibility() == View.VISIBLE && mTemplateData.getPrimaryItem() != null && mTemplateData.getPrimaryItem().getTapAction() != null && mTemplateData.getSubtitleItem() != null && mTemplateData.getSubtitleItem().getTapAction() != null) { + if (mTemplateData.getPrimaryItem().getTapAction().getIntent() != null && !mTemplateData.getPrimaryItem().getTapAction().getIntent().filterEquals(mTemplateData.getSubtitleItem().getTapAction().getIntent())) { + Log.d("SsBaseTemplateCard", "Primary item tapAction intent = " + mTemplateData.getPrimaryItem().getTapAction().getIntent()); + Log.d("SsBaseTemplateCard", "Subtitle item tapAction intent = " + mTemplateData.getSubtitleItem().getTapAction().getIntent()); + } else if (mTemplateData.getPrimaryItem().getTapAction().getPendingIntent() == null || mTemplateData.getPrimaryItem().getTapAction().getPendingIntent().equals(mTemplateData.getSubtitleItem().getTapAction().getPendingIntent())) { + mTitleTextView.setOnClickListener(null); + mTitleTextView.setClickable(false); + mSubtitleTextView.setOnClickListener(null); + mSubtitleTextView.setClickable(false); + } else { + Log.d("SsBaseTemplateCard", "Primary item tapAction pendingIntent = " + mTemplateData.getPrimaryItem().getTapAction().getPendingIntent()); + Log.d("SsBaseTemplateCard", "Subtitle item tapAction pendingIntent = " + mTemplateData.getSubtitleItem().getTapAction().getPendingIntent()); + } + } + } + if (mSecondaryCardPane == null) { + Log.i("SsBaseTemplateCard", "Secondary card pane is null"); + return; + } + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mSecondaryCardPane.getLayoutParams(); + params.matchConstraintMaxWidth = getWidth() / 2; + mSecondaryCardPane.setLayoutParams(params); + mTouchDelegateIsDirty = true; + } + + @Override + public final AccessibilityNodeInfo createAccessibilityNodeInfo() { + AccessibilityNodeInfo info = super.createAccessibilityNodeInfo(); + AccessibilityNodeInfoCompat.wrap(info).setRoleDescription(" "); + return info; + } + + @Override + public final BcSmartspaceCardLoggingInfo getLoggingInfo() { + if (mLoggingInfo != null) { + return mLoggingInfo; + } + BcSmartspaceCardLoggingInfo.Builder builder = + new BcSmartspaceCardLoggingInfo.Builder() + .setDisplaySurface( + BcSmartSpaceUtil.getLoggingDisplaySurface(mUiSurface, mDozeAmount)) + .setFeatureType(mFeatureType) + .setUid(-1); + builder.setDimensionalInfo( + BcSmartspaceCardLoggerUtil.createDimensionalLoggingInfo(mTarget.getTemplateData())); + return new BcSmartspaceCardLoggingInfo(builder); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + setPaddingRelative(getResources().getDimensionPixelSize(R.dimen.non_remoteviews_card_padding_start), getPaddingTop(), getPaddingEnd(), getPaddingBottom()); + mTextGroup = findViewById(R.id.text_group); + mSecondaryCardPane = findViewById(R.id.secondary_card_group); + mDateView = findViewById(R.id.date); + mTitleTextView = findViewById(R.id.title_text); + mSubtitleGroup = findViewById(R.id.smartspace_subtitle_group); + mSubtitleTextView = findViewById(R.id.subtitle_text); + mSubtitleSupplementalView = findViewById(R.id.base_action_icon_subtitle); + mExtrasGroup = findViewById(R.id.smartspace_extras_group); + if (mSubtitleTextView != null) { + mSubtitleHitRect = new Rect(); + } + if (mSubtitleSupplementalView != null) { + mSubtitleSupplementalHitRect = new Rect(); + } + if (mSubtitleTextView != null || mSubtitleSupplementalView != null) { + setTouchDelegate(mTouchDelegateComposite); + } + if (mExtrasGroup != null) { + mSupplementalLineTextView = mExtrasGroup.findViewById(R.id.supplemental_line_text); + } + if (mBgHandler != null) { + mDateView.mBgHandler = mBgHandler; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!changed && !mTouchDelegateIsDirty) { + return; + } + mTouchDelegateIsDirty = false; + TouchDelegate touchDelegate = getTouchDelegate(); + if (touchDelegate == null || !(touchDelegate instanceof TouchDelegateComposite)) { + return; + } + TouchDelegateComposite composite = (TouchDelegateComposite) touchDelegate; + composite.mDelegates.clear(); + if (mSubtitleGroup == null || mSubtitleGroup.getVisibility() != View.VISIBLE) { + return; + } + boolean subtitleTextVisible = mSubtitleTextView != null + && mSubtitleTextView.getVisibility() == View.VISIBLE + && mSubtitleTextView.hasOnClickListeners(); + boolean subtitleSupplementalVisible = mSubtitleSupplementalView != null + && mSubtitleSupplementalView.getVisibility() == View.VISIBLE + && mSubtitleSupplementalView.hasOnClickListeners(); + if (!subtitleTextVisible && !subtitleSupplementalVisible) { + return; + } + int padding = + getResources().getDimensionPixelSize(R.dimen.subtitle_hit_rect_height) + - mSubtitleGroup.getHeight(); + padding = Math.max(padding / 2, 0); + if (padding <= 0 && mSubtitleGroup.getBottom() == getHeight()) { + return; + } + if (subtitleTextVisible) { + mSubtitleTextView.getHitRect(mSubtitleHitRect); + offsetDescendantRectToMyCoords(mSubtitleGroup, mSubtitleHitRect); + if (padding > 0) { + mSubtitleHitRect.top -= padding; + } + mSubtitleHitRect.bottom = getBottom(); + composite.mDelegates.add(new TouchDelegate(mSubtitleHitRect, mSubtitleTextView)); + } + if (subtitleSupplementalVisible) { + mSubtitleSupplementalView.getHitRect(mSubtitleSupplementalHitRect); + offsetDescendantRectToMyCoords(mSubtitleGroup, mSubtitleSupplementalHitRect); + if (padding > 0) { + mSubtitleSupplementalHitRect.top -= padding; + } + mSubtitleSupplementalHitRect.bottom = getBottom(); + composite.mDelegates.add( + new TouchDelegate(mSubtitleSupplementalHitRect, mSubtitleSupplementalView)); + } + } + + public final void resetTextView(DoubleShadowTextView textView) { + if (textView == null) { + return; + } + textView.setCompoundDrawablesRelative(null, null, null, null); + textView.setOnClickListener(null); + textView.setClickable(false); + textView.setContentDescription(null); + textView.setText((CharSequence) null); + textView.setTranslationX(0.0f); + } + + @Override + public final void setDozeAmount(float dozeAmount) { + mDozeAmount = dozeAmount; + if (mTarget != null && mTarget.getBaseAction() != null && mTarget.getBaseAction().getExtras() != null) { + Bundle extras = mTarget.getBaseAction().getExtras(); + if (mTitleTextView != null && extras.getBoolean("hide_title_on_aod")) { + mTitleTextView.setAlpha(1.0f - dozeAmount); + } + if (mSubtitleTextView != null && extras.getBoolean("hide_subtitle_on_aod")) { + mSubtitleTextView.setAlpha(1.0f - dozeAmount); + } + } + if (mTextGroup == null) { + return; + } + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondaryCardPane, (mDozeAmount == 1.0f || !mValidSecondaryCard) ? View.GONE : View.VISIBLE); + if (mSecondaryCardPane == null || mSecondaryCardPane.getVisibility() == View.GONE) { + mTextGroup.setTranslationX(0.0f); + return; + } + mTextGroup.setTranslationX(((PathInterpolator) Interpolators.EMPHASIZED).getInterpolation(mDozeAmount) * mSecondaryCardPane.getWidth() * (isRtl() ? 1 : -1)); + mSecondaryCardPane.setAlpha(Math.max(0.0f, Math.min(1.0f, ((1.0f - mDozeAmount) * 9.0f) - 6.0f))); + } + + @Override + public final void setPrimaryTextColor(int color) { + mIconTintColor = color; + if (mTitleTextView != null) { + mTitleTextView.setTextColor(color); + if (mTemplateData != null) { + updateTextViewIconTint(mTitleTextView, shouldTint(mTemplateData.getPrimaryItem())); + } + } + if (mDateView != null) { + mDateView.setTextColor(color); + } + if (mSubtitleTextView != null) { + mSubtitleTextView.setTextColor(color); + if (mTemplateData != null) { + updateTextViewIconTint(mSubtitleTextView, shouldTint(mTemplateData.getSubtitleItem())); + } + } + if (mSubtitleSupplementalView != null) { + mSubtitleSupplementalView.setTextColor(color); + if (mTemplateData != null) { + updateTextViewIconTint(mSubtitleSupplementalView, shouldTint(mTemplateData.getSubtitleSupplementalItem())); + } + } + updateZenColors(); + } + + @Override + public final void setScreenOn(boolean screenOn) { + if (mDateView != null) { + mDateView.mIsInteractive = screenOn; + mDateView.rescheduleTicker(); + } + } + public final void setSecondaryCard(BcSmartspaceCardSecondary secondaryCard) { + if (mSecondaryCardPane == null) { + return; + } + mSecondaryCard = secondaryCard; + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondaryCardPane, View.GONE); + mSecondaryCardPane.removeAllViews(); + if (secondaryCard != null) { + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_card_height)); + params.setMarginStart(getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_secondary_card_start_margin)); + params.startToStart = 0; + params.topToTop = 0; + params.bottomToBottom = 0; + mSecondaryCardPane.addView(secondaryCard, params); + } + } + + public final void setUpTextView(DoubleShadowTextView textView, BaseTemplateData.SubItemInfo subItemInfo, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, boolean z) { + if (textView == null) { + Log.d("SsBaseTemplateCard", "No text view can be set up"); + return; + } + resetTextView(textView); + if (subItemInfo == null) { + Log.d("SsBaseTemplateCard", "Passed-in item info is null"); + BcSmartspaceTemplateDataUtils.updateVisibility(textView, View.GONE); + return; + } + Text text = subItemInfo.getText(); + BcSmartspaceTemplateDataUtils.setText(textView, subItemInfo.getText()); + if (!SmartspaceUtils.isEmpty(text)) { + textView.setTextColor(this.mIconTintColor); + } + Icon icon = subItemInfo.getIcon(); + if (icon != null) { + DoubleShadowIconDrawable drawable = new DoubleShadowIconDrawable(getContext()); + drawable.setIcon(BcSmartSpaceUtil.getIconDrawableWithCustomSize(icon.getIcon(), getContext(), getContext().getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_icon_size))); + textView.setCompoundDrawablesRelative(drawable, null, null, null); + ContentDescriptionUtil.setFormattedContentDescription("SsBaseTemplateCard", textView, SmartspaceUtils.isEmpty(text) ? "" : text.getText(), icon.getContentDescription()); + updateTextViewIconTint(textView, icon.shouldTint()); + if (z) { + BcSmartspaceTemplateDataUtils.offsetTextViewForIcon(textView, drawable, isRtl()); + } + } + int subCardRank = 0; + BcSmartspaceTemplateDataUtils.updateVisibility(textView, View.VISIBLE); + TapAction tapAction = subItemInfo.getTapAction(); + if (mLoggingInfo != null && mLoggingInfo.mSubcardInfo != null && mLoggingInfo.mSubcardInfo.mSubcards != null && !mLoggingInfo.mSubcardInfo.mSubcards.isEmpty() && subItemInfo.getLoggingInfo() != null) { + int targetFeatureType = subItemInfo.getLoggingInfo().getFeatureType(); + if (targetFeatureType != mLoggingInfo.mFeatureType) { + for (int i = 0; i < mLoggingInfo.mSubcardInfo.mSubcards.size(); i++) { + BcSmartspaceCardMetadataLoggingInfo subCard = + mLoggingInfo.mSubcardInfo.mSubcards.get(i); + if (subCard.mInstanceId == subItemInfo.getLoggingInfo().getInstanceId() + && subCard.mCardTypeId == targetFeatureType) { + subCardRank = i + 1; + break; + } + } + } + } + BcSmartSpaceUtil.setOnClickListener(textView, mTarget, tapAction, eventNotifier, "SsBaseTemplateCard", mLoggingInfo, subCardRank); + } + + public final void updateTextViewIconTint(DoubleShadowTextView textView, boolean shouldTint) { + for (Drawable drawable : textView.getCompoundDrawablesRelative()) { + if (drawable != null) { + if (shouldTint) { + drawable.setTint(mIconTintColor); + } else { + drawable.setTintList(null); + } + } + } + } + + public final void updateZenColors() { + if (mSupplementalLineTextView != null) { + mSupplementalLineTextView.setTextColor(mIconTintColor); + if (BcSmartspaceCardLoggerUtil.containsValidTemplateType(mTemplateData)) { + updateTextViewIconTint(mSupplementalLineTextView, shouldTint(mTemplateData.getSupplementalLineItem())); + } + } + } + + public BaseTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + mSecondaryCard = null; + mFeatureType = 0; + mLoggingInfo = null; + mIconTintColor = GraphicsUtils.getAttrColor(context, android.R.attr.textColorPrimary); + mTextGroup = null; + mSecondaryCardPane = null; + mDateView = null; + mTitleTextView = null; + mSubtitleGroup = null; + mSubtitleTextView = null; + mSubtitleSupplementalView = null; + mSubtitleHitRect = null; + mSubtitleSupplementalHitRect = null; + mExtrasGroup = null; + mSupplementalLineTextView = null; + mTouchDelegateComposite = new TouchDelegateComposite(new Rect(), this); + mTouchDelegateIsDirty = false; + context.getTheme().applyStyle(R.style.Smartspace, false); + setDefaultFocusHighlightEnabled(false); + } + + @Override + public final View getView() { + return this; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CarouselTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CarouselTemplateCard.java new file mode 100644 index 000000000000..2840b63ba5aa --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CarouselTemplateCard.java @@ -0,0 +1,133 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.CarouselTemplateData; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.Constraints; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; + +public class CarouselTemplateCard extends BcSmartspaceCardSecondary { + public CarouselTemplateCard(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + ConstraintLayout[] columns = new ConstraintLayout[4]; + for (int i = 0; i < 4; i++) { + ConstraintLayout column = (ConstraintLayout) ViewGroup.inflate(getContext(), R.layout.smartspace_carousel_column_template_card, null); + column.setId(View.generateViewId()); + columns[i] = column; + } + for (int i = 0; i < 4; i++) { + Constraints.LayoutParams params = new Constraints.LayoutParams(-2, 0); + ConstraintLayout prevColumn = i > 0 ? columns[i - 1] : null; + ConstraintLayout nextColumn = i < 3 ? columns[i + 1] : null; + if (i == 0) { + params.startToStart = 0; + params.horizontalChainStyle = 1; + } else { + params.startToEnd = prevColumn.getId(); + } + if (i == 3) { + params.endToEnd = 0; + } else { + params.endToStart = nextColumn.getId(); + } + params.topToTop = 0; + params.bottomToBottom = 0; + addView(columns[i], params); + } + } + + @Override + public final void resetUi() { + for (int i = 0; i < getChildCount(); i++) { + View column = getChildAt(i); + BcSmartspaceTemplateDataUtils.updateVisibility(column.findViewById(R.id.upper_text), View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(column.findViewById(R.id.icon), View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(column.findViewById(R.id.lower_text), View.GONE); + } + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + CarouselTemplateData templateData = (CarouselTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData) || templateData.getCarouselItems() == null) { + Log.w("CarouselTemplateCard", "CarouselTemplateData is null or has no CarouselItem or invalid template type"); + return false; + } + List carouselItems = templateData.getCarouselItems(); + long validItemsCount = + carouselItems.stream() + .filter( + item -> + item.getImage() != null + && item.getLowerText() != null + && item.getUpperText() != null) + .count(); + int validItems = (int) validItemsCount; + if (validItems < 4) { + Log.w("CarouselTemplateCard", "Hiding " + (4 - validItems) + " incomplete column(s)."); + for (int i = 0; i < 4; i++) { + BcSmartspaceTemplateDataUtils.updateVisibility(getChildAt(i), i <= (3 - (4 - validItems)) ? View.VISIBLE : View.GONE); + } + ((ConstraintLayout.LayoutParams) ((ConstraintLayout) getChildAt(0)).getLayoutParams()).horizontalChainStyle = (4 - validItems) == 0 ? 1 : 0; + } + for (int i = 0; i < validItems; i++) { + TextView upperText = getChildAt(i).findViewById(R.id.upper_text); + ImageView icon = getChildAt(i).findViewById(R.id.icon); + TextView lowerText = getChildAt(i).findViewById(R.id.lower_text); + BcSmartspaceTemplateDataUtils.setText(upperText, ((CarouselTemplateData.CarouselItem) carouselItems.get(i)).getUpperText()); + BcSmartspaceTemplateDataUtils.updateVisibility(upperText, View.VISIBLE); + BcSmartspaceTemplateDataUtils.setIcon(icon, ((CarouselTemplateData.CarouselItem) carouselItems.get(i)).getImage()); + BcSmartspaceTemplateDataUtils.updateVisibility(icon, View.VISIBLE); + BcSmartspaceTemplateDataUtils.setText(lowerText, ((CarouselTemplateData.CarouselItem) carouselItems.get(i)).getLowerText()); + BcSmartspaceTemplateDataUtils.updateVisibility(lowerText, View.VISIBLE); + } + if (templateData.getCarouselAction() != null) { + BcSmartSpaceUtil.setOnClickListener(this, target, templateData.getCarouselAction(), eventNotifier, "CarouselTemplateCard", loggingInfo, 0); + } + for (CarouselTemplateData.CarouselItem item : templateData.getCarouselItems()) { + if (item.getTapAction() != null) { + BcSmartSpaceUtil.setOnClickListener(this, target, item.getTapAction(), eventNotifier, "CarouselTemplateCard", loggingInfo, 0); + } + } + return true; + } + + @Override + public final void setTextColor(int color) { + for (int i = 0; i < getChildCount(); i++) { + View column = getChildAt(i); + TextView upperText = column.findViewById(R.id.upper_text); + upperText.setTextColor(color); + TextView lowerText = column.findViewById(R.id.lower_text); + lowerText.setTextColor(color); + } + } + + public CarouselTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CombinedCardsTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CombinedCardsTemplateCard.java new file mode 100644 index 000000000000..7f29aeeea58e --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/CombinedCardsTemplateCard.java @@ -0,0 +1,106 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.BaseTemplateData; +import android.app.smartspace.uitemplatedata.CombinedCardsTemplateData; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; + +public class CombinedCardsTemplateCard extends BcSmartspaceCardSecondary { + public ConstraintLayout mFirstSubCard; + public ConstraintLayout mSecondSubCard; + + public CombinedCardsTemplateCard(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mFirstSubCard = findViewById(R.id.first_sub_card_container); + mSecondSubCard = findViewById(R.id.second_sub_card_container); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstSubCard, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondSubCard, View.GONE); + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + reset(target.getSmartspaceTargetId()); + CombinedCardsTemplateData templateData = (CombinedCardsTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData) || templateData.getCombinedCardDataList().isEmpty()) { + Log.w("CombinedCardsTemplateCard", "TemplateData is null or empty or invalid template type"); + return false; + } + List combinedCardDataList = templateData.getCombinedCardDataList(); + BaseTemplateData firstCardData = combinedCardDataList.get(0); + BaseTemplateData secondCardData = combinedCardDataList.size() > 1 ? combinedCardDataList.get(1) : null; + return setupSubCard(mFirstSubCard, firstCardData, target, eventNotifier, loggingInfo) && (secondCardData == null || setupSubCard(mSecondSubCard, secondCardData, target, eventNotifier, loggingInfo)); + } + + @Override + public void setTextColor(int color) { + if (mFirstSubCard.getChildCount() > 0) { + BcSmartspaceCardSecondary firstSubCard = + (BcSmartspaceCardSecondary) mFirstSubCard.getChildAt(0); + firstSubCard.setTextColor(color); + } + if (mSecondSubCard.getChildCount() > 0) { + BcSmartspaceCardSecondary secondSubCard = + (BcSmartspaceCardSecondary) mSecondSubCard.getChildAt(0); + secondSubCard.setTextColor(color); + } + } + + public final boolean setupSubCard(ViewGroup container, BaseTemplateData templateData, SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + if (templateData == null) { + BcSmartspaceTemplateDataUtils.updateVisibility(container, View.GONE); + Log.w("CombinedCardsTemplateCard", "Sub-card templateData is null or empty"); + return false; + } + Integer subCardResId = + BcSmartspaceTemplateDataUtils.TEMPLATE_TYPE_TO_SECONDARY_CARD_RES.get( + templateData.getTemplateType()); + if (subCardResId == 0) { + BcSmartspaceTemplateDataUtils.updateVisibility(container, View.GONE); + Log.w("CombinedCardsTemplateCard", "Combined sub-card res is null. Cannot set it up"); + return false; + } + BcSmartspaceCardSecondary subCard = (BcSmartspaceCardSecondary) LayoutInflater.from(container.getContext()).inflate(subCardResId, container, false); + subCard.setSmartspaceActions(new SmartspaceTarget.Builder(target.getSmartspaceTargetId(), target.getComponentName(), target.getUserHandle()).setTemplateData(templateData).build(), eventNotifier, loggingInfo); + container.removeAllViews(); + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, getResources().getDimensionPixelSize(R.dimen.enhanced_smartspace_card_height)); + params.startToStart = 0; + params.endToEnd = 0; + params.topToTop = 0; + params.bottomToBottom = 0; + BcSmartspaceTemplateDataUtils.updateVisibility(subCard, View.VISIBLE); + container.addView(subCard, params); + BcSmartspaceTemplateDataUtils.updateVisibility(container, View.VISIBLE); + return true; + } + + public CombinedCardsTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/HeadToHeadTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/HeadToHeadTemplateCard.java new file mode 100644 index 000000000000..df4366053bb4 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/HeadToHeadTemplateCard.java @@ -0,0 +1,147 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.HeadToHeadTemplateData; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.Text; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class HeadToHeadTemplateCard extends BcSmartspaceCardSecondary { + public ImageView mFirstCompetitorIcon; + public TextView mFirstCompetitorText; + public TextView mHeadToHeadTitle; + public ImageView mSecondCompetitorIcon; + public TextView mSecondCompetitorText; + + public HeadToHeadTemplateCard(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mHeadToHeadTitle = findViewById(R.id.head_to_head_title); + mFirstCompetitorText = findViewById(R.id.first_competitor_text); + mSecondCompetitorText = findViewById(R.id.second_competitor_text); + mFirstCompetitorIcon = findViewById(R.id.first_competitor_icon); + mSecondCompetitorIcon = findViewById(R.id.second_competitor_icon); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mHeadToHeadTitle, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorText, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorText, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorIcon, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorIcon, View.GONE); + } + + @Override + public boolean setSmartspaceActions( + SmartspaceTarget target, + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, + BcSmartspaceCardLoggingInfo loggingInfo) { + HeadToHeadTemplateData templateData = (HeadToHeadTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData)) { + Log.w( + "HeadToHeadTemplateCard", + "HeadToHeadTemplateData is null or invalid template type"); + return false; + } + + boolean isValid = false; + + Text title = templateData.getHeadToHeadTitle(); + if (title != null) { + if (mHeadToHeadTitle == null) { + Log.w("HeadToHeadTemplateCard", "No head-to-head title view to update"); + } else { + BcSmartspaceTemplateDataUtils.setText(mHeadToHeadTitle, title); + BcSmartspaceTemplateDataUtils.updateVisibility(mHeadToHeadTitle, View.VISIBLE); + isValid = true; + } + } + + Text firstCompetitorText = templateData.getHeadToHeadFirstCompetitorText(); + if (firstCompetitorText != null) { + if (mFirstCompetitorText == null) { + Log.w("HeadToHeadTemplateCard", "No first competitor text view to update"); + } else { + BcSmartspaceTemplateDataUtils.setText(mFirstCompetitorText, firstCompetitorText); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorText, View.VISIBLE); + isValid = true; + } + } + + Text secondCompetitorText = templateData.getHeadToHeadSecondCompetitorText(); + if (secondCompetitorText != null) { + if (mSecondCompetitorText == null) { + Log.w("HeadToHeadTemplateCard", "No second competitor text view to update"); + } else { + BcSmartspaceTemplateDataUtils.setText(mSecondCompetitorText, secondCompetitorText); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorText, View.VISIBLE); + isValid = true; + } + } + + Icon firstCompetitorIcon = templateData.getHeadToHeadFirstCompetitorIcon(); + if (firstCompetitorIcon != null) { + if (mFirstCompetitorIcon == null) { + Log.w("HeadToHeadTemplateCard", "No first competitor icon view to update"); + } else { + BcSmartspaceTemplateDataUtils.setIcon(mFirstCompetitorIcon, firstCompetitorIcon); + BcSmartspaceTemplateDataUtils.updateVisibility(mFirstCompetitorIcon, View.VISIBLE); + isValid = true; + } + } + + Icon secondCompetitorIcon = templateData.getHeadToHeadSecondCompetitorIcon(); + if (secondCompetitorIcon != null) { + if (mSecondCompetitorIcon == null) { + Log.w("HeadToHeadTemplateCard", "No second competitor icon view to update"); + } else { + BcSmartspaceTemplateDataUtils.setIcon(mSecondCompetitorIcon, secondCompetitorIcon); + BcSmartspaceTemplateDataUtils.updateVisibility(mSecondCompetitorIcon, View.VISIBLE); + isValid = true; + } + } + + if (isValid && templateData.getHeadToHeadAction() != null) { + BcSmartSpaceUtil.setOnClickListener( + this, + target, + templateData.getHeadToHeadAction(), + eventNotifier, + "HeadToHeadTemplateCard", + loggingInfo, + 0); + } + + return isValid; + } + + @Override + public final void setTextColor(int color) { + mFirstCompetitorText.setTextColor(color); + mSecondCompetitorText.setTextColor(color); + } + + public HeadToHeadTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubCardTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubCardTemplateCard.java new file mode 100644 index 000000000000..ae01d4e987ae --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubCardTemplateCard.java @@ -0,0 +1,95 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceUtils; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.SubCardTemplateData; +import android.app.smartspace.uitemplatedata.Text; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +public class SubCardTemplateCard extends BcSmartspaceCardSecondary { + public ImageView mImageView; + public TextView mTextView; + + public SubCardTemplateCard(Context context) { + super(context); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mImageView = findViewById(R.id.image_view); + mTextView = findViewById(R.id.card_prompt); + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.GONE); + BcSmartspaceTemplateDataUtils.updateVisibility(mTextView, View.GONE); + } + + @Override + public boolean setSmartspaceActions( + SmartspaceTarget target, + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, + BcSmartspaceCardLoggingInfo loggingInfo) { + SubCardTemplateData templateData = (SubCardTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData)) { + Log.w("SubCardTemplateCard", "SubCardTemplateData is null or invalid template type"); + return false; + } + + boolean isValid = false; + + Icon subCardIcon = templateData.getSubCardIcon(); + if (subCardIcon != null) { + BcSmartspaceTemplateDataUtils.setIcon(mImageView, subCardIcon); + BcSmartspaceTemplateDataUtils.updateVisibility(mImageView, View.VISIBLE); + isValid = true; + } + + Text subCardText = templateData.getSubCardText(); + if (!SmartspaceUtils.isEmpty(subCardText)) { + BcSmartspaceTemplateDataUtils.setText(mTextView, subCardText); + BcSmartspaceTemplateDataUtils.updateVisibility(mTextView, View.VISIBLE); + isValid = true; + } + + if (isValid && templateData.getSubCardAction() != null) { + BcSmartSpaceUtil.setOnClickListener( + this, + target, + templateData.getSubCardAction(), + eventNotifier, + "SubCardTemplateCard", + loggingInfo, + 0); + } + + return isValid; + } + + @Override + public void setTextColor(int color) { + mTextView.setTextColor(color); + } + + public SubCardTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubImageTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubImageTemplateCard.java new file mode 100644 index 000000000000..785a47e9b02f --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubImageTemplateCard.java @@ -0,0 +1,263 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.Icon; +import android.app.smartspace.uitemplatedata.SubImageTemplateData; +import android.app.smartspace.uitemplatedata.TapAction; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.ImageDecoder; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon.OnDrawableLoadedListener; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.VarHandle; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class SubImageTemplateCard extends BcSmartspaceCardSecondary { + private static final String TAG = "SubImageTemplateCard"; + + public final Handler mHandler; + public final Map mIconDrawableCache; + public final int mImageHeight; + public ImageView mImageView; + + public SubImageTemplateCard(Context context) { + this(context, null); + } + + public SubImageTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + mIconDrawableCache = new HashMap<>(); + mHandler = new Handler(); + mImageHeight = getResources().getDimensionPixelOffset(R.dimen.enhanced_smartspace_card_height); + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mImageView = findViewById(R.id.image_view); + } + + @Override + public final void resetUi() { + if (mIconDrawableCache != null) { + mIconDrawableCache.clear(); + } + if (mImageView != null) { + mImageView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + mImageView.setImageDrawable(null); + mImageView.setBackgroundTintList(null); + } + } + + @Override + public final void setTextColor(int color) { + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, + BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, + BcSmartspaceCardLoggingInfo loggingInfo) { + + SubImageTemplateData templateData = (SubImageTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData) || + templateData.getSubImages() == null || templateData.getSubImages().isEmpty()) { + Log.w(TAG, "SubImageTemplateData is null or has no SubImage or invalid template type"); + return false; + } + + List subImages = templateData.getSubImages(); + TapAction tapAction = templateData.getSubImageAction(); + + if (mImageView == null) { + Log.w(TAG, "No image view can be updated. Skipping background update..."); + } else if (tapAction != null && tapAction.getExtras() != null) { + Bundle extras = tapAction.getExtras(); + String dimensionRatio = extras.getString("imageDimensionRatio", ""); + if (!TextUtils.isEmpty(dimensionRatio)) { + mImageView.getLayoutParams().width = 0; + ((ConstraintLayout.LayoutParams) mImageView.getLayoutParams()).dimensionRatio = dimensionRatio; + } + if (extras.getBoolean("shouldShowBackground", false)) { + mImageView.setBackgroundTintList(ColorStateList.valueOf( + getContext().getColor(R.color.smartspace_button_background))); + } + } + + int frameDurationMillis = (tapAction != null && tapAction.getExtras() != null) + ? tapAction.getExtras().getInt("GifFrameDurationMillis", 200) + : 200; + + ContentResolver contentResolver = getContext().getApplicationContext().getContentResolver(); + TreeMap frameMap = new TreeMap<>(); + WeakReference imageViewRef = new WeakReference<>(mImageView); + String prevTargetId = mPrevSmartspaceTargetId; + + for (int i = 0; i < subImages.size(); i++) { + int index = i; + Icon subImage = subImages.get(i); + if (subImage == null || subImage.getIcon() == null) continue; + + android.graphics.drawable.Icon icon = subImage.getIcon(); + String cacheKey = getCacheKey(icon); + + OnDrawableLoadedListener listener = drawable -> { + if (!prevTargetId.equals(mPrevSmartspaceTargetId)) { + Log.d(TAG, "SmartspaceTarget has changed. Skip the loaded result..."); + return; + } + + if (drawable != null) { + mIconDrawableCache.put(cacheKey, drawable); + frameMap.put(index, drawable); + } + + if (frameMap.size() == subImages.size()) { + createAndStartAnimation(frameMap, frameDurationMillis, imageViewRef); + } + }; + + if (mIconDrawableCache.containsKey(cacheKey)) { + listener.onDrawableLoaded(mIconDrawableCache.get(cacheKey)); + } else if (icon.getType() == android.graphics.drawable.Icon.TYPE_URI) { + DrawableWrapper wrapper = new DrawableWrapper(); + wrapper.mUri = icon.getUri(); + wrapper.mHeightInPx = mImageHeight; + wrapper.mContentResolver = contentResolver; + wrapper.mListener = listener; + new LoadUriTask().execute(wrapper); + } else { + icon.loadDrawableAsync(getContext(), listener, mHandler); + } + } + + if (tapAction != null) { + BcSmartSpaceUtil.setOnClickListener(this, target, tapAction, + eventNotifier, TAG, loggingInfo, 0); + } + + return true; + } + + private String getCacheKey(android.graphics.drawable.Icon icon) { + StringBuilder keyBuilder = new StringBuilder().append(icon.getType()); + switch (icon.getType()) { + case android.graphics.drawable.Icon.TYPE_BITMAP: + case android.graphics.drawable.Icon.TYPE_ADAPTIVE_BITMAP: + keyBuilder.append(icon.getBitmap().hashCode()); + break; + case android.graphics.drawable.Icon.TYPE_RESOURCE: + keyBuilder.append(icon.getResPackage()).append(String.format("0x%08x", icon.getResId())); + break; + case android.graphics.drawable.Icon.TYPE_DATA: + keyBuilder.append(Arrays.hashCode(icon.getDataBytes())); + break; + case android.graphics.drawable.Icon.TYPE_URI: + case android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP: + keyBuilder.append(icon.getUriString()); + break; + } + return keyBuilder.toString(); + } + + private void createAndStartAnimation(TreeMap drawables, int duration, WeakReference imageViewRef) { + List validDrawables = drawables.values().stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + ImageView iv = imageViewRef.get(); + if (validDrawables.isEmpty()) { + Log.w(TAG, "All images failed to load. Resetting imageView"); + if (iv != null) { + iv.getLayoutParams().width = -2; + iv.setImageDrawable(null); + iv.setBackgroundTintList(null); + } + return; + } + + AnimationDrawable animationDrawable = new AnimationDrawable(); + for (Drawable d : validDrawables) { + animationDrawable.addFrame(d, duration); + } + + if (iv != null) { + iv.setImageDrawable(animationDrawable); + int intrinsicWidth = animationDrawable.getIntrinsicWidth(); + if (iv.getLayoutParams().width != intrinsicWidth) { + iv.getLayoutParams().width = intrinsicWidth; + iv.requestLayout(); + } + animationDrawable.start(); + } + } + + private static final class DrawableWrapper { + ContentResolver mContentResolver; + Drawable mDrawable; + int mHeightInPx; + OnDrawableLoadedListener mListener; + Uri mUri; + } + + private static final class LoadUriTask extends AsyncTask { + @Override + protected DrawableWrapper doInBackground(DrawableWrapper... wrappers) { + if (wrappers.length == 0) return null; + DrawableWrapper wrapper = wrappers[0]; + try (InputStream inputStream = wrapper.mContentResolver.openInputStream(wrapper.mUri)) { + ImageDecoder.Source source = ImageDecoder.createSource(null, inputStream); + + wrapper.mDrawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); + int height = info.getSize().getHeight(); + float ratio = height != 0 ? (float) info.getSize().getWidth() / height : 0.0f; + decoder.setTargetSize((int) (wrapper.mHeightInPx * ratio), wrapper.mHeightInPx); + }); + } catch (Exception e) { + Log.w(TAG, "Failed to load uri: " + wrapper.mUri, e); + } + return wrapper; + } + + @Override + protected void onPostExecute(DrawableWrapper wrapper) { + if (wrapper != null && wrapper.mListener != null) { + wrapper.mListener.onDrawableLoaded(wrapper.mDrawable); + } + } + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubListTemplateCard.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubListTemplateCard.java new file mode 100644 index 000000000000..1c365afd2c7a --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/uitemplate/SubListTemplateCard.java @@ -0,0 +1,115 @@ +package com.google.android.systemui.smartspace.uitemplate; + +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.uitemplatedata.SubListTemplateData; +import android.app.smartspace.uitemplatedata.Text; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import com.google.android.systemui.smartspace.BcSmartSpaceUtil; +import com.google.android.systemui.smartspace.BcSmartspaceCardSecondary; +import com.google.android.systemui.smartspace.BcSmartspaceTemplateDataUtils; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggerUtil; +import com.google.android.systemui.smartspace.logging.BcSmartspaceCardLoggingInfo; + +import com.android.systemui.res.R; + +import java.util.List; +import java.util.Locale; + +public class SubListTemplateCard extends BcSmartspaceCardSecondary { + public static final int[] LIST_ITEM_TEXT_VIEW_IDS = {R.id.list_item_1, R.id.list_item_2, R.id.list_item_3}; + public ImageView mListIconView; + public final TextView[] mListItems; + + public SubListTemplateCard(Context context) { + super(context); + mListItems = new TextView[3]; + } + + @Override + public final void onFinishInflate() { + super.onFinishInflate(); + mListIconView = findViewById(R.id.list_icon); + for (int i = 0; i < 3; i++) { + mListItems[i] = findViewById(LIST_ITEM_TEXT_VIEW_IDS[i]); + } + } + + @Override + public final void resetUi() { + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, View.GONE); + for (int i = 0; i < 3; i++) { + BcSmartspaceTemplateDataUtils.updateVisibility(mListItems[i], View.GONE); + } + } + + @Override + public final boolean setSmartspaceActions(SmartspaceTarget target, BcSmartspaceDataPlugin.SmartspaceEventNotifier eventNotifier, BcSmartspaceCardLoggingInfo loggingInfo) { + reset(target.getSmartspaceTargetId()); + SubListTemplateData templateData = (SubListTemplateData) target.getTemplateData(); + if (!BcSmartspaceCardLoggerUtil.containsValidTemplateType(templateData)) { + Log.w("SubListTemplateCard", "SubListTemplateData is null or contains invalid template type"); + return false; + } + if (templateData.getSubListIcon() != null) { + BcSmartspaceTemplateDataUtils.setIcon(mListIconView, templateData.getSubListIcon()); + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, View.VISIBLE); + } else { + BcSmartspaceTemplateDataUtils.updateVisibility(mListIconView, View.GONE); + } + if (templateData.getSubListTexts() != null) { + List subListTexts = templateData.getSubListTexts(); + if (subListTexts.isEmpty()) { + return false; + } + for (int i = 0; i < 3; i++) { + TextView textView = mListItems[i]; + if (textView == null) { + Log.w( + "SubListTemplateCard", + String.format( + Locale.US, "Missing list item view to update at row: %d", i + 1)); + break; + } + if (i < subListTexts.size()) { + BcSmartspaceTemplateDataUtils.setText(textView, subListTexts.get(i)); + BcSmartspaceTemplateDataUtils.updateVisibility(textView, View.VISIBLE); + } else { + textView.setText(""); + BcSmartspaceTemplateDataUtils.updateVisibility(textView, View.GONE); + } + } + } + if (templateData.getSubListAction() != null) { + BcSmartSpaceUtil.setOnClickListener(this, target, templateData.getSubListAction(), eventNotifier, "SubListTemplateCard", loggingInfo, 0); + } + return true; + } + + @Override + public final void setTextColor(int color) { + for (int i = 0; i < 3; i++) { + TextView textView = mListItems[i]; + if (textView == null) { + Log.w( + "SubListTemplateCard", + String.format( + Locale.US, "Missing list item view to update at row: %d", i + 1)); + return; + } + textView.setTextColor(color); + } + } + + public SubListTemplateCard(Context context, AttributeSet attrs) { + super(context, attrs); + mListItems = new TextView[3]; + } +} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/utils/ContentDescriptionUtil.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/utils/ContentDescriptionUtil.java new file mode 100644 index 000000000000..a80903708b61 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/utils/ContentDescriptionUtil.java @@ -0,0 +1,16 @@ +package com.google.android.systemui.smartspace.utils; + +import android.util.Log; +import android.view.View; + +import com.android.systemui.res.R; + +import java.util.Arrays; + +public abstract class ContentDescriptionUtil { + public static final void setFormattedContentDescription(String str, View view, CharSequence charSequence, CharSequence charSequence2) { + CharSequence string = (charSequence == null || charSequence.length() == 0) ? charSequence2 : (charSequence2 == null || charSequence2.length() == 0) ? charSequence : view.getContext().getString(R.string.generic_smartspace_concatenated_desc, charSequence2, charSequence); + Log.i(str, String.format("setFormattedContentDescription: text=%s, iconDescription=%s, contentDescription=%s", Arrays.copyOf(new Object[]{charSequence, charSequence2, string}, 3))); + view.setContentDescription(string); + } +} From da18a257a201d6196535bf3cecb0fc3dd4978150 Mon Sep 17 00:00:00 2001 From: aswin7469 Date: Sat, 3 Jan 2026 04:21:34 +0530 Subject: [PATCH 0278/1315] SystemUI: smartspace: fix duplicate date view on lockscreen * make sure to disable the default view if smartspace impl provides decoupled date and weather Signed-off-by: aswin7469 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../lockscreen/LockscreenSmartspaceController.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 8bd4c993731a..f97777e4fd79 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -127,6 +127,15 @@ constructor( private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null) private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null) + private val smartspaceConfigPlugin = + object : BcSmartspaceConfigPlugin { + override val isDefaultDateWeatherDisabled: Boolean + get() = isDateWeatherDecoupled || (configPlugin?.isDefaultDateWeatherDisabled ?: false) + override val isViewPager2Enabled: Boolean + get() = configPlugin?.isViewPager2Enabled ?: false + override val isSwipeEventLoggingEnabled: Boolean + get() = configPlugin?.isSwipeEventLoggingEnabled ?: false + } // This stores recently received Smartspace pushes to be included in dumpsys. private val recentSmartspaceData: Deque> = LinkedList() @@ -386,14 +395,14 @@ constructor( throw RuntimeException("Cannot build view when not enabled") } - configPlugin?.let { plugin?.registerConfigProvider(it) } + plugin?.registerConfigProvider(smartspaceConfigPlugin) val view = buildView( surfaceName = SmartspaceViewModel.SURFACE_GENERAL_VIEW, context = context, plugin = plugin, - configPlugin = configPlugin, + configPlugin = smartspaceConfigPlugin, isLargeClock = false, ) connectSession() From cc3d97ea1452a02b527b9896decaa8476531a2d4 Mon Sep 17 00:00:00 2001 From: John Galt Date: Mon, 22 Apr 2024 16:00:19 -0400 Subject: [PATCH 0279/1315] SystemUI: re enable KeyguardSliceProvider This does not need to be disabled for smartspace, and breaks non smartspace slices Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c99eff119631..59c7a098ecbd 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -1111,8 +1111,7 @@ + android:exported="true"> Date: Wed, 7 Jan 2026 23:39:45 +0800 Subject: [PATCH 0280/1315] SystemUI: smartspace: fix media view mistake Change-Id: I2e948393b3cf2c59a7221355b43c129a329bee08 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../smartspace/KeyguardMediaViewController.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt index 8a22ecd2f8c9..1dd4ce927c0f 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/KeyguardMediaViewController.kt @@ -57,9 +57,12 @@ constructor( val newTitle = metadata?.let { - it.getText(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) - ?: it.getText(MediaMetadata.METADATA_KEY_TITLE) - ?: context.resources.getString(R.string.music_controls_no_title) + val displayTitle = it.getText(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (TextUtils.isEmpty(displayTitle)) { + it.getText(MediaMetadata.METADATA_KEY_TITLE) + } else { + displayTitle + } ?: context.resources.getString(R.string.music_controls_no_title) } val newArtist = metadata?.getText(MediaMetadata.METADATA_KEY_ARTIST) @@ -78,7 +81,7 @@ constructor( mediaComponent, UserHandle.of(userTracker.userId), ) - .setFeatureType(SmartspaceTarget.FEATURE_MEDIA) + .setFeatureType(41) .setHeaderAction( SmartspaceAction.Builder("deviceMediaTitle", newTitle.toString()) .setSubtitle(artist) From 56235dea8731612b507217447692767dbae7d7c0 Mon Sep 17 00:00:00 2001 From: ralph950412 Date: Thu, 8 Jan 2026 11:09:02 -0500 Subject: [PATCH 0281/1315] SystemUI: smartspace: refactor dagger Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/dagger/SystemUIModule.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index e2e34009f56e..46d706dc7c2d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -135,6 +135,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.shade.transition.LargeScreenShadeInterpolatorImpl; import com.android.systemui.shared.condition.Monitor; +import com.android.systemui.smartspace.config.BcSmartspaceConfigProvider; import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.CommandQueue; @@ -420,7 +421,7 @@ static BackupManager provideBackupManager(@Application Context context) { @BindsOptionalOf @Named(SmartspaceModule.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) - abstract BcSmartspaceDataPlugin optionalGlanceableHubSmartspaceDataPlugin(); + abstract BcSmartspaceDataPlugin optionalGlanceableHubBcSmartspaceDataPlugin(); @BindsOptionalOf @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN) @@ -575,8 +576,7 @@ static KeyguardZenAlarmViewController provideKeyguardZenAlarmViewController( static KeyguardMediaViewController provideKeyguardMediaViewController( Context context, NotificationMediaManager mediaManager, - @Named(SmartspaceModule.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) - BcSmartspaceDataPlugin plugin, + BcSmartspaceDataPlugin plugin, UserTracker userTracker, @Main DelayableExecutor uiExecutor) { KeyguardMediaViewController controller = new KeyguardMediaViewController( @@ -585,6 +585,14 @@ static KeyguardMediaViewController provideKeyguardMediaViewController( return controller; } + + @Provides + @SysUISingleton + static BcSmartspaceConfigProvider provideBcSmartspaceConfigPlugin( + FeatureFlags featureFlags) { + return new BcSmartspaceConfigProvider(featureFlags); + } + @Provides @SysUISingleton static BcSmartspaceDataPlugin provideBcSmartspaceDataPlugin() { @@ -611,16 +619,4 @@ static BcSmartspaceDataPlugin provideGlanceableHubSmartspaceDataPlugin() { static BcSmartspaceDataPlugin provideWeatherSmartspaceDataPlugin() { return new WeatherSmartspaceDataProvider(); } - - @Provides - @SysUISingleton - static DateSmartspaceDataProvider provideDateSmartspaceDataProvider() { - return new DateSmartspaceDataProvider(); - } - - @Provides - @SysUISingleton - static WeatherSmartspaceDataProvider provideWeatherSmartspaceDataProvider() { - return new WeatherSmartspaceDataProvider(); - } } From a749a089f300b84758818ba1051ea1c0abc9ffb9 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 26 Jan 2023 12:32:39 +0530 Subject: [PATCH 0282/1315] SystemUI: smartspace: Open google weather on tapping smartspace Since smartspace weather activity won't open due to caller package (Launcher3) not being google signed, hijack the intent to open the google app's exported weather activity, which behaves in the same fashion. Change-Id: Ic01ede73ab6253bcb301ac794985c3720c5beda3 [cyberknight777: Adapt for BP4A smartspace] Signed-off-by: Cyber Knight Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/smartspace/BcSmartSpaceUtil.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java index f47d7c21c4a4..7c41312fdbc2 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartSpaceUtil.java @@ -6,6 +6,7 @@ import android.app.smartspace.SmartspaceTargetEvent; import android.app.smartspace.uitemplatedata.TapAction; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.content.Intent; @@ -30,6 +31,8 @@ import java.util.Map; public abstract class BcSmartSpaceUtil { + private static final String GSA_PACKAGE = "com.google.android.googlequicksearchbox"; + private static final String GSA_WEATHER_ACTIVITY = "com.google.android.apps.search.weather.WeatherExportedActivity"; public static final Map FEATURE_TYPE_TO_SECONDARY_CARD_RESOURCE_MAP; public static FalsingManager sFalsingManager; @@ -64,6 +67,21 @@ public final boolean onInteraction(View view, PendingIntent pendingIntent, Remot } } + // Workaround for Google weather + private static boolean hijackIntent(SmartspaceTarget target, BcSmartspaceDataPlugin.IntentStarter intentStarter, View view) { + if (view instanceof IcuDateTextView) { + // Ensure we don't change date view + return false; + } + if (target != null && target.getFeatureType() == target.FEATURE_WEATHER) { + Intent intent = new Intent().setComponent(new ComponentName(GSA_PACKAGE, GSA_WEATHER_ACTIVITY)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intentStarter.startIntent(view, intent, true); + return true; + } + return false; + } + public static class DefaultIntentStarter implements BcSmartspaceDataPlugin.IntentStarter { public final String tag; @@ -180,7 +198,7 @@ public static void setOnClickListener(View view, SmartspaceTarget target, Smarts BcSmartspaceCardLogger.log(BcSmartspaceEvent.SMARTSPACE_CARD_CLICK, loggingInfo); } - if (!isNoIntent) { + if (!isNoIntent && !hijackIntent(target, intentStarter, v)) { intentStarter.startFromAction(action, v, showOnLockscreen); } @@ -216,7 +234,7 @@ public static void setOnClickListener(View view, SmartspaceTarget target, TapAct } BcSmartspaceDataPlugin.IntentStarter intentStarter = getIntentStarter(eventNotifier, tag); - if (tapAction.getIntent() != null || tapAction.getPendingIntent() != null) { + if ((tapAction.getIntent() != null || tapAction.getPendingIntent() != null) && !hijackIntent(target, intentStarter, v)) { intentStarter.startFromAction(tapAction, v, showOnLockscreen); } From 01d3cb184332c1bccffb0a7364d7387904579698 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 18 Oct 2025 01:35:16 +0530 Subject: [PATCH 0283/1315] base: Allow screen off UDFPS when configured As per config.xml - config_screen_off_udfps_enabled: Whether to enable fp unlock when screen turns off on udfps devices - config_screen_off_udfps_default_on: Default value for fp screen off unlock toggle Use both these configs properly in AmbientDisplayConfiguration as well as UdfpsController. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../hardware/display/AmbientDisplayConfiguration.java | 7 +++---- .../com/android/systemui/biometrics/UdfpsController.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index d341805b1588..1634f9e4d61b 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -167,11 +167,10 @@ && pickupGestureEnabled(user) /** @hide */ public boolean screenOffUdfpsEnabled(int user) { - return !TextUtils.isEmpty(udfpsLongPressSensorType()) - && ((mScreenOffUdfpsAvailable && Flags.screenOffUnlockUdfps()) - && mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_default_on) + if (!mScreenOffUdfpsAvailable) return false; + return mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_default_on) ? boolSettingDefaultOn(SCREEN_OFF_UNLOCK_UDFPS_ENABLED, user) - : boolSettingDefaultOff(SCREEN_OFF_UNLOCK_UDFPS_ENABLED, user)); + : boolSettingDefaultOff(SCREEN_OFF_UNLOCK_UDFPS_ENABLED, user); } /** @hide */ diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 5f7949984426..8dac750462c6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -1082,7 +1082,7 @@ private boolean isScreenOffUnlockEnabled() { && Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.SCREEN_OFF_UNLOCK_UDFPS_ENABLED, - 0, + mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_default_on) ? 1 : 0, mContext.getUserId()) != 0; } From 06fc6bf0789799701bea692ae68b2a668c1ad57d Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 16 May 2023 14:32:50 +0530 Subject: [PATCH 0284/1315] Allow overlaying font spacing for lockscreen clock Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/values/matrixx_arrays.xml | 3 +++ core/res/res/values/matrixx_symbols.xml | 3 +++ .../SystemUI/customization/clocks/common/res/values/dimens.xml | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/res/res/values/matrixx_arrays.xml b/core/res/res/values/matrixx_arrays.xml index a2a2f90915aa..19983f138b83 100644 --- a/core/res/res/values/matrixx_arrays.xml +++ b/core/res/res/values/matrixx_arrays.xml @@ -4,5 +4,8 @@ SPDX-License-Identifier: Apache-2.0 --> + + + .7 diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 9ce0cc95c435..3c1835d5171f 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -54,4 +54,7 @@ + + + diff --git a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml index ea954cc16a42..4e5ed62e10c7 100644 --- a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml +++ b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml @@ -22,7 +22,7 @@ 86dp - .7 + @*android:dimen/keyguard_clock_line_spacing_scale 1 From bb050f926c99a352d8ed714e75da0cc0ce9763b6 Mon Sep 17 00:00:00 2001 From: Nauval Rizky Date: Mon, 1 May 2023 21:19:39 +0700 Subject: [PATCH 0285/1315] QRCodeScannerController: Use Lens as fallback activity When .mlkit.barcode.ui.PlatformBarcodeScanningActivityProxy cannot be found (such as on non-Tensor devices), the tile will never works. Add a fallback mechanism to use Lens as it can also scan QRCode as well. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../controller/QRCodeScannerController.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index bab88c0b0bf9..edd87f019aa2 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -24,6 +24,9 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; import android.provider.DeviceConfig; import android.provider.Settings; import android.util.Log; @@ -86,6 +89,9 @@ default void onQRCodeScannerPreferenceChanged() { private static final String TAG = "QRCodeScannerController"; + public static final String GSA_PACKAGE = "com.google.android.googlequicksearchbox"; + public static final String LENS_ACTIVITY = "com.google.android.apps.lens.MainActivity"; + private final Context mContext; private final Executor mExecutor; private final SecureSettings mSecureSettings; @@ -266,6 +272,21 @@ private String getDefaultScannerActivity() { com.android.internal.R.string.config_defaultQrCodeComponent); } + private Intent getLensIntent() { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + + bundle.putString("caller_package", GSA_PACKAGE); + bundle.putLong("start_activity_time_nanos", SystemClock.elapsedRealtimeNanos()); + intent.setComponent(new ComponentName(GSA_PACKAGE, LENS_ACTIVITY)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setPackage(GSA_PACKAGE) + .setData(Uri.parse("google://lens")) + .putExtra("lens_activity_params", bundle); + + return intent; + } + private void updateQRCodeScannerActivityDetails() { String qrCodeScannerActivity = mDeviceConfigProxy.getString( DeviceConfig.NAMESPACE_SYSTEMUI, @@ -286,10 +307,15 @@ private void updateQRCodeScannerActivityDetails() { intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); } + Intent lensIntent = getLensIntent(); if (isActivityAvailable(intent)) { mQRCodeScannerActivity = qrCodeScannerActivity; mComponentName = componentName; mIntent = intent; + } else if (isActivityCallable(lensIntent)) { + mQRCodeScannerActivity = LENS_ACTIVITY; + mComponentName = lensIntent.getComponent(); + mIntent = lensIntent; } else { mQRCodeScannerActivity = null; mComponentName = null; From c0019ac92c6d5c7f2fe61bc6d142b135b24795f1 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 5 May 2023 17:58:39 +0530 Subject: [PATCH 0286/1315] QRCodeScannerController: Check for google package availability Change-Id: I6202dbbac714e3a947388e1feccc6f753bff0f41 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../controller/QRCodeScannerController.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index edd87f019aa2..a65e7a295620 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -32,6 +32,7 @@ import android.util.Log; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.internal.util.matrixx.Utils; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.UserTracker; @@ -279,7 +280,7 @@ private Intent getLensIntent() { bundle.putString("caller_package", GSA_PACKAGE); bundle.putLong("start_activity_time_nanos", SystemClock.elapsedRealtimeNanos()); intent.setComponent(new ComponentName(GSA_PACKAGE, LENS_ACTIVITY)) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) .setPackage(GSA_PACKAGE) .setData(Uri.parse("google://lens")) .putExtra("lens_activity_params", bundle); @@ -301,18 +302,19 @@ private void updateQRCodeScannerActivityDetails() { String prevQrCodeScannerActivity = mQRCodeScannerActivity; ComponentName componentName = null; Intent intent = new Intent(); - if (qrCodeScannerActivity != null) { + if (qrCodeScannerActivity != null && !qrCodeScannerActivity.isEmpty()) { componentName = ComponentName.unflattenFromString(qrCodeScannerActivity); intent.setComponent(componentName); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); } Intent lensIntent = getLensIntent(); - if (isActivityAvailable(intent)) { + if (intent != null && isActivityAvailable(intent)) { mQRCodeScannerActivity = qrCodeScannerActivity; mComponentName = componentName; mIntent = intent; - } else if (isActivityCallable(lensIntent)) { + } else if (Utils. isPackageInstalled(mContext, GSA_PACKAGE, false) && + lensIntent != null && isActivityCallable(lensIntent)) { mQRCodeScannerActivity = LENS_ACTIVITY; mComponentName = lensIntent.getComponent(); mIntent = lensIntent; From bcfd98517ce30ab458baca129d50995e19bdc958 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Sat, 16 Mar 2024 15:52:34 +0800 Subject: [PATCH 0287/1315] ThemeOverlayApplier: Exclude Launcher3 and Themepicker overlays * fixes monet breakage when using launchers other than Launcher3. test: apply gradicon icon pack-> change monet colors-> confirmed that theme seed color changes were applied. Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/theme/ThemeOverlayApplier.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index b8da99faf0f8..caae2c4ce99a 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -120,15 +120,13 @@ public class ThemeOverlayApplier implements Dumpable { */ static final List THEME_CATEGORIES = Lists.newArrayList( OVERLAY_CATEGORY_SYSTEM_PALETTE, - OVERLAY_CATEGORY_ICON_LAUNCHER, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_ACCENT_COLOR, OVERLAY_CATEGORY_DYNAMIC_COLOR, OVERLAY_CATEGORY_ICON_ANDROID, OVERLAY_CATEGORY_ICON_SYSUI, - OVERLAY_CATEGORY_ICON_SETTINGS, - OVERLAY_CATEGORY_ICON_THEME_PICKER); + OVERLAY_CATEGORY_ICON_SETTINGS); /* Categories that need to be applied to the current user as well as the system user. */ @VisibleForTesting @@ -172,10 +170,6 @@ public ThemeOverlayApplier(OverlayManager overlayManager, Sets.newHashSet(OVERLAY_CATEGORY_ICON_SYSUI)); mTargetPackageToCategories.put(SETTINGS_PACKAGE, Sets.newHashSet(OVERLAY_CATEGORY_ICON_SETTINGS)); - mTargetPackageToCategories.put(mLauncherPackage, - Sets.newHashSet(OVERLAY_CATEGORY_ICON_LAUNCHER)); - mTargetPackageToCategories.put(mThemePickerPackage, - Sets.newHashSet(OVERLAY_CATEGORY_ICON_THEME_PICKER)); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ACCENT_COLOR, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_DYNAMIC_COLOR, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_FONT, ANDROID_PACKAGE); @@ -183,8 +177,6 @@ public ThemeOverlayApplier(OverlayManager overlayManager, mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_ANDROID, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SYSUI, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SETTINGS, SETTINGS_PACKAGE); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_LAUNCHER, mLauncherPackage); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_THEME_PICKER, mThemePickerPackage); dumpManager.registerDumpable(TAG, this); } From 1070d82daea124d275757338898b82ff2a57b5ee Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 21 Oct 2025 11:39:29 +0000 Subject: [PATCH 0288/1315] SystemUI: Add custom udfps fp icon support Change-Id: Ia5044a111d3ac112f2904108ca7dd4d797bb86db Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 10 + .../biometrics/UdfpsIconDrawable.java | 323 +++++++++++++++++- 2 files changed, 318 insertions(+), 15 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index dc05ce32aa0e..ec1e2a59e11a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7121,6 +7121,16 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String UDFPS_ICON = "udfps_icon"; + /** + * @hide + */ + public static final String UDFPS_ICON_TYPE = "udfps_icon_type"; + + /** + * @hide + */ + public static final String UDFPS_CUSTOM_FP_ICON_PATH = "udfps_custom_fp_icon_path"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java index 6fed12cce00b..987ddd40860b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2024-2025 crDroid Android Project + * Copyright (C) 2024-2025 Lunaris AOSP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +20,16 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.ColorFilter; import android.graphics.Rect; +import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -34,30 +42,39 @@ import com.android.systemui.res.R; import com.android.systemui.tuner.TunerService; +import java.io.File; +import java.io.IOException; + /** * Abstract base class for drawable displayed when the finger is not touching the * sensor area. */ public abstract class UdfpsIconDrawable extends Drawable { + private static final String TAG = "UdfpsIconDrawable"; + private static final boolean DEBUG = false; + private static final String UDFPS_ICON = "system:" + Settings.System.UDFPS_ICON; + private static final String UDFPS_ICON_TYPE = "system:" + Settings.System.UDFPS_ICON_TYPE; + private static final String UDFPS_CUSTOM_ICON_PATH = "system:" + Settings.System.UDFPS_CUSTOM_FP_ICON_PATH; + + private static final int ICON_TYPE_PREBUILT = 0; + private static final int ICON_TYPE_CUSTOM = 1; + private static final int MAX_IMAGE_SIZE = 2 * 1024 * 1024; + private static final int MAX_DIMENSION = 400; + private final String udfpsResourcesPackage = "com.matrixx.udfps.icons"; + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); @NonNull private final Context mContext; private Drawable mUdfpsDrawable; private Resources udfpsRes; private String[] mUdfpsIcons; + private String mLastLoadedCustomPath = null; + private int mCurrentIconType = ICON_TYPE_PREBUILT; + private boolean mIsVisible = true; - private final TunerService.Tunable mTunable = (key, newValue) -> { - if (UDFPS_ICON.equals(key)) { - int selectedIcon = newValue == null ? 0 : Integer.parseInt(newValue); - if (mUdfpsDrawable != null) { - mUdfpsDrawable.setCallback(null); // Clear previous drawable callback to avoid leaks - mUdfpsDrawable = null; - } - mUdfpsDrawable = selectedIcon == 0 ? null : loadDrawable(udfpsRes, mUdfpsIcons[selectedIcon]); - } - }; + private TunerService.Tunable mTunable; public UdfpsIconDrawable(@NonNull Context context) { mContext = context; @@ -71,15 +88,292 @@ private void init() { udfpsRes = pm.getResourcesForApplication(udfpsResourcesPackage); int res = udfpsRes.getIdentifier("udfps_icons", "array", udfpsResourcesPackage); mUdfpsIcons = udfpsRes.getStringArray(res); - Dependency.get(TunerService.class).addTunable(mTunable, UDFPS_ICON); } catch (PackageManager.NameNotFoundException e) { - Log.e("UdfpsIconDrawable", "UDFPS package not found", e); + Log.e(TAG, "UDFPS package not found", e); + } + } + + mCurrentIconType = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.UDFPS_ICON_TYPE, + ICON_TYPE_PREBUILT, + UserHandle.USER_CURRENT + ); + + mTunable = (key, newValue) -> { + if (UDFPS_ICON_TYPE.equals(key)) { + int iconType = newValue == null ? ICON_TYPE_PREBUILT : Integer.parseInt(newValue); + mCurrentIconType = iconType; + + if (mCurrentIconType == ICON_TYPE_PREBUILT) { + runOnMainThread(() -> Settings.System.putStringForUser( + mContext.getContentResolver(), + Settings.System.UDFPS_CUSTOM_FP_ICON_PATH, + null, + UserHandle.USER_CURRENT + )); + mLastLoadedCustomPath = null; + } + + updateIcon(); + } else if (UDFPS_ICON.equals(key)) { + if (mCurrentIconType == ICON_TYPE_PREBUILT) { + updateIcon(); + } + } else if (UDFPS_CUSTOM_ICON_PATH.equals(key)) { + String newPath = newValue; + if (mCurrentIconType == ICON_TYPE_CUSTOM && + (newPath == null || !newPath.equals(mLastLoadedCustomPath))) { + mLastLoadedCustomPath = null; + updateIcon(); + } + } + }; + + Dependency.get(TunerService.class).addTunable( + mTunable, + UDFPS_ICON_TYPE, + UDFPS_ICON, + UDFPS_CUSTOM_ICON_PATH + ); + } + + private void runOnMainThread(Runnable runnable) { + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { + runnable.run(); + } else { + mMainHandler.post(runnable); + } + } + + private void updateIcon() { + cleanupCurrentDrawable(); + + if (mCurrentIconType == ICON_TYPE_CUSTOM) { + loadCustomIcon(); + } else { + loadPrebuiltIcon(); + } + + runOnMainThread(this::invalidateSelf); + } + + private void loadPrebuiltIcon() { + int selectedIcon = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.UDFPS_ICON, 0, + UserHandle.USER_CURRENT); + + if (selectedIcon == 0 || udfpsRes == null || mUdfpsIcons == null) { + mUdfpsDrawable = null; + return; + } + + mUdfpsDrawable = loadDrawable(udfpsRes, mUdfpsIcons[selectedIcon]); + } + + public void onVisibilityChanged(boolean visible) { + mIsVisible = visible; + if (mUdfpsDrawable instanceof AnimatedImageDrawable) { + AnimatedImageDrawable anim = (AnimatedImageDrawable) mUdfpsDrawable; + if (visible && !anim.isRunning()) { + anim.start(); + if (DEBUG) Log.d(TAG, "Animation started - visibility changed"); + } else if (!visible && anim.isRunning()) { + anim.stop(); + if (DEBUG) Log.d(TAG, "Animation stopped - visibility changed"); + } + } + } + + private void loadCustomIcon() { + String path = Settings.System.getStringForUser(mContext.getContentResolver(), + Settings.System.UDFPS_CUSTOM_FP_ICON_PATH, + UserHandle.USER_CURRENT); + + if (path == null || path.isEmpty()) { + if (DEBUG) Log.d(TAG, "No custom icon path set"); + mUdfpsDrawable = null; + return; + } + + if (path.equals(mLastLoadedCustomPath)) { + if (DEBUG) Log.d(TAG, "Custom icon already loaded for path: " + path); + return; + } + + mLastLoadedCustomPath = path; + + File imageFile = new File(path); + if (!isValidImageFile(imageFile)) { + Log.w(TAG, "Custom icon file validation failed: " + path); + mUdfpsDrawable = null; + mLastLoadedCustomPath = null; + return; + } + + String extension = getFileExtension(path); + boolean isAnimated = extension.matches("\\.(gif|webp)$"); + + if (isAnimated) { + if (loadAnimatedCustomIcon(imageFile, path)) { + return; + } + if (DEBUG) Log.d(TAG, "Falling back to static image loading for: " + path); + } + + loadStaticCustomIcon(path); + } + + private boolean isValidImageFile(File imageFile) { + if (!imageFile.exists()) { + Log.w(TAG, "Custom icon file does not exist: " + imageFile.getAbsolutePath()); + return false; + } + + if (!imageFile.canRead()) { + Log.w(TAG, "Custom icon file is not readable: " + imageFile.getAbsolutePath()); + return false; + } + + if (imageFile.length() > MAX_IMAGE_SIZE) { + Log.w(TAG, "Custom icon file too large: " + imageFile.length() + " bytes"); + return false; + } + + String name = imageFile.getName().toLowerCase(); + return name.endsWith(".png") || name.endsWith(".jpg") || + name.endsWith(".jpeg") || name.endsWith(".gif") || + name.endsWith(".webp"); + } + + private String getFileExtension(String path) { + if (path == null) return ""; + String lower = path.toLowerCase(); + if (lower.endsWith(".gif")) return ".gif"; + if (lower.endsWith(".webp")) return ".webp"; + if (lower.endsWith(".png")) return ".png"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return ".jpg"; + return ""; + } + + private boolean loadAnimatedCustomIcon(File imageFile, String path) { + try { + android.graphics.ImageDecoder.Source source = + android.graphics.ImageDecoder.createSource(imageFile); + Drawable drawable = android.graphics.ImageDecoder.decodeDrawable(source); + + if (drawable == null) { + Log.w(TAG, "ImageDecoder returned null drawable for: " + path); + return false; + } + + mUdfpsDrawable = drawable; + + if (drawable instanceof AnimatedImageDrawable) { + AnimatedImageDrawable animDrawable = (AnimatedImageDrawable) drawable; + animDrawable.setRepeatCount(AnimatedImageDrawable.REPEAT_INFINITE); + + if (mIsVisible && !animDrawable.isRunning()) { + animDrawable.start(); + if (DEBUG) Log.d(TAG, "Animation started for custom icon: " + path); + } + } + + if (DEBUG) Log.d(TAG, "Animated custom icon loaded successfully: " + path); + return true; + + } catch (IOException e) { + Log.e(TAG, "IOException loading animated custom icon: " + e.getMessage()); + return false; + } catch (Exception e) { + Log.e(TAG, "Error loading animated custom icon: " + e.getMessage()); + return false; + } + } + + private int calculateInSampleSize(BitmapFactory.Options options, int maxSize) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > maxSize || width > maxSize) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) >= maxSize && (halfWidth / inSampleSize) >= maxSize) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + private void loadStaticCustomIcon(String path) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, options); + + if (options.outWidth <= 0 || options.outHeight <= 0) { + Log.w(TAG, "Invalid custom icon dimensions"); + mUdfpsDrawable = null; + return; + } + + options.inSampleSize = calculateInSampleSize(options, MAX_DIMENSION); + options.inJustDecodeBounds = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + + Bitmap bitmap = BitmapFactory.decodeFile(path, options); + if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { + Log.w(TAG, "Failed to decode custom icon bitmap"); + mUdfpsDrawable = null; + return; + } + + mUdfpsDrawable = new BitmapDrawable(mContext.getResources(), bitmap); + if (DEBUG) Log.d(TAG, "Static custom icon loaded successfully: " + path); + + } catch (OutOfMemoryError e) { + Log.e(TAG, "OutOfMemoryError loading static custom icon: " + e.getMessage()); + mUdfpsDrawable = null; + } catch (Exception e) { + Log.e(TAG, "Failed to load static custom icon: " + e.getMessage()); + mUdfpsDrawable = null; + } + } + + private void cleanupCurrentDrawable() { + if (mUdfpsDrawable != null) { + if (mUdfpsDrawable instanceof AnimatedImageDrawable) { + try { + AnimatedImageDrawable animDrawable = (AnimatedImageDrawable) mUdfpsDrawable; + if (animDrawable.isRunning()) { + animDrawable.stop(); + } + } catch (Exception e) { + if (DEBUG) Log.e(TAG, "Error stopping animation during cleanup", e); + } } + + if (mUdfpsDrawable instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) mUdfpsDrawable).getBitmap(); + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + + mUdfpsDrawable.setCallback(null); + mUdfpsDrawable = null; } } public void destroy() { - Dependency.get(TunerService.class).removeTunable(mTunable); + try { + Dependency.get(TunerService.class).removeTunable(mTunable); + } catch (Exception e) { + Log.e(TAG, "Error removing tunable: " + e.getMessage()); + } + cleanupCurrentDrawable(); } @Override @@ -105,7 +399,7 @@ private Drawable loadDrawable(Resources res, String resName) { @Override public void setBounds(int left, int top, int right, int bottom) { if (mUdfpsDrawable != null) { - mUdfpsDrawable.setBounds(left , top, right, bottom); + mUdfpsDrawable.setBounds(left, top, right, bottom); } invalidateSelf(); } @@ -128,7 +422,6 @@ protected void onBoundsChange(Rect bounds) { @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { - // Do not set color filter } @Override From b5de50a97f43eb695a25578b5acdce516b873d76 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 25 Oct 2025 13:31:00 +0000 Subject: [PATCH 0289/1315] SystemUI: Fix lockscreen crash due to recycled bitmap Change-Id: Id435f6a265abb0af036c3c13f31a9f784d0ae64b Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../biometrics/UdfpsFpIconDrawable.kt | 31 +++- .../biometrics/UdfpsIconDrawable.java | 160 +++++++++++++----- 2 files changed, 142 insertions(+), 49 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpIconDrawable.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpIconDrawable.kt index 38217da37cb6..b5c0a9c05edf 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpIconDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpIconDrawable.kt @@ -18,20 +18,39 @@ package com.android.systemui.biometrics import android.content.Context import android.graphics.Canvas import android.graphics.PixelFormat +import android.graphics.drawable.BitmapDrawable +import android.util.Log /** * Draws udfps fingerprint if sensor isn't illuminating. */ class UdfpsFpIconDrawable(context: Context) : UdfpsIconDrawable(context) { + + companion object { + private const val TAG = "UdfpsFpIconDrawable" + } + override fun draw(canvas: Canvas) { - val udfpsDrawable = getUdfpsDrawable() - udfpsDrawable?.apply { - setBounds(bounds) - draw(canvas) + try { + val udfpsDrawable = getUdfpsDrawable() + udfpsDrawable?.apply { + if (this is BitmapDrawable) { + val bitmap = this.bitmap + if (bitmap == null || bitmap.isRecycled) { + Log.w(TAG, "Skipping draw - bitmap is null or recycled") + return + } + } + + setBounds(bounds) + draw(canvas) + } + } catch (e: Exception) { + Log.e(TAG, "Error drawing UDFPS icon: ${e.message}", e) } } - + override fun getOpacity(): Int { return PixelFormat.TRANSLUCENT } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java index 987ddd40860b..e64154bed6ff 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java @@ -68,11 +68,13 @@ public abstract class UdfpsIconDrawable extends Drawable { @NonNull private final Context mContext; private Drawable mUdfpsDrawable; + private Drawable mPendingCleanupDrawable; private Resources udfpsRes; private String[] mUdfpsIcons; private String mLastLoadedCustomPath = null; private int mCurrentIconType = ICON_TYPE_PREBUILT; private boolean mIsVisible = true; + private boolean mIsDestroyed = false; private TunerService.Tunable mTunable; @@ -101,17 +103,23 @@ private void init() { ); mTunable = (key, newValue) -> { + if (mIsDestroyed) return; + if (UDFPS_ICON_TYPE.equals(key)) { int iconType = newValue == null ? ICON_TYPE_PREBUILT : Integer.parseInt(newValue); mCurrentIconType = iconType; if (mCurrentIconType == ICON_TYPE_PREBUILT) { - runOnMainThread(() -> Settings.System.putStringForUser( - mContext.getContentResolver(), - Settings.System.UDFPS_CUSTOM_FP_ICON_PATH, - null, - UserHandle.USER_CURRENT - )); + runOnMainThread(() -> { + if (!mIsDestroyed) { + Settings.System.putStringForUser( + mContext.getContentResolver(), + Settings.System.UDFPS_CUSTOM_FP_ICON_PATH, + null, + UserHandle.USER_CURRENT + ); + } + }); mLastLoadedCustomPath = null; } @@ -139,6 +147,8 @@ private void init() { } private void runOnMainThread(Runnable runnable) { + if (mIsDestroyed) return; + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { runnable.run(); } else { @@ -147,7 +157,9 @@ private void runOnMainThread(Runnable runnable) { } private void updateIcon() { - cleanupCurrentDrawable(); + if (mIsDestroyed) return; + + Drawable oldDrawable = mUdfpsDrawable; if (mCurrentIconType == ICON_TYPE_CUSTOM) { loadCustomIcon(); @@ -155,7 +167,14 @@ private void updateIcon() { loadPrebuiltIcon(); } - runOnMainThread(this::invalidateSelf); + runOnMainThread(() -> { + if (!mIsDestroyed) { + invalidateSelf(); + if (oldDrawable != null) { + mMainHandler.postDelayed(() -> cleanupDrawable(oldDrawable), 100); + } + } + }); } private void loadPrebuiltIcon() { @@ -172,15 +191,21 @@ private void loadPrebuiltIcon() { } public void onVisibilityChanged(boolean visible) { + if (mIsDestroyed) return; + mIsVisible = visible; if (mUdfpsDrawable instanceof AnimatedImageDrawable) { AnimatedImageDrawable anim = (AnimatedImageDrawable) mUdfpsDrawable; - if (visible && !anim.isRunning()) { - anim.start(); - if (DEBUG) Log.d(TAG, "Animation started - visibility changed"); - } else if (!visible && anim.isRunning()) { - anim.stop(); - if (DEBUG) Log.d(TAG, "Animation stopped - visibility changed"); + try { + if (visible && !anim.isRunning()) { + anim.start(); + if (DEBUG) Log.d(TAG, "Animation started - visibility changed"); + } else if (!visible && anim.isRunning()) { + anim.stop(); + if (DEBUG) Log.d(TAG, "Animation stopped - visibility changed"); + } + } catch (Exception e) { + Log.e(TAG, "Error handling animation visibility", e); } } } @@ -196,7 +221,7 @@ private void loadCustomIcon() { return; } - if (path.equals(mLastLoadedCustomPath)) { + if (path.equals(mLastLoadedCustomPath) && mUdfpsDrawable != null) { if (DEBUG) Log.d(TAG, "Custom icon already loaded for path: " + path); return; } @@ -308,6 +333,7 @@ private int calculateInSampleSize(BitmapFactory.Options options, int maxSize) { } private void loadStaticCustomIcon(String path) { + Bitmap bitmap = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; @@ -322,69 +348,118 @@ private void loadStaticCustomIcon(String path) { options.inSampleSize = calculateInSampleSize(options, MAX_DIMENSION); options.inJustDecodeBounds = false; options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.inMutable = false; - Bitmap bitmap = BitmapFactory.decodeFile(path, options); - if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { + bitmap = BitmapFactory.decodeFile(path, options); + if (bitmap == null || bitmap.isRecycled() || + bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { Log.w(TAG, "Failed to decode custom icon bitmap"); + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } mUdfpsDrawable = null; return; } - mUdfpsDrawable = new BitmapDrawable(mContext.getResources(), bitmap); + Bitmap immutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); + if (immutableBitmap == null) { + Log.w(TAG, "Failed to create immutable copy of bitmap"); + if (!bitmap.isRecycled()) { + bitmap.recycle(); + } + mUdfpsDrawable = null; + return; + } + + if (!bitmap.isRecycled()) { + bitmap.recycle(); + } + + mUdfpsDrawable = new BitmapDrawable(mContext.getResources(), immutableBitmap); if (DEBUG) Log.d(TAG, "Static custom icon loaded successfully: " + path); } catch (OutOfMemoryError e) { Log.e(TAG, "OutOfMemoryError loading static custom icon: " + e.getMessage()); + if (bitmap != null && !bitmap.isRecycled()) { + try { + bitmap.recycle(); + } catch (Exception ex) { + } + } mUdfpsDrawable = null; } catch (Exception e) { Log.e(TAG, "Failed to load static custom icon: " + e.getMessage()); + if (bitmap != null && !bitmap.isRecycled()) { + try { + bitmap.recycle(); + } catch (Exception ex) { + } + } mUdfpsDrawable = null; } } - private void cleanupCurrentDrawable() { - if (mUdfpsDrawable != null) { - if (mUdfpsDrawable instanceof AnimatedImageDrawable) { - try { - AnimatedImageDrawable animDrawable = (AnimatedImageDrawable) mUdfpsDrawable; - if (animDrawable.isRunning()) { - animDrawable.stop(); - } - } catch (Exception e) { - if (DEBUG) Log.e(TAG, "Error stopping animation during cleanup", e); + private void cleanupDrawable(Drawable drawable) { + if (drawable == null || drawable == mUdfpsDrawable) { + return; + } + + try { + if (drawable instanceof AnimatedImageDrawable) { + AnimatedImageDrawable animDrawable = (AnimatedImageDrawable) drawable; + if (animDrawable.isRunning()) { + animDrawable.stop(); } } - if (mUdfpsDrawable instanceof BitmapDrawable) { - Bitmap bitmap = ((BitmapDrawable) mUdfpsDrawable).getBitmap(); + drawable.setCallback(null); + + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + Bitmap bitmap = bitmapDrawable.getBitmap(); if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); + if (DEBUG) Log.d(TAG, "Bitmap cleanup delegated to GC"); } } - - mUdfpsDrawable.setCallback(null); - mUdfpsDrawable = null; + } catch (Exception e) { + if (DEBUG) Log.e(TAG, "Error during drawable cleanup", e); } } public void destroy() { + mIsDestroyed = true; + try { Dependency.get(TunerService.class).removeTunable(mTunable); } catch (Exception e) { Log.e(TAG, "Error removing tunable: " + e.getMessage()); } - cleanupCurrentDrawable(); + + mMainHandler.removeCallbacksAndMessages(null); + + if (mUdfpsDrawable != null) { + cleanupDrawable(mUdfpsDrawable); + mUdfpsDrawable = null; + } } @Override public void setAlpha(int alpha) { - if (mUdfpsDrawable != null) { + if (mUdfpsDrawable != null && !mIsDestroyed) { mUdfpsDrawable.setAlpha(alpha); + invalidateSelf(); } - invalidateSelf(); } Drawable getUdfpsDrawable() { + if (mUdfpsDrawable instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) mUdfpsDrawable).getBitmap(); + if (bitmap == null || bitmap.isRecycled()) { + if (DEBUG) Log.w(TAG, "Drawable has recycled bitmap, returning null"); + mUdfpsDrawable = null; + return null; + } + } return mUdfpsDrawable; } @@ -398,26 +473,25 @@ private Drawable loadDrawable(Resources res, String resName) { @Override public void setBounds(int left, int top, int right, int bottom) { - if (mUdfpsDrawable != null) { + super.setBounds(left, top, right, bottom); + if (mUdfpsDrawable != null && !mIsDestroyed) { mUdfpsDrawable.setBounds(left, top, right, bottom); } - invalidateSelf(); } @Override public void setBounds(Rect bounds) { - if (mUdfpsDrawable != null) { + super.setBounds(bounds); + if (mUdfpsDrawable != null && !mIsDestroyed) { mUdfpsDrawable.setBounds(bounds); } - invalidateSelf(); } @Override protected void onBoundsChange(Rect bounds) { - if (mUdfpsDrawable != null) { + if (mUdfpsDrawable != null && !mIsDestroyed) { mUdfpsDrawable.setBounds(bounds); } - invalidateSelf(); } @Override From f7a2c8def6c7247805783aa9a6e6b9f36b76b389 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 26 Oct 2025 04:31:17 +0000 Subject: [PATCH 0290/1315] SystemUI: Fix udfps icon load issue Change-Id: Ie6f99e85237aaa0386536f5cb0138cf2c533af6a Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../biometrics/UdfpsIconDrawable.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java index e64154bed6ff..310a7f13f68f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsIconDrawable.java @@ -68,7 +68,6 @@ public abstract class UdfpsIconDrawable extends Drawable { @NonNull private final Context mContext; private Drawable mUdfpsDrawable; - private Drawable mPendingCleanupDrawable; private Resources udfpsRes; private String[] mUdfpsIcons; private String mLastLoadedCustomPath = null; @@ -144,6 +143,8 @@ private void init() { UDFPS_ICON, UDFPS_CUSTOM_ICON_PATH ); + + updateIcon(); } private void runOnMainThread(Runnable runnable) { @@ -170,7 +171,7 @@ private void updateIcon() { runOnMainThread(() -> { if (!mIsDestroyed) { invalidateSelf(); - if (oldDrawable != null) { + if (oldDrawable != null && oldDrawable != mUdfpsDrawable) { mMainHandler.postDelayed(() -> cleanupDrawable(oldDrawable), 100); } } @@ -226,27 +227,35 @@ private void loadCustomIcon() { return; } - mLastLoadedCustomPath = path; + if (DEBUG) Log.d(TAG, "Loading custom icon from: " + path); File imageFile = new File(path); if (!isValidImageFile(imageFile)) { Log.w(TAG, "Custom icon file validation failed: " + path); mUdfpsDrawable = null; - mLastLoadedCustomPath = null; return; } + mLastLoadedCustomPath = path; + String extension = getFileExtension(path); boolean isAnimated = extension.matches("\\.(gif|webp)$"); if (isAnimated) { if (loadAnimatedCustomIcon(imageFile, path)) { + if (DEBUG) Log.d(TAG, "Successfully loaded animated custom icon"); return; } if (DEBUG) Log.d(TAG, "Falling back to static image loading for: " + path); } loadStaticCustomIcon(path); + if (mUdfpsDrawable != null) { + if (DEBUG) Log.d(TAG, "Successfully loaded static custom icon"); + } else { + Log.w(TAG, "Failed to load custom icon from: " + path); + mLastLoadedCustomPath = null; + } } private boolean isValidImageFile(File imageFile) { @@ -260,8 +269,14 @@ private boolean isValidImageFile(File imageFile) { return false; } - if (imageFile.length() > MAX_IMAGE_SIZE) { - Log.w(TAG, "Custom icon file too large: " + imageFile.length() + " bytes"); + long fileSize = imageFile.length(); + if (fileSize > MAX_IMAGE_SIZE) { + Log.w(TAG, "Custom icon file too large: " + fileSize + " bytes"); + return false; + } + + if (fileSize == 0) { + Log.w(TAG, "Custom icon file is empty"); return false; } From e91d19be6ea1b42e33dccab1acf9a654661357f1 Mon Sep 17 00:00:00 2001 From: beanstown106 Date: Thu, 14 Jan 2016 05:56:50 -0500 Subject: [PATCH 0291/1315] Fingerprint authentication vibration [1/2] Co-authored-by: Pranav Vashi Change-Id: Ie0437d2cc6b0fb23e36f032c9ad60fc7730e653f Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 12 ++++++ .../keyguard/KeyguardViewConfigurator.kt | 1 + .../ui/binder/KeyguardRootViewBinder.kt | 42 ++++++++++++------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ec1e2a59e11a..98c84a1a6938 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7131,6 +7131,18 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String UDFPS_CUSTOM_FP_ICON_PATH = "udfps_custom_fp_icon_path"; + /** + * Whether to vibrate on succesful fingerprint authentication + * @hide + */ + public static final String FP_SUCCESS_VIBRATE = "fp_success_vibrate"; + + /** + * Whether to vibrate on unsuccesful fingerprint authentication + * @hide + */ + public static final String FP_ERROR_VIBRATE = "fp_error_vibrate"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index cd890a684d84..b5e86ec14433 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -141,6 +141,7 @@ constructor( keyguardRootViewModel, keyguardBlueprintViewModel, configuration, + context, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, shadeInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 6e7846016966..50c4ec3b8a49 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -20,8 +20,11 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.DrawableRes import android.annotation.SuppressLint +import android.content.Context; import android.graphics.Point import android.graphics.Rect +import android.os.UserHandle; +import android.provider.Settings import android.view.HapticFeedbackConstants import android.view.InputDevice import android.view.MotionEvent @@ -96,6 +99,7 @@ object KeyguardRootViewBinder { viewModel: KeyguardRootViewModel, blueprintViewModel: KeyguardBlueprintViewModel, configuration: ConfigurationState, + context: Context, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?, chipbarCoordinator: ChipbarCoordinator?, shadeInteractor: ShadeInteractor, @@ -146,17 +150,20 @@ object KeyguardRootViewBinder { view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { + launch { deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect { - if (msdlFeedback()) { - msdlPlayer?.playToken( - MSDLToken.UNLOCK, - authInteractionProperties, - ) - } else { + if (!isFpHapticEnabled(context, Settings.System.FP_SUCCESS_VIBRATE)) return@collect + + val playedMsdl = msdlFeedback() && (msdlPlayer?.let { + it.playToken(MSDLToken.UNLOCK, authInteractionProperties) + true + } == true) + + if (!playedMsdl) { vibratorHelper.performHapticFeedback( view, - HapticFeedbackConstants.BIOMETRIC_CONFIRM, + HapticFeedbackConstants.BIOMETRIC_CONFIRM ) } } @@ -164,15 +171,17 @@ object KeyguardRootViewBinder { launch { deviceEntryHapticsInteractor.playErrorHaptic.collect { - if (msdlFeedback()) { - msdlPlayer?.playToken( - MSDLToken.FAILURE, - authInteractionProperties, - ) - } else { + if (!isFpHapticEnabled(context, Settings.System.FP_ERROR_VIBRATE)) return@collect + + val playedMsdl = msdlFeedback() && (msdlPlayer?.let { + it.playToken(MSDLToken.FAILURE, authInteractionProperties) + true + } == true) + + if (!playedMsdl) { vibratorHelper.performHapticFeedback( view, - HapticFeedbackConstants.BIOMETRIC_REJECT, + HapticFeedbackConstants.BIOMETRIC_REJECT ) } } @@ -434,6 +443,11 @@ object KeyguardRootViewBinder { return disposables } + private fun isFpHapticEnabled(context: Context, key: String): Boolean { + return Settings.System.getIntForUser( + context.contentResolver, key, 1, UserHandle.USER_CURRENT) == 1 + } + /** * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. */ From 0df83b9804250681ba41141e46fc511ad7192d90 Mon Sep 17 00:00:00 2001 From: lijilou Date: Mon, 6 May 2024 14:41:14 +0800 Subject: [PATCH 0292/1315] FingerprintAuthenticationClient:fix NPE problem due to getListener method return var is null. Bug: 338907035 Change-Id: I875afd3eeeba24b731e68161060285dcaf3f41aa Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../fingerprint/aidl/FingerprintAuthenticationClient.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index cd864eb43e84..51fc0b3e0da4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -445,7 +445,9 @@ public void onLockoutTimed(long durationMillis) { .Builder(BiometricSourceType.FINGERPRINT, getRequestReason(), getErrorString(getContext(), error, 0), error).build() ); - getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */); + if (getListener() != null) { + getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -479,7 +481,9 @@ public void onLockoutPermanent() { .Builder(BiometricSourceType.FINGERPRINT, getRequestReason(), getErrorString(getContext(), error, 0), error).build() ); - getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */); + if (getListener() != null) { + getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } From 486207cd772f881cc9fa41c55f210179059ac21e Mon Sep 17 00:00:00 2001 From: "muzbit.kim" Date: Thu, 7 Nov 2013 14:18:23 +0900 Subject: [PATCH 0293/1315] SQLiteDatabase: Catch corrupt exception during transaction If corruption occurs in a transaction running, DB file should be deleted. Change-Id: I5b3a122ebe51770966c334021ab6717d5ff081c3 Signed-off-by: muzbit.kim Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/database/sqlite/SQLiteDatabase.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 432954036912..f370c1dd04f8 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -844,6 +844,9 @@ private void beginTransaction(@Nullable SQLiteTransactionListener listener, int boolean readOnly = (mode == SQLiteSession.TRANSACTION_MODE_DEFERRED); getThreadSession().beginTransaction(mode, listener, getThreadDefaultConnectionFlags(readOnly), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { releaseReference(); } @@ -857,6 +860,9 @@ public void endTransaction() { acquireReference(); try { getThreadSession().endTransaction(null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { releaseReference(); } @@ -974,6 +980,9 @@ private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYie acquireReference(); try { return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { releaseReference(); } From ade8a9d5764ad586d1345ea79a75245737d47c78 Mon Sep 17 00:00:00 2001 From: blackshibe Date: Fri, 11 Oct 2024 17:48:46 +0200 Subject: [PATCH 0294/1315] SystemUI: Add ability to hide carrier name on lockscreen [1/2] [someone5678] * Minor clean-ups and edit * Make it default to on (Show carrier text) [ShevT] * Adapt for A15 [neobuddy89] * Code clean up! Original commit: https://github.com/blackshibe/android_frameworks_base/commit/080225e4e911c62315f1e5cf6e0488705d90f0ed Change-Id: I5951c88d789cf66720b702d61e156982526dcf59 Signed-off-by: someone5678 <59456192+someone5678@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 +++ .../android/keyguard/CarrierTextManager.java | 43 +++++++++++++++++++ .../logging/CarrierTextManagerLogger.kt | 3 ++ 3 files changed, 52 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 98c84a1a6938..a879bb012a69 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7143,6 +7143,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String FP_ERROR_VIBRATE = "fp_error_vibrate"; + /** + * Whether to show the carrier name on the lockscreen + * @hide + */ + public static final String LOCKSCREEN_SHOW_CARRIER = "lockscreen_show_carrier"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index 3186aadee211..ba86e3d81cb9 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -17,6 +17,7 @@ package com.android.keyguard; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ACTIVE_DATA_SUB_CHANGED; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_CARRIER_ON_LOCKSCREEN_CHANGED; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_TELEPHONY_CAPABLE; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_REFRESH_CARRIER_INFO; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SATELLITE_CHANGED; @@ -27,7 +28,12 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; import android.os.Trace; +import android.os.UserHandle; +import android.provider.Settings; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener; @@ -94,6 +100,8 @@ public class CarrierTextManager { @Nullable private String mSatelliteCarrierText; + private boolean mShowCarrierText = true; + private final Context mContext; private final TelephonyManager mTelephonyManager; private final CharSequence mSeparator; @@ -231,6 +239,8 @@ private CarrierTextManager( }); } }); + SettingsObserver observer = new SettingsObserver(); + observer.observe(); } private TelephonyManager getTelephonyManager() { @@ -457,6 +467,12 @@ protected void updateCarrierText() { } boolean isInSatelliteMode = mSatelliteCarrierText != null; + + // Hide the carrier text if the user requests + if (!mShowCarrierText) { + displayText = ""; + } + final CarrierTextCallbackInfo info = new CarrierTextCallbackInfo( displayText, carrierNames, @@ -671,6 +687,33 @@ private void cancelSatelliteCollectionJob(String reason) { } } + private class SettingsObserver extends ContentObserver { + SettingsObserver() { + super(new Handler()); + } + + void observe() { + mContext.getContentResolver().registerContentObserver(Settings.System.getUriFor( + Settings.System.LOCKSCREEN_SHOW_CARRIER), false, this, + UserHandle.USER_ALL); + updateSettings(); + } + + void updateSettings() { + mShowCarrierText = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.LOCKSCREEN_SHOW_CARRIER, 1, UserHandle.USER_CURRENT) != 0; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (Settings.System.getUriFor(Settings.System.LOCKSCREEN_SHOW_CARRIER).equals(uri)) { + mLogger.logUpdateCarrierTextForReason(REASON_CARRIER_ON_LOCKSCREEN_CHANGED); + updateSettings(); + updateCarrierText(); + } + } + } + /** Injectable Buildeer for {@#link CarrierTextManager}. */ public static class Builder { private final Context mContext; diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt index 7d0c49144219..6a5d4b018c70 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt @@ -178,6 +178,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu const val REASON_SIM_ERROR_STATE_CHANGED = 3 const val REASON_ACTIVE_DATA_SUB_CHANGED = 4 const val REASON_SATELLITE_CHANGED = 5 + const val REASON_CARRIER_ON_LOCKSCREEN_CHANGED = 6 @Retention(AnnotationRetention.SOURCE) @IntDef( @@ -188,6 +189,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu REASON_SIM_ERROR_STATE_CHANGED, REASON_ACTIVE_DATA_SUB_CHANGED, REASON_SATELLITE_CHANGED, + REASON_CARRIER_ON_LOCKSCREEN_CHANGED, ] ) annotation class CarrierTextRefreshReason @@ -199,6 +201,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu REASON_SIM_ERROR_STATE_CHANGED -> "SIM_ERROR_STATE_CHANGED" REASON_ACTIVE_DATA_SUB_CHANGED -> "ACTIVE_DATA_SUB_CHANGED" REASON_SATELLITE_CHANGED -> "SATELLITE_CHANGED" + REASON_CARRIER_ON_LOCKSCREEN_CHANGED -> "CARRIER_ON_LOCKSCREEN_CHANGED" else -> "unknown" } } From 6301e0848c68ed32dad3b48b74b7fe044f27e3f1 Mon Sep 17 00:00:00 2001 From: jhenrique09 Date: Mon, 13 Aug 2018 12:19:51 -0400 Subject: [PATCH 0295/1315] Shell: Don't show bugreport on DocumentsUI Change-Id: I3dde24312f416b1ae2d365fea683aacd8970d923 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/Shell/AndroidManifest.xml | 4 ++-- .../java/com/android/server/am/ContentProviderHelper.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 5e993fca1e50..0f6062a3719d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -1072,7 +1072,7 @@ android:resource="@xml/file_provider_paths" /> - - + --> Date: Sat, 22 Sep 2018 19:05:47 -0500 Subject: [PATCH 0296/1315] development: Address NPE when removing preferences out of developer options Change-Id: I9402a9668c046f29024793d473aed90b74a008a3 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../development/DeveloperOptionsPreferenceController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java index b29595eab5c7..79416ce6a4f8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java @@ -75,14 +75,18 @@ public void onDeveloperOptionsDisabled() { * Called when developer options is enabled and the preference is available */ protected void onDeveloperOptionsSwitchEnabled() { - mPreference.setEnabled(true); + if (mPreference != null) { + mPreference.setEnabled(true); + } } /** * Called when developer options is disabled and the preference is available */ protected void onDeveloperOptionsSwitchDisabled() { - mPreference.setEnabled(false); + if (mPreference != null) { + mPreference.setEnabled(false); + } } } From 1e01399f2901b37339a3b99aa29bc6f2d2ae4c8f Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 10 Mar 2023 20:18:18 +0800 Subject: [PATCH 0297/1315] ConfigurationController: Prevent app crash on orientation change * performConfigurationChanged expects a non-null callback due to a existing null check (callbacks != null), for some reasons the config has no null check not sure if its non-null or it has a null check somewhere else * attempt to fix random app crash on apps that starts by changing orientation: 04-04 05:57:08.186 3024 3024 E AndroidRuntime: FATAL EXCEPTION: main 04-04 05:57:08.186 3024 3024 E AndroidRuntime: Process: com.android.systemui, PID: 3024 04-04 05:57:08.186 3024 3024 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void com.android.systemui.statusbar.policy.ConfigurationController$ConfigurationListener.onConfigChanged(android.content.res.Configuration)' on a null object reference 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at com.android.systemui.statusbar.phone.ConfigurationControllerImpl.onConfigurationChanged(ConfigurationControllerImpl.kt:31) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at com.android.systemui.SystemUIApplication.onConfigurationChanged(SystemUIApplication.java:46) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ConfigurationController.performConfigurationChanged(ConfigurationController.java:246) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:220) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:131) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ActivityThread.handleConfigurationChanged(ActivityThread.java:5998) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.servertransaction.ConfigurationChangeItem.execute(ConfigurationChangeItem.java:43) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:201) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.os.Looper.loop(Looper.java:288) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7943) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 04-04 05:57:08.186 3024 3024 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:848) Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ConfigurationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index f491e3d274db..c6a4d2ad321b 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -231,7 +231,7 @@ private void handleConfigurationChangedInner(@Nullable Configuration config, final int size = callbacks.size(); for (int i = 0; i < size; i++) { ComponentCallbacks2 cb = callbacks.get(i); - if (!equivalent) { + if (!equivalent && cb != null && config != null) { performConfigurationChanged(cb, config); } } From f55bb5022c46b0e1fd79dbbee224381d06154695 Mon Sep 17 00:00:00 2001 From: lijilou Date: Fri, 26 Apr 2024 09:50:31 +0800 Subject: [PATCH 0298/1315] FileRotator:fix NPE due to The File.list() method may be return null. Bug: 337070728 Change-Id: I63707b4f9fe3bcd705e98ac2e69a5101d092de76 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/util/FileRotator.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java index bcad6fc85ca9..22014a411e49 100644 --- a/core/java/com/android/internal/util/FileRotator.java +++ b/core/java/com/android/internal/util/FileRotator.java @@ -334,7 +334,15 @@ private String getActiveName(long currentTimeMillis) { long oldestActiveStart = Long.MAX_VALUE; final FileInfo info = new FileInfo(mPrefix); - for (String name : mBasePath.list()) { + String[] baseFiles = mBasePath.list(); + if (baseFiles == null) { + // no file in the path and create one starting now + info.startMillis = currentTimeMillis; + info.endMillis = Long.MAX_VALUE; + return info.build(); + } + + for (String name : baseFiles) { if (!info.parse(name)) continue; // pick the oldest active file which covers current time From b13560f0bd55c9a3fce771a8e23de3f66a5cad24 Mon Sep 17 00:00:00 2001 From: fusionjack Date: Thu, 18 Jun 2015 20:08:56 +0200 Subject: [PATCH 0299/1315] ActivityManagerNative: Prevent possible soft-reboot E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void android.app.IActivityManager.attachApplication(android.app.IApplicationThread)' on a null object reference E AndroidRuntime: at android.app.ActivityThread.attach(ActivityThread.java:5110) E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5262) E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) E AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:375) E AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:904) E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:699) E AndroidRuntime: Error reporting crash E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void android.app.IActivityManager.handleApplicationCrash(android.os.IBinder, android.app.ApplicationErrorReport$CrashInfo)' on a null object reference E AndroidRuntime: at com.android.internal.os.RuntimeInit$UncaughtHandler.uncaughtException(RuntimeInit.java:89) E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:693) E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:690) Change-Id: I6940b723de455ae867ae784d4636e38a4196a689 Change-Id: I073fcb6d4e5ca506b7cfc953e1fc3667d84f5432 Signed-off-by: someone5678 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 45c312ee6598..811ff1a97369 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -9073,7 +9073,9 @@ private void attach(boolean system, long startSeq) { RuntimeInit.setApplicationObject(mAppThread.asBinder()); final IActivityManager mgr = ActivityManager.getService(); try { - mgr.attachApplication(mAppThread, startSeq); + if (mgr != null) { + mgr.attachApplication(mAppThread, startSeq); + } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -9091,8 +9093,11 @@ private void attach(boolean system, long startSeq) { + " total=" + (runtime.totalMemory()/1024) + " used=" + (dalvikUsed/1024)); mSomeActivitiesChanged = false; + final IActivityTaskManager atmgr = ActivityTaskManager.getService(); try { - ActivityTaskManager.getService().releaseSomeActivities(mAppThread); + if (atmgr != null) { + atmgr.releaseSomeActivities(mAppThread); + } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } From d3764e57e004a86a98d52da402f4af959663dae5 Mon Sep 17 00:00:00 2001 From: Andy CrossGate Yan Date: Sat, 7 Oct 2023 17:41:37 +0800 Subject: [PATCH 0300/1315] Make all activities resizable This eliminates black borders in legacy apps on 18:9 screens Change-Id: Ied0b8bead9a3996c60cebd20538c12dce5071568 Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/content/pm/PackageParser.java | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 4dbbb0f9d056..a72af4dd7e92 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -4633,43 +4633,7 @@ private Activity parseActivity(Package owner, Resources res, } private void setActivityResizeMode(ActivityInfo aInfo, TypedArray sa, Package owner) { - final boolean appExplicitDefault = (owner.applicationInfo.privateFlags - & (PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE - | PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE)) != 0; - - if (sa.hasValue(R.styleable.AndroidManifestActivity_resizeableActivity) - || appExplicitDefault) { - // Activity or app explicitly set if it is resizeable or not; - final boolean appResizeable = (owner.applicationInfo.privateFlags - & PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE) != 0; - if (sa.getBoolean(R.styleable.AndroidManifestActivity_resizeableActivity, - appResizeable)) { - aInfo.resizeMode = RESIZE_MODE_RESIZEABLE; - } else { - aInfo.resizeMode = RESIZE_MODE_UNRESIZEABLE; - } - return; - } - - if ((owner.applicationInfo.privateFlags - & PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION) != 0) { - // The activity or app didn't explicitly set the resizing option, however we want to - // make it resize due to the sdk version it is targeting. - aInfo.resizeMode = RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; - return; - } - - // resize preference isn't set and target sdk version doesn't support resizing apps by - // default. For the app to be resizeable if it isn't fixed orientation or immersive. - if (aInfo.isFixedOrientationPortrait()) { - aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY; - } else if (aInfo.isFixedOrientationLandscape()) { - aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; - } else if (aInfo.isFixedOrientation()) { - aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION; - } else { - aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZEABLE; - } + aInfo.resizeMode = RESIZE_MODE_RESIZEABLE; } /** From 03be150566c4f8eb42cfb7d4d9b170284b9b4940 Mon Sep 17 00:00:00 2001 From: Daisuke Sakamoto Date: Thu, 12 Jul 2018 10:27:10 +0900 Subject: [PATCH 0301/1315] Avoid crash when dream starts A clean flashed device with developer options "Stay awake" set crashes at onDreamingStarted. Prevent SystemUI from crashing. Test: Enable stay awake and charge device Bug: 233361654 Change-Id: I75689ebc3ac1597f0c48b7a000745b67dec0d941 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/doze/DozeService.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index 35e53ecfa3b4..e887bf42e3de 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -74,7 +74,9 @@ public void onDestroy() { mPluginManager.removePluginListener(this); } super.onDestroy(); - mDozeMachine.destroy(); + if (mDozeMachine != null) { + mDozeMachine.destroy(); + } mDozeMachine = null; } @@ -95,7 +97,9 @@ public void onPluginDisconnected(DozeServicePlugin plugin) { @Override public void onDreamingStarted() { super.onDreamingStarted(); - mDozeMachine.requestState(DozeMachine.State.INITIALIZED); + if (mDozeMachine != null) { + mDozeMachine.requestState(DozeMachine.State.INITIALIZED); + } startDozing(); if (mDozePlugin != null) { mDozePlugin.onDreamingStarted(); @@ -105,7 +109,9 @@ public void onDreamingStarted() { @Override public void onDreamingStopped() { super.onDreamingStopped(); - mDozeMachine.requestState(DozeMachine.State.FINISH); + if (mDozeMachine != null) { + mDozeMachine.requestState(DozeMachine.State.FINISH); + } if (mDozePlugin != null) { mDozePlugin.onDreamingStopped(); } From bc3a99a7ba167509475c9a8e7eb81400152f8f6c Mon Sep 17 00:00:00 2001 From: Jason Edson Date: Mon, 26 Sep 2022 07:27:48 +0000 Subject: [PATCH 0302/1315] EnhancedEstimates: Get estimates from Device Health Services Get battery estimates from com.google.android.apps.turbo (Turbo.apk) Code taken from android11 SystemUIGoogle.apk and cleaned up [jhonboy121]: * use GlobalSettings class for acquiring settings * use proper access modifiers and finalize many vars. * simplify estimates processing logic * use modern try catch method for properly closing cursor * cache many variables used a lot to reduce runtime * use Utils class method to check whether package is installed Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/power/EnhancedEstimatesImpl.java | 102 ++++++++++++++++-- 1 file changed, 91 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java index 90da8912ad75..5c2acef4c281 100644 --- a/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java @@ -1,43 +1,123 @@ package com.android.systemui.power; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.Settings; +import android.util.KeyValueListParser; +import android.util.Log; + +import com.android.internal.util.matrixx.Utils; import com.android.settingslib.fuelgauge.Estimate; -import com.android.settingslib.fuelgauge.EstimateKt; +import com.android.settingslib.utils.PowerUtil; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.power.EnhancedEstimates; +import com.android.systemui.util.settings.GlobalSettings; + +import java.time.Duration; import javax.inject.Inject; @SysUISingleton -public class EnhancedEstimatesImpl implements EnhancedEstimates { +public final class EnhancedEstimatesImpl implements EnhancedEstimates { + + private static final String TAG = "EnhancedEstimatesImpl"; + + private static final Estimate EMPTY_ESTIMATE = new Estimate(-1L, false, -1L); + + private static final Duration DAY = Duration.ofDays(1L); + private static final long HOUR = Duration.ofHours(1L).toMillis(); + private static final long THREE_HOURS = Duration.ofHours(3L).toMillis(); + private static final long FIFTEEN_MINUTES = Duration.ofMinutes(15L).toMillis(); + + private final Context mContext; + private final GlobalSettings mGlobalSettings; + private final KeyValueListParser mParser; @Inject - public EnhancedEstimatesImpl() { + public EnhancedEstimatesImpl( + Context context, + GlobalSettings globalSettings + ) { + mContext = context; + mGlobalSettings = globalSettings; + mParser = new KeyValueListParser(','); } @Override public boolean isHybridNotificationEnabled() { - return false; + final boolean isTurboInstalled = Utils.isPackageInstalled( + mContext, + "com.google.android.apps.turbo", + false /* ignoreState */ + ); + if (!isTurboInstalled) return false; + updateFlags(); + return mParser.getBoolean("hybrid_enabled", true); } @Override public Estimate getEstimate() { - // Returns an unknown estimate. - return new Estimate(EstimateKt.ESTIMATE_MILLIS_UNKNOWN, - false /* isBasedOnUsage */, - EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + final Uri build = new Uri.Builder() + .scheme("content") + .authority("com.google.android.apps.turbo.estimated_time_remaining") + .appendPath("time_remaining") + .build(); + try (final Cursor query = mContext.getContentResolver().query(build, null, null, null, null)) { + if (query == null) return EMPTY_ESTIMATE; + try { + if (query.moveToFirst()) { + long timeRemaining = -1L; + final int usageColumnIndex = query.getColumnIndex("is_based_on_usage"); + final boolean isBasedOnUsage = usageColumnIndex != -1 && query.getInt(usageColumnIndex) != 0; + final int batteryLifecolumnIndex = query.getColumnIndex("average_battery_life"); + if (batteryLifecolumnIndex != -1) { + final long averageBatteryLife = query.getLong(batteryLifecolumnIndex); + if (averageBatteryLife != -1L) { + final long duration = Duration.ofMillis(averageBatteryLife).compareTo(DAY) >= 0 + ? HOUR : FIFTEEN_MINUTES; + timeRemaining = PowerUtil.roundTimeToNearestThreshold(averageBatteryLife, duration); + } + } + return new Estimate( + query.getLong(query.getColumnIndex("battery_estimate")), + isBasedOnUsage, + timeRemaining + ); + } + } catch (Exception ex) { + // Catch and release + } + } catch (Exception e) { + Log.e(TAG, "Something went wrong when getting an estimate from Turbo", e); + } + return EMPTY_ESTIMATE; } @Override public long getLowWarningThreshold() { - return 0; + updateFlags(); + return mParser.getLong("low_threshold", THREE_HOURS); } @Override public long getSevereWarningThreshold() { - return 0; + updateFlags(); + return mParser.getLong("severe_threshold", HOUR); } @Override public boolean getLowWarningEnabled() { - return true; + updateFlags(); + return mParser.getBoolean("low_warning_enabled", false); + } + + private void updateFlags() { + final String string = mGlobalSettings.getString("hybrid_sysui_battery_warning_flags"); + try { + mParser.setString(string); + } catch (IllegalArgumentException ex) { + Log.e(TAG, "Bad hybrid sysui warning flags"); + } } } From 4cf2990002ceaf0f8a96675c06fad170210b968c Mon Sep 17 00:00:00 2001 From: SamarV-121 Date: Fri, 8 Apr 2022 17:25:58 +0530 Subject: [PATCH 0303/1315] telephony: SmsMessage: Bring newFromCDS method back 04-07 07:52:44.272 2200 2200 E AndroidRuntime: FATAL EXCEPTION: main 04-07 07:52:44.272 2200 2200 E AndroidRuntime: Process: com.mediatek.ims, PID: 2200 04-07 07:52:44.272 2200 2200 E AndroidRuntime: java.lang.NoSuchMethodError: No static method newFromCDS([B)Lcom/android/internal/telephony/gsm/SmsMessage; in class Lcom/android/internal/telephony/gsm/SmsMessage; or its super classes (declaration of 'com.android.internal.telephony.gsm.SmsMessage' appears in /system/framework/framework.jar!classes4.dex) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at com.mediatek.ims.feature.MtkImsSmsImpl.newStatusReportInd(MtkImsSmsImpl.java:153) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at com.mediatek.ims.feature.MtkMmTelFeature$1.newStatusReportInd(MtkMmTelFeature.java:173) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at com.mediatek.ims.ImsService$MyHandler.handleMessage(ImsService.java:2606) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:201) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at android.os.Looper.loop(Looper.java:288) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7870) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 04-07 07:52:44.272 2200 2200 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) Change-Id: If37782879a9d9ddfac8283fa243858327ce78850 Signed-off-by: SamarV-121 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/internal/telephony/gsm/SmsMessage.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java b/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java index 547cda2e6b10..44bd01416d25 100644 --- a/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java +++ b/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java @@ -140,6 +140,17 @@ public boolean isTypeZero() { return (mProtocolIdentifier == 0x40); } + public static SmsMessage newFromCDS(byte[] pdu) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Rlog.e(LOG_TAG, "CDS SMS PDU parsing failed: ", ex); + return null; + } + } + /** * Creates an SmsMessage from an SMS EF record. * From 98477a6b7aafa1c0a0b8af1e3822875f4b539583 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 25 Mar 2025 17:01:32 +0200 Subject: [PATCH 0304/1315] SystemUI: fix a screenshot process crash in ScrollCaptureController Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/screenshot/scroll/ScrollCaptureController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java index 8c8ceba12fe2..3073605e37dc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java @@ -286,7 +286,7 @@ private void onStartComplete() { } mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_STARTED, 0, mWindowOwner); requestNextTile(0); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException | ExecutionException | CancellationException e) { // Failure to start, propagate to caller Log.e(TAG, "session start failed!"); if (mCaptureCompleter != null) { From a8bd58ad9e007ad8ba53c3a6371dcff34918d184 Mon Sep 17 00:00:00 2001 From: Simao Gomes Viana Date: Sun, 9 Jul 2017 20:53:52 +0200 Subject: [PATCH 0305/1315] base: Sensor block per-package switch Co-authored-by: LorDClockaN Co-authored-by: HolyAngel Co-authored-by: Hikari-no-Tenshi Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/hardware/SystemSensorManager.java | 116 ++++++++++++++++++ core/java/android/provider/Settings.java | 14 +++ core/res/res/values/matrixx_config.xml | 37 ++++++ core/res/res/values/matrixx_symbols.xml | 3 + .../server/wm/ActivityTaskManagerService.java | 9 ++ 5 files changed, 179 insertions(+) diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 8f6a23dc389a..1c14f12dbab6 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -29,15 +29,20 @@ import android.compat.annotation.EnabledAfter; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.MemoryFile; import android.os.MessageQueue; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -56,6 +61,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * Sensor manager implementation that communicates with the built-in @@ -145,6 +152,10 @@ private static native int nativeSetOperationParameter( private Optional mHasHighSamplingRateSensorsPermission = Optional.empty(); + private String mBlockedPackageList; + private final Set mBlockedApps = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + /** @hide */ public SystemSensorManager(Context context, Looper mainLooper) { synchronized (sLock) { @@ -172,6 +183,12 @@ public SystemSensorManager(Context context, Looper mainLooper) { mHandleToSensor.put(sensor.getHandle(), sensor); } } + + parsePackageList(); + + SettingsObserver observer = new SettingsObserver( + new Handler(mMainLooper)); + observer.observe(); } /** @hide */ @@ -252,6 +269,76 @@ protected List getFullDynamicSensorList() { return mFullDynamicSensorsList; } + private void parsePackageList() { + String blockedApp = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.SENSOR_BLOCKED_APP); + if (blockedApp == null) { + blockedApp = TextUtils.join("|", mContext.getResources().getStringArray( + com.android.internal.R.array.config_blockPackagesSensorDrain)); + } + splitAndAddToArrayList(blockedApp, "\\|"); + } + + private void savePackageList(ArrayList arrayList) { + String setting = Settings.Global.SENSOR_BLOCKED_APP; + + List settings = new ArrayList(); + for (String app : arrayList) { + settings.add(app.toString()); + } + final String value = TextUtils.join("|", settings); + if (TextUtils.equals(setting, Settings.Global.SENSOR_BLOCKED_APP)) { + mBlockedPackageList = value; + } + Settings.Global.putString(mContext.getContentResolver(), + setting, value); + } + + private void addBlockedApp(String packageName) { + if (mBlockedApps.add(packageName)) { + savePackageList(new ArrayList<>(mBlockedApps)); + } + } + + private boolean isBlockedApp(String packageName) { + return mBlockedApps.contains(packageName); + } + + public void notePackageUninstalled(String pkgName) { + if (mBlockedApps.remove(pkgName)) { + savePackageList(new ArrayList<>(mBlockedApps)); + } + } + + private void splitAndAddToArrayList(String baseString, String separator) { + mBlockedApps.clear(); + if (baseString != null) { + for (String s : TextUtils.split(baseString, separator)) { + final String v = s.trim(); + if (!v.isEmpty()) mBlockedApps.add(v); + } + } + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.SENSOR_BLOCKED_APP), false, this); + resolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.SENSOR_BLOCK), false, this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + parsePackageList(); + } + } + /** @hide */ @Override protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, @@ -269,6 +356,21 @@ protected boolean registerListenerImpl(SensorEventListener listener, Sensor sens Log.e(TAG, "maxBatchReportLatencyUs and delayUs should be non-negative"); return false; } + + if (Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SENSOR_BLOCK, 0) == 1) { + int sensortype = sensor.getType(); + if (sensortype == Sensor.TYPE_SIGNIFICANT_MOTION || + sensortype == Sensor.TYPE_ACCELEROMETER || + sensortype == Sensor.TYPE_LINEAR_ACCELERATION) { + String pkgName = mContext.getPackageName(); + if (isBlockedApp(pkgName)) { + Log.w(TAG, "Preventing " + pkgName + " from using " + sensor.getStringType()); + return false; + } + } + } + if (mSensorListeners.size() >= MAX_LISTENER_COUNT) { Log.e(TAG, "Too many sensor listeners! Dump:"); Map listenerCounts = new HashMap<>(); @@ -350,6 +452,20 @@ protected boolean requestTriggerSensorImpl(TriggerEventListener listener, Sensor if (sensor.getReportingMode() != Sensor.REPORTING_MODE_ONE_SHOT) return false; + if (Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SENSOR_BLOCK, 0) == 1) { + final int sensortype = sensor.getType(); + if (sensortype == Sensor.TYPE_SIGNIFICANT_MOTION || + sensortype == Sensor.TYPE_ACCELEROMETER || + sensortype == Sensor.TYPE_LINEAR_ACCELERATION) { + final String pkgName = mContext.getPackageName(); + if (isBlockedApp(pkgName)) { + Log.w(TAG, "Preventing " + pkgName + " from using " + sensor.getStringType()); + return false; + } + } + } + if (mTriggerListeners.size() >= MAX_LISTENER_COUNT) { throw new IllegalStateException("request failed, " + "the trigger listeners size has exceeded the maximum limit " diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a879bb012a69..8082f4015846 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -19635,6 +19635,20 @@ public static final class Global extends NameValueTable { */ public static final String BLUETOOTH_OFF_TIMEOUT = "bluetooth_off_timeout"; + /** + * Sensor block per-package + * @hide + */ + @Readable + public static final String SENSOR_BLOCK = "sensor_block"; + + /** + * Sensor blocked packages + * @hide + */ + @Readable + public static final String SENSOR_BLOCKED_APP = "sensor_blocked_app"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index 55d6173ab4b5..29e8042cd4b9 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -37,4 +37,41 @@ 1 + + + + com.whatsapp + com.gbwhatsapp + com.yowhatsapp + com.whatsapp.plus + org.telegram.messenger + org.telegram.messenger.plus + com.netease.cloudmusic + fm.xiami.main + com.netease.snailread + com.baidu.browser.apps + org.thunderdog.challegram + com.snapchat.android + com.facebook.orca + com.Slack + tugapower.codeaurora.browser + org.mozilla.firefox + com.android.chrome + com.amazon.mShop.android.shopping + com.google.android.inputmethod.latin + com.google.android.apps.plus + com.google.android.apps.maps + ru.ok.android + com.instagram.android.MainTabActivity + com.facebook.orca + com.facebook.orca.StartScreenActivity + com.spotify.music + com.spotify.music.MainActivity + com.android.vending + com.trtf.blue + com.truecaller + com.gaana + com.facebook.katana.LoginActivity + diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 3c1835d5171f..065d50d8c1e1 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -57,4 +57,7 @@ + + + diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 8606d439eea3..bf0a08b5ae36 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -209,6 +209,7 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; +import android.hardware.SystemSensorManager; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -827,6 +828,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // Lineage sdk activity related helper private LineageActivityManager mLineageActivityManager; + private SystemSensorManager mSystemSensorManager; + private final class SettingObserver extends ContentObserver { private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE); private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS); @@ -923,6 +926,9 @@ public void installSystemProviders() { // LineageActivityManager depends on settings so we can initialize only // after providers are available. mLineageActivityManager = new LineageActivityManager(mContext); + + // Block sensor usage per app + mSystemSensorManager = new SystemSensorManager(mContext, mContext.getMainLooper()); } public void retrieveSettings(ContentResolver resolver) { @@ -7032,6 +7038,9 @@ public void onPackageUninstalled(String name, int userId) { mAppWarnings.onPackageUninstalled(name, userId); mCompatModePackages.handlePackageUninstalledLocked(name); mPackageConfigPersister.onPackageUninstall(name, userId); + if (mSystemSensorManager != null) { + mSystemSensorManager.notePackageUninstalled(name); + } } mWindowStyleCache.invalidatePackage(name); } From 05733c650d70b4abbcdc64a1199268a18dfc9f1a Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 11 Dec 2025 06:33:26 +0530 Subject: [PATCH 0306/1315] KeyGestureController: Fix screenshot shortcut crash Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/input/KeyGestureController.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 3ca77fe37406..7292c30732aa 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -1384,8 +1384,15 @@ private boolean handleMessage(Message msg) { mAccessibilityShortcutController.performAccessibilityShortcut(); break; case MSG_SCREENSHOT_SHORTCUT: - TakeScreenshotData data = (TakeScreenshotData) msg.obj; - takeScreenshot(data.source, data.type, data.displayId); + Object o = msg.obj; + if (o instanceof TakeScreenshotData) { + TakeScreenshotData data = (TakeScreenshotData) o; + takeScreenshot(data.source, data.type, data.displayId); + } else { + int source = msg.arg1 != 0 ? msg.arg1 : SCREENSHOT_KEY_OTHER; + int displayId = msg.arg2 != 0 ? msg.arg2 : Display.DEFAULT_DISPLAY; + takeScreenshot(source, WindowManager.TAKE_SCREENSHOT_FULLSCREEN, displayId); + } break; case MSG_EXIT_FOCUSED_APP: handleKeyGesture((AidlKeyGestureEvent) msg.obj, /* focusedToken= */null); @@ -1920,8 +1927,14 @@ public void handleKeyGestureEvent(@NonNull KeyGestureEvent event, break; case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT: if (complete) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_SCREENSHOT_SHORTCUT, - SCREENSHOT_KEY_OTHER, event.getDisplayId())); + mHandler.sendMessage( + mHandler.obtainMessage( + MSG_SCREENSHOT_SHORTCUT, + new TakeScreenshotData( + SCREENSHOT_KEY_OTHER, + WindowManager.TAKE_SCREENSHOT_FULLSCREEN, + event.getDisplayId() + ))); } break; case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD: From 49712d5e7ba32f4a492b014db44813c1a7863767 Mon Sep 17 00:00:00 2001 From: rohan Date: Sun, 22 Mar 2020 14:47:47 +0530 Subject: [PATCH 0307/1315] Add more device key actions - Torch - Screenshot - Volume - Clear All Notifications - Expand Notifications - QS panel - Ringer modes [neobuddy89: Rewrite all.] Change-Id: I410f75b9429b90bfe8985dd44558a1194abd6c8c Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/policy/PhoneWindowManager.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f33c4402cec5..97d551ad829a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -87,6 +87,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; @@ -246,6 +247,7 @@ import com.android.internal.policy.PhoneWindow; import com.android.internal.policy.TransitionAnimation; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.util.ScreenshotHelper; import com.android.internal.widget.LockPatternUtils; import com.android.server.AccessibilityManagerInternal; import com.android.server.DockObserverInternal; @@ -835,6 +837,8 @@ public void onDrawn() { private boolean mLongSwipeDown; private CameraAvailbilityListener mCameraAvailabilityListener; + private ScreenshotHelper mScreenshotHelper; + private class PolicyHandler extends Handler { private PolicyHandler(Looper looper) { @@ -2308,6 +2312,27 @@ private void performKeyAction(Action action, KeyEvent event, int assistInvocatio case PLAY_PAUSE_MUSIC: triggerVirtualKeypress(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); break; + case TORCH: + toggleTorch(); + break; + case SCREENSHOT: + mScreenshotHelper.takeScreenshot(SCREENSHOT_KEY_OTHER, mHandler, null); + break; + case VOLUME_PANEL: + toggleVolumePanel(); + break; + case CLEAR_ALL_NOTIFICATIONS: + clearAllNotifications(); + break; + case NOTIFICATIONS: + toggleNotificationPanel(); + break; + case QS_PANEL: + toggleQsPanel(); + break; + case RINGER_MODES: + toggleRingerModes(); + break; default: break; } @@ -2604,6 +2629,7 @@ void init(Injector injector) { } mHandler = new PolicyHandler(injector.getLooper()); + mScreenshotHelper = new ScreenshotHelper(mContext); mWakeGestureListener = new MyWakeGestureListener(mContext, mHandler); mSettingsObserver = new SettingsObserver(mHandler); @@ -7823,4 +7849,51 @@ public boolean isAnyCameraInUse() { return !mCameraInUse.isEmpty(); } } + + private void toggleVolumePanel() { + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + am.adjustVolume(AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI); + } + + private void clearAllNotifications() { + IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + try { + statusBarService.onClearAllNotifications(ActivityManager.getCurrentUser()); + } catch (RemoteException e) { + // do nothing. + } + } + } + + private void toggleQsPanel() { + IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + try { + statusBarService.expandSettingsPanel(null); + } catch (RemoteException e) { + // do nothing. + } + } + } + + private void toggleRingerModes() { + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + + switch (am.getRingerMode()) { + case AudioManager.RINGER_MODE_NORMAL: + if (mVibrator.hasVibrator()) { + am.setRingerMode(AudioManager.RINGER_MODE_VIBRATE); + } + break; + case AudioManager.RINGER_MODE_VIBRATE: + am.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + NotificationManager nm = getNotificationService(); + nm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY); + break; + case AudioManager.RINGER_MODE_SILENT: + am.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + break; + } + } } From 3e8fc21ac84a7487fb6d1ec85b59342a8858e023 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 1 Feb 2021 07:17:35 +0530 Subject: [PATCH 0308/1315] Move Swap capacitive buttons to Settings [1/3] Change-Id: I87739219673b7f3d7fdef046b93bb323a9c60a1c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++++++ .../com/android/server/policy/PhoneWindowManager.java | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8082f4015846..ec51404d3791 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7149,6 +7149,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String LOCKSCREEN_SHOW_CARRIER = "lockscreen_show_carrier"; + /** + * Swap capacitive keys + * @hide + */ + public static final String SWAP_CAPACITIVE_KEYS = "swap_capacitive_keys"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 97d551ad829a..dd3580fd377a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -694,6 +694,7 @@ public void onDrawn() { // User defined hw key config boolean mHardwareKeysDisable = false; + boolean mSwapCapacitiveKeys = false; // Tracks user-customisable behavior for certain key events private Action mBackLongPressAction; @@ -1089,6 +1090,9 @@ void observe() { resolver.registerContentObserver(Settings.System.getUriFor( Settings.System.LOCKSCREEN_ENABLE_POWER_MENU), true, this, UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.System.getUriFor( + Settings.System.SWAP_CAPACITIVE_KEYS), false, this, + UserHandle.USER_ALL); updateSettings(); } @@ -3432,6 +3436,13 @@ void updateSettings(Handler handler) { mLineageHardware.set(LineageHardwareManager.FEATURE_KEY_DISABLE, mHardwareKeysDisable); } + if (mLineageHardware.isSupported(LineageHardwareManager.FEATURE_KEY_SWAP)) { + mSwapCapacitiveKeys = Settings.System.getIntForUser(resolver, + Settings.System.SWAP_CAPACITIVE_KEYS, 0, + UserHandle.USER_CURRENT) == 1; + mLineageHardware.set(LineageHardwareManager.FEATURE_KEY_SWAP, mSwapCapacitiveKeys); + } + updateKeyAssignments(); // use screen off timeout setting as the timeout for the lockscreen From 78717c008679a0852afe101e4526f629fd2582e5 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 13 Jul 2023 08:08:05 +0800 Subject: [PATCH 0309/1315] SystemUI: Fix slice view widget padding Change-Id: I73242d10a1378cdc95f05c720386d8c73bbcdf3f Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/keyguard/KeyguardSliceView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java index 10d23e7c4dc2..a1ab0f6248d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java @@ -469,7 +469,7 @@ private void updatePadding() { int iconPadding = (int) mContext.getResources() .getDimension(R.dimen.widget_icon_padding); // orientation is vertical, so add padding to top & bottom - setPadding(!isDate ? iconPadding : 0, padding, 0, hasText ? padding : 0); + setPadding(0, padding, 0, hasText ? padding : 0); setCompoundDrawablePadding(iconPadding); } From 9fd2433d656c3a19124886f7e04f7ed199ef22b6 Mon Sep 17 00:00:00 2001 From: Carlo Savignano Date: Tue, 8 Dec 2015 20:06:26 +0800 Subject: [PATCH 0310/1315] base: Introduce new navigation bar key event source * When generating virtual navigation bar KeyEvent, set a specific source fort it. There is no precise way to recognize if the KeyEvent is coming from KeyButtonView. Another way is passing another flag. Change-Id: I243c9f91afcbf4c430e2d526e8c814a5a7975e51 Signed-off-by: Carlo Savignano Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/view/InputDevice.java | 8 ++++++++ .../navigationbar/views/buttons/KeyButtonView.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index e3d2ad528201..3960099bc82c 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -355,6 +355,14 @@ public final class InputDevice implements Parcelable { */ public static final int SOURCE_SENSOR = 0x04000000 | SOURCE_CLASS_NONE; + /** + * The input source is a specific virtual event sent from navigation bar. + * + * @see com.android.systemui.navigationbar.buttons.KeyButtonView#sendEvent() + * @hide + */ + public static final int SOURCE_NAVIGATION_BAR = 0x06000000 | SOURCE_CLASS_BUTTON; + /** * A special input source constant that is used when filtering input devices * to match devices that provide any type of input source. diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java index 559d75f2c54c..7f021250eccf 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java @@ -450,7 +450,7 @@ private void sendEvent(int action, int flags, long when) { final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, - InputDevice.SOURCE_KEYBOARD); + InputDevice.SOURCE_NAVIGATION_BAR); int displayId = INVALID_DISPLAY; From 46d7bdd279451958169429ec83d9a3714fec73ea Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 22 Feb 2020 02:25:03 -0500 Subject: [PATCH 0311/1315] Switch gesture navbar to new navigation bar key event source - Fixes issue with back gesture not working while using custom key policy Change-Id: Ie10da1b8acfc7e5b87a6e9c107e6666eca1aa0d1 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/navigationbar/gestural/EdgeBackGestureHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 12cb7aeb9b81..375233ccade4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -1486,7 +1486,7 @@ private boolean sendEvent(int action, int code, int flags) { final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, - InputDevice.SOURCE_KEYBOARD); + InputDevice.SOURCE_NAVIGATION_BAR); ev.setDisplayId(mContext.getDisplay().getDisplayId()); return mContext.getSystemService(InputManager.class) From de3c361986f5638c198afcca6ac00bfb7d82bed2 Mon Sep 17 00:00:00 2001 From: Thecrazyskull Date: Tue, 26 Sep 2017 12:44:51 +0000 Subject: [PATCH 0312/1315] base: Introduce Accidental Touch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accidental Touch is a feature useful for many, especially the gamers out there. It prevents any sort of accidental touch on hardware buttons while the touchscreen is in use. This means you won’t experience any unexpected behaviour while playing games, web browsing or even when attempting to reach your finger out to the edge of that humongous display. @neobuddy89: * Also add Assist Key to the list. * Micro-optimize code - Run into key checks only if ANBI enabled Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++ .../android/server/policy/ANBIHandler.java | 61 +++++++++++++++++++ .../server/policy/PhoneWindowManager.java | 31 ++++++++++ 3 files changed, 98 insertions(+) create mode 100644 services/core/java/com/android/server/policy/ANBIHandler.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ec51404d3791..0c8c99d1cbc0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7155,6 +7155,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SWAP_CAPACITIVE_KEYS = "swap_capacitive_keys"; + /** + * Indicates whether ANBI (Accidental navigation button interaction) is enabled. + * @hide + */ + public static final String ANBI_ENABLED = "anbi_enabled"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/policy/ANBIHandler.java b/services/core/java/com/android/server/policy/ANBIHandler.java new file mode 100644 index 000000000000..7f77d62f9234 --- /dev/null +++ b/services/core/java/com/android/server/policy/ANBIHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.policy; + +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.view.WindowManagerPolicyConstants.PointerEventListener; + +public class ANBIHandler implements PointerEventListener { + + private static final String TAG = ANBIHandler.class.getSimpleName(); + private static final boolean DEBUG = false; + + private boolean mScreenTouched; + + private Context mContext; + + public ANBIHandler(Context context) { + mContext = context; + } + + @Override + public void onPointerEvent(MotionEvent event) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_POINTER_DOWN: + mScreenTouched = true; + break; + default: + mScreenTouched = false; + break; + } + if (DEBUG) { + Log.d(TAG, "Screen touched= " + mScreenTouched); + } + } + + public boolean isScreenTouched() { + if (DEBUG) { + Log.d(TAG, "isScreenTouched: mScreenTouched= " + mScreenTouched); + } + return mScreenTouched; + } +} diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index dd3580fd377a..6b3ca24f4c78 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -695,6 +695,8 @@ public void onDrawn() { // User defined hw key config boolean mHardwareKeysDisable = false; boolean mSwapCapacitiveKeys = false; + ANBIHandler mANBIHandler; + private boolean mANBIEnabled; // Tracks user-customisable behavior for certain key events private Action mBackLongPressAction; @@ -1093,6 +1095,9 @@ void observe() { resolver.registerContentObserver(Settings.System.getUriFor( Settings.System.SWAP_CAPACITIVE_KEYS), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.System.getUriFor( + Settings.System.ANBI_ENABLED), false, this, + UserHandle.USER_ALL); updateSettings(); } @@ -2593,6 +2598,8 @@ void init(Injector injector) { mLogger = new MetricsLogger(); mLineageHardware = LineageHardwareManager.getInstance(mContext); + mANBIHandler = new ANBIHandler(mContext); + Resources res = mContext.getResources(); mWakeOnDpadKeyPress = res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress); @@ -3443,6 +3450,17 @@ void updateSettings(Handler handler) { mLineageHardware.set(LineageHardwareManager.FEATURE_KEY_SWAP, mSwapCapacitiveKeys); } + boolean ANBIEnabled = Settings.System.getIntForUser(resolver, + Settings.System.ANBI_ENABLED, 0, UserHandle.USER_CURRENT) == 1; + if (mANBIHandler != null && mANBIEnabled != ANBIEnabled) { + mANBIEnabled = ANBIEnabled; + if (mANBIEnabled) { + mWindowManagerFuncs.registerPointerEventListener(mANBIHandler, DEFAULT_DISPLAY); + } else { + mWindowManagerFuncs.unregisterPointerEventListener(mANBIHandler, DEFAULT_DISPLAY); + } + } + updateKeyAssignments(); // use screen off timeout setting as the timeout for the lockscreen @@ -5034,6 +5052,19 @@ && isWakeKeyWhenScreenOff(keyCode)) { final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0; final boolean longPress = (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0; + if (mANBIEnabled && mANBIHandler != null && mANBIHandler.isScreenTouched()) { + final boolean navBarKey = event.getSource() == InputDevice.SOURCE_NAVIGATION_BAR; + final boolean appSwitchKey = keyCode == KeyEvent.KEYCODE_APP_SWITCH; + final boolean homeKey = keyCode == KeyEvent.KEYCODE_HOME; + final boolean menuKey = keyCode == KeyEvent.KEYCODE_MENU; + final boolean backKey = keyCode == KeyEvent.KEYCODE_BACK; + final boolean assistKey = keyCode == KeyEvent.KEYCODE_ASSIST; + + if (!navBarKey && (appSwitchKey || homeKey || menuKey || backKey || assistKey)) { + return 0; + } + } + // If screen is off then we treat the case where the keyguard is open but hidden // the same as if it were open and in front. // This will prevent any keys other than the power button from waking the screen From 0546da5f1ac22cc934931fbef995905068892fa4 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 31 Jul 2024 00:42:34 +0530 Subject: [PATCH 0313/1315] PhoneWindowManager: Check NPE for LineageHardware * LineageHardware is initialized in systemReady() * If user is switched before systemReady(), mMultiuserReceiver will invoke mSettingsObserver.onChange(false); resulting in below crash 07-30 11:44:41.772 E/AndroidRuntime( 2056): *** FATAL EXCEPTION IN SYSTEM PROCESS: main 07-30 11:44:41.772 E/AndroidRuntime( 2056): java.lang.RuntimeException: Error receiving broadcast Intent { act=android.intent.action.USER_SWITCHED flg=0x50000010 (has extras) } in com.android.server.policy.PhoneWindowManager$17@e24db87 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1818) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.app.LoadedApk$ReceiverDispatcher$Args.$r8$lambda$mcNAAl1SQ4MyJPyDg8TJ2x2h0Rk(Unknown Source:0) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.app.LoadedApk$ReceiverDispatcher$Args$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.os.Handler.handleCallback(Handler.java:959) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.os.Handler.dispatchMessage(Handler.java:100) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.os.Looper.loopOnce(Looper.java:232) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.os.Looper.loop(Looper.java:317) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.SystemServer.run(SystemServer.java:1030) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.SystemServer.main(SystemServer.java:714) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at java.lang.reflect.Method.invoke(Native Method) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:583) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:856) 07-30 11:44:41.772 E/AndroidRuntime( 2056): Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean lineageos.hardware.LineageHardwareManager.isSupported(int)' on a null object reference 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.policy.PhoneWindowManager.updateSettings(PhoneWindowManager.java:3477) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.policy.PhoneWindowManager.updateSettings(PhoneWindowManager.java:3368) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.policy.PhoneWindowManager.-$$Nest$mupdateSettings(PhoneWindowManager.java:0) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.policy.PhoneWindowManager$SettingsObserver.onChange(PhoneWindowManager.java:1136) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at com.android.server.policy.PhoneWindowManager$17.onReceive(PhoneWindowManager.java:6323) 07-30 11:44:41.772 E/AndroidRuntime( 2056): at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1810) 07-30 11:44:41.772 E/AndroidRuntime( 2056): ... 11 more * We either guard settingsObserver or LineageHardware. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/policy/PhoneWindowManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 6b3ca24f4c78..02057e583969 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3436,14 +3436,14 @@ void updateSettings(Handler handler) { updateWakeGestureListenerLp(); } - if (mLineageHardware.isSupported(LineageHardwareManager.FEATURE_KEY_DISABLE)) { + if (mLineageHardware != null && mLineageHardware.isSupported(LineageHardwareManager.FEATURE_KEY_DISABLE)) { mHardwareKeysDisable = Settings.System.getIntForUser(resolver, Settings.System.HARDWARE_KEYS_DISABLE, 0, UserHandle.USER_CURRENT) == 1; mLineageHardware.set(LineageHardwareManager.FEATURE_KEY_DISABLE, mHardwareKeysDisable); } - if (mLineageHardware.isSupported(LineageHardwareManager.FEATURE_KEY_SWAP)) { + if (mLineageHardware != null && mLineageHardware.isSupported(LineageHardwareManager.FEATURE_KEY_SWAP)) { mSwapCapacitiveKeys = Settings.System.getIntForUser(resolver, Settings.System.SWAP_CAPACITIVE_KEYS, 0, UserHandle.USER_CURRENT) == 1; From 10caf3b7994dd5fe0f94cc16a70e5b22e14dafca Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 21 Aug 2024 09:49:41 +0530 Subject: [PATCH 0314/1315] PhoneWindowManager: Prevent NPE with voice search action Log: 08-21 09:37:42.335 6607 6630 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: android.ui 08-21 09:37:42.335 6607 6630 E AndroidRuntime: java.lang.RuntimeException: WakeLock under-locked PhoneWindowManager.mBroadcastWakeLock 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.PowerManager$WakeLock.release(PowerManager.java:3949) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.PowerManager$WakeLock.release(PowerManager.java:3910) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.PhoneWindowManager.launchVoiceAssistWithWakeLock(PhoneWindowManager.java:6288) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.PhoneWindowManager.performKeyAction(PhoneWindowManager.java:2294) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.PhoneWindowManager.performKeyAction(PhoneWindowManager.java:2274) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.PhoneWindowManager.-$$Nest$mperformKeyAction(PhoneWindowManager.java:0) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.PhoneWindowManager$17.onSwipeThreeFinger(PhoneWindowManager.java:6971) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.policy.ThreeFingerSwipeListener.onPointerEvent(ThreeFingerSwipeListener.java:90) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.wm.PointerEventDispatcher.onInputEvent(PointerEventDispatcher.java:53) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:295) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:259) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.view.InputEventReceiver.onBatchedInputEventPending(InputEventReceiver.java:203) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.MessageQueue.nativePollOnce(Native Method) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.MessageQueue.next(MessageQueue.java:349) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:189) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:85) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.ServiceThread.run(ServiceThread.java:46) 08-21 09:37:42.335 6607 6630 E AndroidRuntime: at com.android.server.UiThread.run(UiThread.java:45) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/policy/PhoneWindowManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 02057e583969..e9fe0bba7214 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2298,7 +2298,7 @@ private void performKeyAction(Action action, KeyEvent event, int assistInvocatio notifyKeyGestureCompleted(event, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT); break; case VOICE_SEARCH: - launchVoiceAssistWithWakeLock(); + launchVoiceAssist(mAllowStartActivityForLongPressOnPowerDuringSetup); break; case IN_APP_SEARCH: triggerVirtualKeypress(KeyEvent.KEYCODE_SEARCH); From eef5f4c74a7ea1e716b027a92a0d96beb2adc8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Ti=C3=AAn=20Sinh?= Date: Mon, 6 Feb 2023 12:49:29 +0800 Subject: [PATCH 0315/1315] SystemUI: Add restart SystemUI in Advanced Reboot [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_restart_systemui.xml | 50 +++++++++++++++++++ .../SystemUI/res/values/matrixx_strings.xml | 3 ++ .../GlobalActionsDialogLite.java | 30 +++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/SystemUI/res/drawable/ic_restart_systemui.xml diff --git a/packages/SystemUI/res/drawable/ic_restart_systemui.xml b/packages/SystemUI/res/drawable/ic_restart_systemui.xml new file mode 100644 index 000000000000..65065705b195 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_restart_systemui.xml @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 108c14a6d6ab..23ebd1c36024 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -63,4 +63,7 @@ QR code EEEMMMd View + + + SystemUI diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 26a70924e07c..ba443ff44090 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -72,6 +72,7 @@ import android.os.IBinder; import android.os.Message; import android.os.PowerManager; +import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; @@ -213,6 +214,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private static final String RESTART_ACTION_KEY_RESTART_BOOTLOADER = "restart_bootloader"; private static final String RESTART_ACTION_KEY_RESTART_DOWNLOAD = "restart_download"; private static final String RESTART_ACTION_KEY_RESTART_FASTBOOT = "restart_fastboot"; + private static final String RESTART_ACTION_KEY_RESTART_SYSTEMUI = "restart_systemui"; // See NotificationManagerService#scheduleDurationReachedLocked private static final long TOAST_FADE_TIME = 333; @@ -724,6 +726,7 @@ protected void createActionItems() { RestartBootloaderAction blAction = new RestartBootloaderAction(); RestartDownloadAction dlAction = new RestartDownloadAction(); RestartFastbootAction fbAction = new RestartFastbootAction(); + RestartSystemUIAction sysuiAction = new RestartSystemUIAction(); ArraySet addedKeys = new ArraySet<>(); ArraySet addedRestartKeys = new ArraySet(); List tempActions = new ArrayList<>(); @@ -819,6 +822,8 @@ protected void createActionItems() { addIfShouldShowAction(mRestartItems, dlAction); } else if (RESTART_ACTION_KEY_RESTART_FASTBOOT.equals(actionKey)) { addIfShouldShowAction(mRestartItems, fbAction); + } else if (RESTART_ACTION_KEY_RESTART_SYSTEMUI.equals(actionKey)) { + addIfShouldShowAction(mRestartItems, sysuiAction); } // Add here so we don't add more than one. addedRestartKeys.add(actionKey); @@ -1342,6 +1347,31 @@ public void onPress() { } } + private final class RestartSystemUIAction extends SinglePressAction { + private RestartSystemUIAction() { + super(com.android.systemui.res.R.drawable.ic_restart_systemui, com.android.systemui.res.R.string.global_action_restart_systemui); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + + @Override + public void onPress() { + // No time and need to dismiss the dialog here, just kill systemui straight after telling to + // policy/GlobalActions that we hid the dialog within the kill action itself so its onStatusBarConnectedChanged + // won't show the LegacyGlobalActions after systemui restart + mWindowManagerFuncs.onGlobalActionsHidden(); + Process.killProcess(Process.myPid()); + } + } + @VisibleForTesting class ScreenshotAction extends SinglePressAction implements LongPressAction { ScreenshotAction() { From 24ed87885f78995b20291638f2c363e684cea9f3 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 21 Aug 2024 00:28:48 +0530 Subject: [PATCH 0316/1315] Add three fingers swipe actions [2/3] * Inspired by: https://github.com/RisingTechOSS/android_frameworks_base/commit/02780e27862edfa7c0b957073e9f4e1e4da2fb7d * Listener originally authored by: Henrique Silva Co-authored-by: Henrique Silva Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/IActivityManager.aidl | 6 + core/java/android/provider/Settings.java | 8 + core/java/android/view/ViewRootImpl.java | 14 ++ .../server/am/ActivityManagerService.java | 19 +++ .../server/policy/PhoneWindowManager.java | 41 +++++ .../policy/ThreeFingersSwipeListener.java | 144 ++++++++++++++++++ 6 files changed, 232 insertions(+) create mode 100644 services/core/java/com/android/server/policy/ThreeFingersSwipeListener.java diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 7a6ace796b4b..4cbf22506524 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1056,4 +1056,10 @@ interface IActivityManager { */ oneway void reportOptimizationInfo(in IBinder app, in String compilerFilter, in String compilationReason); + + /** + * Should disable touch if three fingers swipe enabled + */ + boolean isThreeFingersSwipeActive(); + void setThreeFingersSwipeActive(boolean active); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0c8c99d1cbc0..3e95481bd786 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7161,6 +7161,14 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String ANBI_ENABLED = "anbi_enabled"; + /** + * Whether three fingers swipe is active + * 0 = Inactive, 1 = Active + * @hide + */ + @Readable + public static final String THREE_FINGER_GESTURE_ACTIVE = "three_fingers_swipe_active"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index fa1c190eb4e2..2d667e19b403 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -8011,6 +8011,11 @@ private int processMotionEvent(QueuedInputEvent q) { private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; + if (event.getPointerCount() == 3 && isThreeFingersSwipeActive()) { + event.setAction(MotionEvent.ACTION_CANCEL); + Log.d(mTag, "canceling motionEvent because of threeGesture detecting"); + } + // Translate the pointer event for compatibility, if needed. if (mTranslator != null) { mTranslator.translateEventInScreenToAppWindow(event); @@ -13824,4 +13829,13 @@ private void preInitBufferAllocator() { public Choreographer getChoreographer() { return mChoreographer; } + + private boolean isThreeFingersSwipeActive() { + try { + return ActivityManager.getService().isThreeFingersSwipeActive(); + } catch (RemoteException e) { + Log.e(mTag, "isThreeFingersSwipeActive exception", e); + return false; + } + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 785f97521153..2cfc38edf715 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1623,6 +1623,9 @@ public void binderDied() { static final HostingRecord sNullHostingRecord = new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY); + + private boolean mThreeFingersSwipeEnabled; + /** * Used to notify activity lifecycle events. */ @@ -19943,4 +19946,20 @@ public void reportOptimizationInfo(@NonNull IBinder app, @NonNull String compile } r.getWindowProcessController().setOptimizationInfo(compilerFilter, compilationReason); } + + @Override + public boolean isThreeFingersSwipeActive() { + if (!mThreeFingersSwipeEnabled) + return false; + synchronized (this) { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.THREE_FINGER_GESTURE_ACTIVE, 0, + UserHandle.USER_CURRENT) == 1; + } + } + + @Override + public void setThreeFingersSwipeActive(boolean active) { + mThreeFingersSwipeEnabled = active; + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index e9fe0bba7214..695081d0edb8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -709,6 +709,7 @@ public void onDrawn() { private Action mAppSwitchPressAction; private Action mAppSwitchLongPressAction; private Action mEdgeLongSwipeAction; + private Action mThreeFingersSwipeAction; // support for activating the lock screen while the screen is on private HashSet mAllowLockscreenWhenOnDisplays = new HashSet<>(); @@ -842,6 +843,9 @@ public void onDrawn() { private ScreenshotHelper mScreenshotHelper; + private ThreeFingersSwipeListener mThreeFingersSwipe; + private boolean mThreeFingersSwipeHasAction; + private class PolicyHandler extends Handler { private PolicyHandler(Looper looper) { @@ -1062,6 +1066,9 @@ void observe() { resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.KEY_EDGE_LONG_SWIPE_ACTION), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(LineageSettings.System.getUriFor( + LineageSettings.System.KEY_THREE_FINGERS_SWIPE_ACTION), false, this, + UserHandle.USER_ALL); resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.CAMERA_WAKE_SCREEN), false, this, UserHandle.USER_ALL); @@ -3333,6 +3340,26 @@ private void updateKeyAssignments() { LineageSettings.System.KEY_EDGE_LONG_SWIPE_ACTION, mEdgeLongSwipeAction); + Action threeFingersSwipeAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_THREE_FINGERS_SWIPE_ACTION, + Action.NOTHING); + + if (mThreeFingersSwipe != null && mThreeFingersSwipeAction != threeFingersSwipeAction) { + mThreeFingersSwipeAction = threeFingersSwipeAction; + if (mThreeFingersSwipeAction != Action.NOTHING && !mThreeFingersSwipeHasAction) { + mThreeFingersSwipeHasAction = true; + mWindowManagerFuncs.registerPointerEventListener(mThreeFingersSwipe, DEFAULT_DISPLAY); + } else if (mThreeFingersSwipeAction == Action.NOTHING && mThreeFingersSwipeHasAction) { + mWindowManagerFuncs.unregisterPointerEventListener(mThreeFingersSwipe, DEFAULT_DISPLAY); + mThreeFingersSwipeHasAction = false; + } + try { + mActivityManagerService.setThreeFingersSwipeActive(mThreeFingersSwipeHasAction); + } catch (Exception e) { + // Do nothing + } + } + mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_NOTHING; if (mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_PICTURE_IN_PICTURE; @@ -6796,6 +6823,20 @@ public void systemReady() { mDefaultDisplayPolicy.setDockMode(dockMode); } + mThreeFingersSwipe = new ThreeFingersSwipeListener(mContext, new ThreeFingersSwipeListener.Callbacks() { + @Override + public void onSwipeThreeFingers() { + if (mThreeFingersSwipeAction == Action.NOTHING) + return; + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SYSRQ, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_TOUCHSCREEN); + performKeyAction(mThreeFingersSwipeAction, event); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Three Fingers Swipe"); + } + }); + // Ensure observe happens in systemReady() since we need // LineageHardwareService to be up and running mSettingsObserver.observe(); diff --git a/services/core/java/com/android/server/policy/ThreeFingersSwipeListener.java b/services/core/java/com/android/server/policy/ThreeFingersSwipeListener.java new file mode 100644 index 000000000000..2f1266271222 --- /dev/null +++ b/services/core/java/com/android/server/policy/ThreeFingersSwipeListener.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 The PixelExperience Project + * Copyright (C) 2024-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.policy; + +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.WindowManagerPolicyConstants.PointerEventListener; + +public class ThreeFingersSwipeListener implements PointerEventListener { + + private static final String TAG = "ThreeFingersSwipeListener"; + private static final int THREE_GESTURE_STATE_NONE = 0; + private static final int THREE_GESTURE_STATE_DETECTING = 1; + private static final int THREE_GESTURE_STATE_DETECTED_FALSE = 2; + private static final int THREE_GESTURE_STATE_DETECTED_TRUE = 3; + private static final int THREE_GESTURE_STATE_NO_DETECT = 4; + private float[] mInitMotionY; + private int[] mPointerIds; + private final Context mContext; + private int mThreeGestureState = THREE_GESTURE_STATE_NONE; + private int mThreeGestureThreshold; + private int mThreshold; + private float mDensity; + private int mScreenHeight; + private int mScreenWidth; + private final Callbacks mCallbacks; + + public ThreeFingersSwipeListener(Context context, Callbacks callbacks) { + mPointerIds = new int[3]; + mInitMotionY = new float[3]; + mCallbacks = callbacks; + mContext = context; + DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + mDensity = displayMetrics.density; + mThreshold = (int) (50.0f * mDensity); + mThreeGestureThreshold = mThreshold * 3; + mScreenHeight = displayMetrics.heightPixels; + mScreenWidth = displayMetrics.widthPixels; + + // reset the setting flag on init + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.THREE_FINGER_GESTURE_ACTIVE, 0, + UserHandle.USER_CURRENT); + } + + @Override + public void onPointerEvent(MotionEvent event) { + if (event.getAction() == 0) { + changeThreeGestureState(THREE_GESTURE_STATE_NONE); + } else if (mThreeGestureState == THREE_GESTURE_STATE_NONE && event.getPointerCount() == 3) { + if (checkIsStartThreeGesture(event)) { + changeThreeGestureState(THREE_GESTURE_STATE_DETECTING); + for (int i = 0; i < 3; i++) { + mPointerIds[i] = event.getPointerId(i); + mInitMotionY[i] = event.getY(i); + } + } else { + changeThreeGestureState(THREE_GESTURE_STATE_NO_DETECT); + } + } + if (mThreeGestureState == THREE_GESTURE_STATE_DETECTING) { + if (event.getPointerCount() != 3) { + changeThreeGestureState(THREE_GESTURE_STATE_DETECTED_FALSE); + return; + } + if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { + float distance = 0.0f; + int i = 0; + while (i < 3) { + int index = event.findPointerIndex(mPointerIds[i]); + if (index < 0 || index >= 3) { + changeThreeGestureState(THREE_GESTURE_STATE_DETECTED_FALSE); + return; + } else { + distance += event.getY(index) - mInitMotionY[i]; + i++; + } + } + if (distance >= ((float) mThreeGestureThreshold)) { + changeThreeGestureState(THREE_GESTURE_STATE_DETECTED_TRUE); + mCallbacks.onSwipeThreeFingers(); + } + } + } + } + + private void changeThreeGestureState(int state) { + if (mThreeGestureState != state){ + mThreeGestureState = state; + boolean shouldEnableFlag = mThreeGestureState == THREE_GESTURE_STATE_DETECTED_TRUE || + mThreeGestureState == THREE_GESTURE_STATE_DETECTING; + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.THREE_FINGER_GESTURE_ACTIVE, shouldEnableFlag ? 1 : 0, + UserHandle.USER_CURRENT); + } + } + + private boolean checkIsStartThreeGesture(MotionEvent event) { + if (event.getEventTime() - event.getDownTime() > 500) { + return false; + } + float minX = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE; + float minY = Float.MAX_VALUE; + float maxY = Float.MIN_VALUE; + for (int i = 0; i < event.getPointerCount(); i++) { + float x = event.getX(i); + float y = event.getY(i); + if (y > ((float) (mScreenHeight - mThreshold))) { + return false; + } + maxX = Math.max(maxX, x); + minX = Math.min(minX, x); + maxY = Math.max(maxY, y); + minY = Math.min(minY, y); + } + if (maxY - minY <= mDensity * 150.0f) { + return maxX - minX <= ((float) (mScreenWidth < mScreenHeight ? mScreenWidth : mScreenHeight)); + } + return false; + } + + interface Callbacks { + void onSwipeThreeFingers(); + } +} From 1e430569d5198f0612854f20050e3fb18c0afac0 Mon Sep 17 00:00:00 2001 From: Praditia Nur Date: Wed, 4 Sep 2024 23:34:23 +0700 Subject: [PATCH 0317/1315] base: Allow to customize bottom corner swipe up action [2/4] Change-Id: I6c7660017947446ad2b803ca9febb56f5d977cf6 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/policy/PhoneWindowManager.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 695081d0edb8..961b690329be 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -708,6 +708,7 @@ public void onDrawn() { private Action mAssistLongPressAction; private Action mAppSwitchPressAction; private Action mAppSwitchLongPressAction; + private Action mCornerLongSwipeAction; private Action mEdgeLongSwipeAction; private Action mThreeFingersSwipeAction; @@ -1063,6 +1064,9 @@ void observe() { resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.KEY_APP_SWITCH_LONG_PRESS_ACTION), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(LineageSettings.System.getUriFor( + LineageSettings.System.KEY_CORNER_LONG_SWIPE_ACTION), false, this, + UserHandle.USER_ALL); resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.KEY_EDGE_LONG_SWIPE_ACTION), false, this, UserHandle.USER_ALL); @@ -3298,6 +3302,7 @@ private void updateKeyAssignments() { mAppSwitchPressAction = Action.APP_SWITCH; mAppSwitchLongPressAction = Action.fromIntSafe(res.getInteger( org.lineageos.platform.internal.R.integer.config_longPressOnAppSwitchBehavior)); + mCornerLongSwipeAction = Action.SEARCH; mEdgeLongSwipeAction = Action.NOTHING; mBackLongPressAction = Action.fromSettings(resolver, @@ -3336,6 +3341,10 @@ private void updateKeyAssignments() { LineageSettings.System.KEY_APP_SWITCH_LONG_PRESS_ACTION, mAppSwitchLongPressAction); + mCornerLongSwipeAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_CORNER_LONG_SWIPE_ACTION, + mCornerLongSwipeAction); + mEdgeLongSwipeAction = Action.fromSettings(resolver, LineageSettings.System.KEY_EDGE_LONG_SWIPE_ACTION, mEdgeLongSwipeAction); @@ -5348,6 +5357,23 @@ && isWakeKeyWhenScreenOff(keyCode)) { } case KeyEvent.KEYCODE_HOME: + boolean isLongSwipe = (event.getFlags() & KeyEvent.FLAG_LONG_SWIPE) != 0; + if (mLongSwipeDown && isLongSwipe && !down) { + // Trigger long swipe action + performKeyAction(mCornerLongSwipeAction, event); + // Reset long swipe state + mLongSwipeDown = false; + // Don't pass back press to app + result &= ~ACTION_PASS_TO_USER; + break; + } + mLongSwipeDown = isLongSwipe && down; + if (mLongSwipeDown) { + // Don't pass back press to app + result &= ~ACTION_PASS_TO_USER; + break; + } + if (down && !interactive) { isWakeKey = mWakeOnHomeKeyPress; if (!isWakeKey) { From 2b663754f0998751d46390fb3c8d9745ecbdad5e Mon Sep 17 00:00:00 2001 From: Elluzion Date: Wed, 7 Sep 2022 20:04:30 +0300 Subject: [PATCH 0318/1315] SystemUI: Blur the power menu Squashed: From: Pranav Vashi Date: Tue, 31 Jan 2023 07:21:15 +0530 Subject: SystemUI: Also blur power sub-menus Signed-off-by: Pranav Vashi From: Dhina17 Date: Tue, 5 Sep 2023 17:04:49 +0530 Subject: SystemUI: Enable power menu blur via window flags Enabling blur behind via style attr causes an unexpected behaviour that power menu always has the blur behind even when the device disables the systemui blur. [persist.sysui.disableBlur=1] GlobalActionsDialog is a part of SystemUI so it should follow BlurUtils#supportBlursOnWindows not only Settings.Global.DISABLE_WINDOW_BLURS. With style attr, it only followed Settings.Global.DISABLE_WINDOW_BLURS, that's why worked fine with blur toggle in targets which don't disable systemui blur. Change-Id: If6aa97ce3df7f12e181274d85a05531fcb575ec5 [neobuddy89: Adapted for our code.] Signed-off-by: Pranav Vashi [@neobuddy89: Adapted for A16.] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../GlobalActionsDialogLite.java | 39 +++++++++++++++---- .../GlobalActionsPowerDialog.java | 21 +++++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index ba443ff44090..dcd08c098f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -156,6 +156,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; +import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -305,6 +306,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final PowerManager mPowerManager; private int mGlobalActionDialogTimeout; private final Handler mHandler; + private final BlurUtils mBlurUtils; private final UserTracker.Callback mOnUserSwitched = new UserTracker.Callback() { @Override @@ -440,7 +442,8 @@ public GlobalActionsDialogLite( GlobalActionsInteractor interactor, ControlsComponent controlsComponent, Lazy displayWindowPropertiesRepository, - PowerManager powerManager) { + PowerManager powerManager, + BlurUtils blurUtils) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -480,6 +483,7 @@ public GlobalActionsDialogLite( mInteractor = interactor; mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository; mPowerManager = powerManager; + mBlurUtils = blurUtils; mHandler = new Handler(mMainHandler.getLooper()) { public void handleMessage(Message msg) { @@ -923,7 +927,8 @@ protected ActionsDialogLite createDialog(int displayId) { mShadeController, mKeyguardUpdateMonitor, mLockPatternUtils, - mSelectedUserInteractor) { + mSelectedUserInteractor, + mBlurUtils) { @Override public boolean dispatchTouchEvent(MotionEvent event) { rescheduleBurninTimeout(mGlobalActionDialogTimeout); @@ -2923,7 +2928,6 @@ static class ActionsDialogLite extends SystemUIDialog implements DialogInterface protected Drawable mBackgroundDrawable; protected final SysuiColorExtractor mColorExtractor; private boolean mKeyguardShowing; - protected float mScrimAlpha; protected final LightBarController mLightBarController; private final KeyguardStateController mKeyguardStateController; protected final TopUiController mTopUiController; @@ -2940,6 +2944,7 @@ static class ActionsDialogLite extends SystemUIDialog implements DialogInterface private SelectedUserInteractor mSelectedUserInteractor; private LockPatternUtils mLockPatternUtils; private float mWindowDimAmount; + private BlurUtils mBlurUtils; protected ViewGroup mContainer; @@ -3023,7 +3028,8 @@ void setBackDispatcherOverride(OnBackInvokedDispatcher mockDispatcher) { ShadeController shadeController, KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, + BlurUtils blurUtils) { // We set dismissOnDeviceLock to false because we have a custom broadcast receiver to // dismiss this dialog when the device is locked. super(context, themeRes, false /* dismissOnDeviceLock */); @@ -3047,6 +3053,7 @@ void setBackDispatcherOverride(OnBackInvokedDispatcher mockDispatcher) { mLockPatternUtils = lockPatternUtils; mGestureDetector = new GestureDetector(mContext, mGestureListener); mSelectedUserInteractor = selectedUserInteractor; + mBlurUtils = blurUtils; } @Override @@ -3119,13 +3126,14 @@ private ListPopupWindow createPowerOverflowPopup() { } public void showPowerOptionsMenu() { - mPowerOptionsDialog = GlobalActionsPowerDialog.create(mContext, mPowerOptionsAdapter); + mPowerOptionsDialog = GlobalActionsPowerDialog.create(mContext, + mPowerOptionsAdapter, mBlurUtils); mPowerOptionsDialog.show(); } public void showRestartOptionsMenu() { mRestartOptionsDialog = GlobalActionsPowerDialog.create(mContext, - mRestartOptionsAdapter); + mRestartOptionsAdapter, mBlurUtils); mRestartOptionsDialog.show(); } @@ -3135,7 +3143,8 @@ protected void showPowerOverflowMenu() { } public void showUsersMenu() { - mUsersDialog = GlobalActionsPowerDialog.create(mContext, mUsersAdapter); + mUsersDialog = GlobalActionsPowerDialog.create(mContext, + mUsersAdapter, mBlurUtils); mUsersDialog.show(); } @@ -3187,7 +3196,21 @@ public boolean dispatchPopulateAccessibilityEvent( if (mBackgroundDrawable == null) { mBackgroundDrawable = new ScrimDrawable(); - mScrimAlpha = 1.0f; + } + + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + if (mBlurUtils.supportsBlursOnWindows()) { + // Enable blur behind + // Enable dim behind since we are setting some amount dim for the blur. + window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + // Set blur behind radius + int blurBehindRadius = mContext.getResources() + .getDimensionPixelSize(com.android.systemui.res.R.dimen.max_window_blur_radius); + window.getAttributes().setBlurBehindRadius(blurBehindRadius); + window.setDimAmount(0.54f); + } else { + window.setDimAmount(0.88f); } if (QsInCompose.isEnabled()) { View v = findViewById(R.id.list); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPowerDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPowerDialog.java index 6d0b6c68feb1..f66a2120b65a 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPowerDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPowerDialog.java @@ -19,6 +19,7 @@ import android.app.Dialog; import android.content.Context; import android.content.res.Resources; +import android.view.CrossWindowBlurListeners; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,8 @@ import android.view.WindowManager; import android.widget.ListAdapter; +import com.android.systemui.statusbar.BlurUtils; + import androidx.constraintlayout.helper.widget.Flow; /** @@ -36,7 +39,7 @@ public class GlobalActionsPowerDialog { /** * Create a dialog for displaying Shut Down and Restart actions. */ - public static Dialog create(@NonNull Context context, ListAdapter adapter) { + public static Dialog create(@NonNull Context context, ListAdapter adapter, BlurUtils blurUtils) { ViewGroup listView = (ViewGroup) LayoutInflater.from(context).inflate( com.android.systemui.res.R.layout.global_actions_power_dialog_flow, null); @@ -62,7 +65,8 @@ public static Dialog create(@NonNull Context context, ListAdapter adapter) { } flow.setMaxElementsWrap(nElementsWrap); - Dialog dialog = new Dialog(context); + Dialog dialog = new Dialog(context, + com.android.systemui.res.R.style.Theme_SystemUI_Dialog_GlobalActionsLite); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setContentView(listView); @@ -73,6 +77,19 @@ public static Dialog create(@NonNull Context context, ListAdapter adapter) { com.android.systemui.res.R.drawable.global_actions_lite_background, context.getTheme())); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + if (blurUtils.supportsBlursOnWindows()) { + // Enable blur behind + // Enable dim behind since we are setting some amount dim for the blur. + window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + // Set blur behind radius + int blurBehindRadius = context.getResources() + .getDimensionPixelSize(com.android.systemui.res.R.dimen.max_window_blur_radius); + window.getAttributes().setBlurBehindRadius(blurBehindRadius); + window.setDimAmount(0.54f); + } else { + window.setDimAmount(0.88f); + } return dialog; } From d2acf760892376c8666b6d6ec86a659c0a370acb Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 24 Nov 2024 11:52:16 +0530 Subject: [PATCH 0319/1315] SystemUI: Sync power menu and restart menu layout * Landscape mode for restart menu still to be fixed for elements > 4. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../globalactions/GlobalActionsDialogLite.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index dcd08c098f5d..c8a224e43048 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -18,7 +18,6 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; @@ -628,13 +627,6 @@ protected void handleShow(@Nullable Expandable expandable, int displayId) { mDialog = createDialog(displayId); prepareDialog(); - WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); - attrs.setTitle("GlobalActionsDialogLite"); - attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mDialog.getWindow().setAttributes(attrs); - // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports - mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); - DialogTransitionAnimator.Controller controller = expandable != null ? expandable.dialogTransitionController( new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, @@ -3059,8 +3051,6 @@ void setBackDispatcherOverride(OnBackInvokedDispatcher mockDispatcher) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getWindow().setTitle(getContext().getString( - com.android.systemui.res.R.string.accessibility_quick_settings_power_menu)); initializeLayout(); mWindowDimAmount = getWindow().getAttributes().dimAmount; getOnBackInvokedDispatcher().registerOnBackInvokedCallback( @@ -3199,6 +3189,9 @@ public boolean dispatchPopulateAccessibilityEvent( } Window window = getWindow(); + window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + window.setTitle(""); // prevent Talkback from speaking first item name twice + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); if (mBlurUtils.supportsBlursOnWindows()) { // Enable blur behind From a4847e795be0df17e5ac5eec0ff1bb58b0bee518 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Sat, 6 May 2023 16:13:00 +0800 Subject: [PATCH 0320/1315] SystemUI: Remove power menu shadow This fits better with Material You, especially when the default shadow gets clipped at the edges. Change-Id: If1f421692bcc737882f76fc7929b9f9dd0dd71f0 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/dimens.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index e848095e7013..8a5f75f03543 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1120,7 +1120,7 @@ 18dp - 9dp + 0dp 330px From 5cf650d787ea9b99e540952e29b5f3676e8ebae6 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Thu, 3 Jul 2025 18:41:41 -0700 Subject: [PATCH 0321/1315] SystemUI: clear keyguard indication background and icon on empty text Fixes an issue where the End session button would show up on the owner profile but with no text (just an empty colored shape). Test: atest SystemUITests:KeyguardIndicationRotateTextViewControllerTest SystemUITests:KeyguardIndicationTest (note that these tests seem to just mock the KeyguardIndicationRotateTextView, so the code doesn't actually get called) Test: atest SystemUITests:com.android.systemui.keyguard Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../phone/KeyguardIndicationTextView.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java index 02ceb29e6239..f3dcdea95af7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java @@ -85,6 +85,7 @@ public void clearMessages() { } mMessage = ""; setText(""); + clearBackgroundAndIcon(); } /** @@ -247,6 +248,12 @@ private void setNextIndication() { setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); forceAssertiveAccessibilityLiveRegion = mKeyguardIndicationInfo.getForceAssertiveAccessibilityLiveRegion(); + } else { + // null mKeyguardIndicationInfo indicates a hideIndication call or INDICATION_TYPE_NONE + // being used. When this happens, upstream currently only removes the text via the + // setText(mMessage) call below (mMessage will be null whenever mKeyguardIndicationInfo + // is null), but they don't remove the background. + clearBackgroundAndIcon(); } if (!forceAssertiveAccessibilityLiveRegion) { setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE); @@ -257,6 +264,15 @@ private void setNextIndication() { } } + private void clearBackgroundAndIcon() { + // setNextIndication will set everything again on a new mKeyguardIndicationInfo, so it + // should be fine to do this. Note that AOSP doesn't use an icon anywhere yet + setBackground(null); + setOnClickListener(null); + setClickable(false); + setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); + } + private AnimatorSet getInAnimator() { AnimatorSet animatorSet = new AnimatorSet(); ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1f); From 564175ec4f1c5c670d967cffdd942e1ec92d5404 Mon Sep 17 00:00:00 2001 From: Alexander Martinz Date: Wed, 9 Sep 2015 18:07:01 -0700 Subject: [PATCH 0322/1315] SystemUI: On-The-Go Mode (1/2) @neobuddy89: Updated for A14 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 26 ++ .../internal/util/matrixx/OnTheGoUtils.java | 106 +++++ packages/SystemUI/AndroidManifest.xml | 5 + .../SystemUI/res/drawable/ic_lock_onthego.xml | 15 + .../layout/quick_settings_onthego_dialog.xml | 85 ++++ packages/SystemUI/res/values/cr_config.xml | 17 + .../SystemUI/res/values/matrixx_strings.xml | 14 + .../crdroid/onthego/OnTheGoDialog.java | 172 ++++++++ .../crdroid/onthego/OnTheGoService.java | 400 ++++++++++++++++++ .../GlobalActionsDialogLite.java | 35 ++ 10 files changed, 875 insertions(+) create mode 100644 core/java/com/android/internal/util/matrixx/OnTheGoUtils.java create mode 100644 packages/SystemUI/res/drawable/ic_lock_onthego.xml create mode 100644 packages/SystemUI/res/layout/quick_settings_onthego_dialog.xml create mode 100644 packages/SystemUI/res/values/cr_config.xml create mode 100644 packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoDialog.java create mode 100644 packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3e95481bd786..0c8fa12178cc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7169,6 +7169,32 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String THREE_FINGER_GESTURE_ACTIVE = "three_fingers_swipe_active"; + /** + * If On-The-Go should be displayed at the power menu. + * @hide + */ + public static final String GLOBAL_ACTIONS_ONTHEGO = "global_actions_onthego"; + + /** + * The alpha value of the On-The-Go overlay. + * @hide + */ + public static final String ON_THE_GO_ALPHA = "on_the_go_alpha"; + + /** + * Whether the service should restart itself or not. + * @hide + */ + public static final String ON_THE_GO_SERVICE_RESTART = "on_the_go_service_restart"; + + /** + * The camera instance to use. + * 0 = Rear Camera + * 1 = Front Camera + * @hide + */ + public static final String ON_THE_GO_CAMERA = "on_the_go_camera"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/com/android/internal/util/matrixx/OnTheGoUtils.java b/core/java/com/android/internal/util/matrixx/OnTheGoUtils.java new file mode 100644 index 000000000000..0263b880a74d --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/OnTheGoUtils.java @@ -0,0 +1,106 @@ +/* +* +*/ + +package com.android.internal.util.matrixx; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import java.util.List; + +public class OnTheGoUtils { + + private static final String TAG = "OnTheGoUtils"; + + /** + * Checks if a specific package is installed. + * + * @param context The context to retrieve the package manager + * @param packageName The name of the package + * @return Whether the package is installed or not. + */ + public static boolean isPackageInstalled(Context context, String packageName) { + PackageManager pm = context.getPackageManager(); + try { + if (pm != null) { + List packages = pm.getInstalledApplications(0); + for (ApplicationInfo packageInfo : packages) { + if (packageInfo.packageName.equals(packageName)) { + return true; + } + } + } + } catch (Exception e) { + Log.e(TAG, "Error: " + e.getMessage()); + } + return false; + } + + /** + * Checks if a specific service is running. + * + * @param context The context to retrieve the activity manager + * @param serviceName The name of the service + * @return Whether the service is running or not + */ + public static boolean isServiceRunning(Context context, String serviceName) { + ActivityManager activityManager = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + List services = activityManager + .getRunningServices(Integer.MAX_VALUE); + + if (services != null) { + for (ActivityManager.RunningServiceInfo info : services) { + if (info.service != null) { + if (info.service.getClassName() != null && info.service.getClassName() + .equalsIgnoreCase(serviceName)) { + return true; + } + } + } + } + + return false; + } + + /** + * Check if system has a camera. + * + * @param context + * @return + */ + public static boolean hasCamera(final Context context) { + final PackageManager pm = context.getPackageManager(); + return pm != null && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA); + } + + /** + * Check if system has a front camera. + * + * @param context + * @return + */ + public static boolean hasFrontCamera(final Context context) { + final PackageManager pm = context.getPackageManager(); + return pm != null && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + } +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 59c7a098ecbd..c4a3737ca157 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -573,6 +573,11 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_lock_onthego.xml b/packages/SystemUI/res/drawable/ic_lock_onthego.xml new file mode 100644 index 000000000000..ea8294dd109a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_lock_onthego.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/layout/quick_settings_onthego_dialog.xml b/packages/SystemUI/res/layout/quick_settings_onthego_dialog.xml new file mode 100644 index 000000000000..da23c1729c9d --- /dev/null +++ b/packages/SystemUI/res/layout/quick_settings_onthego_dialog.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml new file mode 100644 index 000000000000..9af2ad104f0d --- /dev/null +++ b/packages/SystemUI/res/values/cr_config.xml @@ -0,0 +1,17 @@ + + + + + + 999 + + + 1000 + + + 3000 + 6000 + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 23ebd1c36024..64c04253b695 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -66,4 +66,18 @@ SystemUI + + + On-The-Go activated + On-The-Go is active + Stop + Options + Unable to start On-The-Go + Restart On-The-Go + Camera mode changed + Transparency + Use front camera + Automatically restart service + Main + On-The-Go diff --git a/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoDialog.java b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoDialog.java new file mode 100644 index 000000000000..4cfe9fd48d11 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoDialog.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2014 The NamelessRom Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.crdroid.onthego; + +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.CompoundButton; +import android.widget.SeekBar; +import android.widget.Switch; + +import com.android.systemui.res.R; + +import com.android.internal.util.matrixx.OnTheGoUtils; + +public class OnTheGoDialog extends Dialog { + + protected final Context mContext; + protected final Handler mHandler = new Handler(); + + private final int mOnTheGoDialogLongTimeout; + private final int mOnTheGoDialogShortTimeout; + + private final Runnable mDismissDialogRunnable = new Runnable() { + public void run() { + if (OnTheGoDialog.this.isShowing()) { + OnTheGoDialog.this.dismiss(); + } + } + }; + + public OnTheGoDialog(Context ctx) { + super(ctx); + mContext = ctx; + final Resources r = mContext.getResources(); + mOnTheGoDialogLongTimeout = + r.getInteger(R.integer.quick_settings_onthego_dialog_long_timeout); + mOnTheGoDialogShortTimeout = + r.getInteger(R.integer.quick_settings_onthego_dialog_short_timeout); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Window window = getWindow(); + window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + window.getAttributes().privateFlags |= + WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.requestFeature(Window.FEATURE_NO_TITLE); + + setContentView(R.layout.quick_settings_onthego_dialog); + setCanceledOnTouchOutside(true); + + final ContentResolver resolver = mContext.getContentResolver(); + + final SeekBar mSlider = (SeekBar) findViewById(R.id.alpha_slider); + final float value = Settings.System.getFloat(resolver, + Settings.System.ON_THE_GO_ALPHA, + 0.5f); + final int progress = ((int) (value * 100)); + mSlider.setProgress(progress); + mSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + sendAlphaBroadcast(String.valueOf(i + 10)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + removeAllOnTheGoDialogCallbacks(); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + dismissOnTheGoDialog(mOnTheGoDialogShortTimeout); + } + }); + + if (!OnTheGoUtils.hasFrontCamera(getContext())) { + findViewById(R.id.onthego_category_1).setVisibility(View.GONE); + } else { + final Switch mServiceToggle = (Switch) findViewById(R.id.onthego_service_toggle); + final boolean restartService = Settings.System.getInt(resolver, + Settings.System.ON_THE_GO_SERVICE_RESTART, 0) == 1; + mServiceToggle.setChecked(restartService); + mServiceToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + Settings.System.putInt(resolver, + Settings.System.ON_THE_GO_SERVICE_RESTART, + (b ? 1 : 0)); + dismissOnTheGoDialog(mOnTheGoDialogShortTimeout); + } + }); + + final Switch mCamSwitch = (Switch) findViewById(R.id.onthego_camera_toggle); + final boolean useFrontCam = (Settings.System.getInt(resolver, + Settings.System.ON_THE_GO_CAMERA, + 0) == 1); + mCamSwitch.setChecked(useFrontCam); + mCamSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + Settings.System.putInt(resolver, + Settings.System.ON_THE_GO_CAMERA, + (b ? 1 : 0)); + sendCameraBroadcast(); + dismissOnTheGoDialog(mOnTheGoDialogShortTimeout); + } + }); + } + } + + @Override + protected void onStart() { + super.onStart(); + dismissOnTheGoDialog(mOnTheGoDialogLongTimeout); + } + + @Override + protected void onStop() { + super.onStop(); + removeAllOnTheGoDialogCallbacks(); + } + + private void dismissOnTheGoDialog(int timeout) { + removeAllOnTheGoDialogCallbacks(); + mHandler.postDelayed(mDismissDialogRunnable, timeout); + } + + private void removeAllOnTheGoDialogCallbacks() { + mHandler.removeCallbacks(mDismissDialogRunnable); + } + + private void sendAlphaBroadcast(String i) { + final float value = (Float.parseFloat(i) / 100); + final Intent alphaBroadcast = new Intent(); + alphaBroadcast.setAction(OnTheGoService.ACTION_TOGGLE_ALPHA); + alphaBroadcast.putExtra(OnTheGoService.EXTRA_ALPHA, value); + mContext.sendBroadcast(alphaBroadcast); + } + + private void sendCameraBroadcast() { + final Intent cameraBroadcast = new Intent(); + cameraBroadcast.setAction(OnTheGoService.ACTION_TOGGLE_CAMERA); + mContext.sendBroadcast(cameraBroadcast); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java new file mode 100644 index 000000000000..6b9a5b903b16 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2014 The NamelessRom Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.crdroid.onthego; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings; +import android.util.Log; +import android.view.TextureView; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.systemui.res.R; + +import com.android.internal.util.matrixx.OnTheGoUtils; + +import java.io.IOException; + +public class OnTheGoService extends Service { + + private static final String TAG = "OnTheGoService"; + private static final boolean DEBUG = false; + + private static final int ONTHEGO_NOTIFICATION_ID = 81333378; + private static final String ONTHEGO_CHANNEL_ID = "onthego_notif"; + + public static final String ACTION_START = "start"; + public static final String ACTION_STOP = "stop"; + public static final String ACTION_TOGGLE_ALPHA = "toggle_alpha"; + public static final String ACTION_TOGGLE_CAMERA = "toggle_camera"; + public static final String ACTION_TOGGLE_OPTIONS = "toggle_options"; + public static final String EXTRA_ALPHA = "extra_alpha"; + + private static final int CAMERA_BACK = 0; + private static final int CAMERA_FRONT = 1; + + private static final int NOTIFICATION_STARTED = 0; + private static final int NOTIFICATION_RESTART = 1; + private static final int NOTIFICATION_ERROR = 2; + + private final Handler mHandler = new Handler(); + private final Object mRestartObject = new Object(); + + private FrameLayout mOverlay; + private Camera mCamera; + private NotificationManager mNotificationManager; + private NotificationChannel mNotificationChannel; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterReceivers(); + resetViews(); + } + + private void registerReceivers() { + final IntentFilter alphaFilter = new IntentFilter(ACTION_TOGGLE_ALPHA); + registerReceiver(mAlphaReceiver, alphaFilter, Context.RECEIVER_NOT_EXPORTED); + final IntentFilter cameraFilter = new IntentFilter(ACTION_TOGGLE_CAMERA); + registerReceiver(mCameraReceiver, cameraFilter, Context.RECEIVER_NOT_EXPORTED); + } + + private void unregisterReceivers() { + try { + unregisterReceiver(mAlphaReceiver); + } catch (Exception ignored) { } + try { + unregisterReceiver(mCameraReceiver); + } catch (Exception ignored) { } + } + + private final BroadcastReceiver mAlphaReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final float intentAlpha = intent.getFloatExtra(EXTRA_ALPHA, 0.5f); + toggleOnTheGoAlpha(intentAlpha); + } + }; + + private final BroadcastReceiver mCameraReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mRestartObject) { + final ContentResolver resolver = getContentResolver(); + final boolean restartService = Settings.System.getInt(resolver, + Settings.System.ON_THE_GO_SERVICE_RESTART, 0) == 1; + if (restartService) { + restartOnTheGo(); + } else { + stopOnTheGo(true); + } + } + } + }; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + logDebug("onStartCommand called"); + + if (intent == null || !OnTheGoUtils.hasCamera(this)) { + stopSelf(); + return START_NOT_STICKY; + } + + final String action = intent.getAction(); + + if (action != null && !action.isEmpty()) { + logDebug("Action: " + action); + if (action.equals(ACTION_START)) { + startOnTheGo(); + } else if (action.equals(ACTION_STOP)) { + stopOnTheGo(false); + } else if (action.equals(ACTION_TOGGLE_OPTIONS)) { + new OnTheGoDialog(this).show(); + } + } else { + logDebug("Action is NULL or EMPTY!"); + stopSelf(); + } + + return START_NOT_STICKY; + } + + private void startOnTheGo() { + if (mNotificationManager != null) { + logDebug("Starting while active, stopping."); + stopOnTheGo(false); + return; + } + + resetViews(); + registerReceivers(); + setupViews(false); + + createNotification(NOTIFICATION_STARTED); + } + + private void stopOnTheGo(boolean shouldRestart) { + unregisterReceivers(); + resetViews(); + + // Cancel notification + if (mNotificationManager != null) { + mNotificationManager.cancel(ONTHEGO_NOTIFICATION_ID); + mNotificationManager.deleteNotificationChannel(ONTHEGO_CHANNEL_ID); + mNotificationManager = null; + } + + if (shouldRestart) { + createNotification(NOTIFICATION_RESTART); + } + + stopSelf(); + } + + private void restartOnTheGo() { + resetViews(); + mHandler.removeCallbacks(mRestartRunnable); + mHandler.postDelayed(mRestartRunnable, 750); + } + + private final Runnable mRestartRunnable = new Runnable() { + @Override + public void run() { + synchronized (mRestartObject) { + setupViews(true); + } + } + }; + + private void toggleOnTheGoAlpha() { + final float alpha = Settings.System.getFloat(getContentResolver(), + Settings.System.ON_THE_GO_ALPHA, + 0.5f); + toggleOnTheGoAlpha(alpha); + } + + private void toggleOnTheGoAlpha(float alpha) { + Settings.System.putFloat(getContentResolver(), + Settings.System.ON_THE_GO_ALPHA, + alpha); + + if (mOverlay != null) { + mOverlay.setAlpha(alpha); + } + } + + private void getCameraInstance(int type) throws RuntimeException, IOException { + releaseCamera(); + + if (!OnTheGoUtils.hasFrontCamera(this)) { + mCamera = Camera.open(); + return; + } + + switch (type) { + // Get hold of the back facing camera + default: + case CAMERA_BACK: + mCamera = Camera.open(0); + break; + // Get hold of the front facing camera + case CAMERA_FRONT: + final Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + final int cameraCount = Camera.getNumberOfCameras(); + + for (int camIdx = 0; camIdx < cameraCount; camIdx++) { + Camera.getCameraInfo(camIdx, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + mCamera = Camera.open(camIdx); + } + } + break; + } + } + + private void setupViews(final boolean isRestarting) { + logDebug("Setup Views, restarting: " + (isRestarting ? "true" : "false")); + + final int cameraType = Settings.System.getInt(getContentResolver(), + Settings.System.ON_THE_GO_CAMERA, + 0); + + try { + getCameraInstance(cameraType); + } catch (Exception exc) { + // Well, you cant have all in this life.. + logDebug("Exception: " + exc.getMessage()); + createNotification(NOTIFICATION_ERROR); + stopOnTheGo(true); + } + + final TextureView mTextureView = new TextureView(this); + mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) { + try { + if (mCamera != null) { + mCamera.setDisplayOrientation(90); + mCamera.setPreviewTexture(surfaceTexture); + mCamera.startPreview(); + } + } catch (IOException io) { + logDebug("IOException: " + io.getMessage()); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + releaseCamera(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } + }); + + mOverlay = new FrameLayout(this); + mOverlay.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT) + ); + mOverlay.addView(mTextureView); + + final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | + WindowManager.LayoutParams.FLAG_FULLSCREEN | + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | + WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + PixelFormat.TRANSLUCENT + ); + wm.addView(mOverlay, params); + + toggleOnTheGoAlpha(); + } + + private void resetViews() { + releaseCamera(); + final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); + if (mOverlay != null) { + mOverlay.removeAllViews(); + wm.removeView(mOverlay); + mOverlay = null; + } + } + + private void releaseCamera() { + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.release(); + mCamera = null; + } + } + + private void createNotification(final int type) { + final Resources r = getResources(); + final Notification.Builder builder = new Notification.Builder(this, ONTHEGO_CHANNEL_ID) + .setTicker(r.getString( + (type == 1 ? R.string.onthego_notif_camera_changed : + (type == 2 ? R.string.onthego_notif_error + : R.string.onthego_notif_ticker)) + )) + .setContentTitle(r.getString( + (type == 1 ? R.string.onthego_notif_camera_changed : + (type == 2 ? R.string.onthego_notif_error + : R.string.onthego_notif_title)) + )) + .setSmallIcon(com.android.systemui.res.R.drawable.ic_lock_onthego) + .setWhen(System.currentTimeMillis()) + .setOngoing(!(type == 1 || type == 2)); + + if (type == 1 || type == 2) { + final ComponentName cn = new ComponentName("com.android.systemui", + "com.android.systemui.spark.onthego.OnTheGoService"); + final Intent startIntent = new Intent(); + startIntent.setComponent(cn); + startIntent.setAction(ACTION_START); + final PendingIntent startPendIntent = PendingIntent.getService(this, 0, startIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + builder.addAction(com.android.internal.R.drawable.ic_media_play, + r.getString(R.string.onthego_notif_restart), startPendIntent); + } else { + final Intent stopIntent = new Intent(this, OnTheGoService.class) + .setAction(OnTheGoService.ACTION_STOP); + final PendingIntent stopPendIntent = PendingIntent.getService(this, 0, stopIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + final Intent optionsIntent = new Intent(this, OnTheGoService.class) + .setAction(OnTheGoService.ACTION_TOGGLE_OPTIONS); + final PendingIntent optionsPendIntent = PendingIntent.getService(this, 0, optionsIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + builder + .addAction(com.android.internal.R.drawable.ic_media_stop, + r.getString(R.string.onthego_notif_stop), stopPendIntent) + .addAction(com.android.internal.R.drawable.ic_text_dot, + r.getString(R.string.onthego_notif_options), optionsPendIntent); + } + + final Notification notif = builder.build(); + + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationChannel = new NotificationChannel(ONTHEGO_CHANNEL_ID, + r.getString(R.string.onthego_channel_name), + NotificationManager.IMPORTANCE_LOW); + mNotificationManager.createNotificationChannel(mNotificationChannel); + + mNotificationManager.notify(ONTHEGO_NOTIFICATION_ID, notif); + } + + private void logDebug(String msg) { + if (DEBUG) { + Log.e(TAG, msg); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index c8a224e43048..d0462e4c7259 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -42,6 +42,7 @@ import android.app.WallpaperManager; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -781,6 +782,12 @@ protected void createActionItems() { if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_TELEVISION) { addIfShouldShowAction(tempActions, new ScreenshotAction()); } + } else if (GLOBAL_ACTION_KEY_ONTHEGO.equals(actionKey)) { + UiModeManager uiModeManager = + (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_TELEVISION) { + addIfShouldShowAction(tempActions, new getOnTheGoAction()); + } } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { if (mLogoutInteractor.isLogoutEnabled().getValue()) { addIfShouldShowAction(tempActions, new LogoutAction()); @@ -1655,6 +1662,34 @@ public boolean showBeforeProvisioning() { }; } + class getOnTheGoAction extends SinglePressAction { + + public getOnTheGoAction() { + super(com.android.systemui.res.R.drawable.ic_lock_onthego, + com.android.systemui.res.R.string.global_action_onthego); + } + + @Override + public void onPress() { + ComponentName cn = new ComponentName("com.android.systemui", + "com.android.systemui.crdroid.onthego.OnTheGoService"); + Intent onTheGoIntent = new Intent(); + onTheGoIntent.setComponent(cn); + onTheGoIntent.setAction("start"); + mContext.startService(onTheGoIntent); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + } + @VisibleForTesting class LockDownAction extends SinglePressAction { LockDownAction() { From 19cb28e891a2cb3a1dc2102b8ef0237df8253cea Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 15 Jul 2025 01:22:09 +0530 Subject: [PATCH 0323/1315] SystemUI: OnTheGo: Update camera API for service Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../crdroid/onthego/OnTheGoService.java | 274 ++++++++++++------ 1 file changed, 179 insertions(+), 95 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java index 6b9a5b903b16..9e024626718f 100644 --- a/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java +++ b/packages/SystemUI/src/com/android/systemui/crdroid/onthego/OnTheGoService.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 The NamelessRom Project + * (C) 2025 crDroid Android Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +17,7 @@ package com.android.systemui.crdroid.onthego; +import android.Manifest; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -27,24 +29,37 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.SurfaceTexture; -import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.provider.Settings; import android.util.Log; +import android.view.Surface; import android.view.TextureView; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.systemui.res.R; import com.android.internal.util.matrixx.OnTheGoUtils; import java.io.IOException; +import java.util.Arrays; public class OnTheGoService extends Service { @@ -68,14 +83,21 @@ public class OnTheGoService extends Service { private static final int NOTIFICATION_RESTART = 1; private static final int NOTIFICATION_ERROR = 2; - private final Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Object mRestartObject = new Object(); - private FrameLayout mOverlay; - private Camera mCamera; + private FrameLayout mOverlay; + private boolean isOverlayAdded = false; + private NotificationManager mNotificationManager; private NotificationChannel mNotificationChannel; + private CameraDevice mCameraDevice; + private CameraCaptureSession mCaptureSession; + private CaptureRequest.Builder mPreviewRequestBuilder; + private String mCameraId; + private TextureView mTextureView; + @Override public IBinder onBind(Intent intent) { return null; @@ -220,121 +242,183 @@ private void toggleOnTheGoAlpha(float alpha) { } } - private void getCameraInstance(int type) throws RuntimeException, IOException { - releaseCamera(); + private void setupViews(final boolean isRestarting) { + logDebug("Setup Views with Camera2"); - if (!OnTheGoUtils.hasFrontCamera(this)) { - mCamera = Camera.open(); - return; - } + CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); - switch (type) { - // Get hold of the back facing camera - default: - case CAMERA_BACK: - mCamera = Camera.open(0); - break; - // Get hold of the front facing camera - case CAMERA_FRONT: - final Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); - final int cameraCount = Camera.getNumberOfCameras(); - - for (int camIdx = 0; camIdx < cameraCount; camIdx++) { - Camera.getCameraInfo(camIdx, cameraInfo); - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - mCamera = Camera.open(camIdx); - } + try { + for (String cameraId : manager.getCameraIdList()) { + CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + + int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + int preferredFacing = Settings.System.getInt(getContentResolver(), + Settings.System.ON_THE_GO_CAMERA, 0); + + if ((preferredFacing == 1 && lensFacing == CameraCharacteristics.LENS_FACING_FRONT) || + (preferredFacing == 0 && lensFacing == CameraCharacteristics.LENS_FACING_BACK)) { + mCameraId = cameraId; + break; } - break; - } - } + } - private void setupViews(final boolean isRestarting) { - logDebug("Setup Views, restarting: " + (isRestarting ? "true" : "false")); + if (mCameraId == null) { + logDebug("No suitable camera found"); + createNotification(NOTIFICATION_ERROR); + stopOnTheGo(true); + return; + } + + mTextureView = new TextureView(this); + mTextureView.setSurfaceTextureListener(textureListener); + + mOverlay = new FrameLayout(this); + mOverlay.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + mOverlay.addView(mTextureView); + + WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | + WindowManager.LayoutParams.FLAG_FULLSCREEN, + PixelFormat.TRANSLUCENT + ); + if (!isOverlayAdded) { + wm.addView(mOverlay, params); + isOverlayAdded = true; + } - final int cameraType = Settings.System.getInt(getContentResolver(), - Settings.System.ON_THE_GO_CAMERA, - 0); + toggleOnTheGoAlpha(); - try { - getCameraInstance(cameraType); - } catch (Exception exc) { - // Well, you cant have all in this life.. - logDebug("Exception: " + exc.getMessage()); + } catch (CameraAccessException e) { + logDebug("CameraAccessException: " + e.getMessage()); createNotification(NOTIFICATION_ERROR); stopOnTheGo(true); } + } - final TextureView mTextureView = new TextureView(this); - mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) { - try { - if (mCamera != null) { - mCamera.setDisplayOrientation(90); - mCamera.setPreviewTexture(surfaceTexture); - mCamera.startPreview(); - } - } catch (IOException io) { - logDebug("IOException: " + io.getMessage()); - } - } + private final TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + openCamera(); + } - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) { + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + closeCamera(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) {} + }; + + private void openCamera() { + if (mCameraDevice != null) return; + CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); + try { + if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + logDebug("Camera permission not granted"); + stopOnTheGo(true); + return; } - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - releaseCamera(); - return true; + manager.openCamera(mCameraId, new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice camera) { + mCameraDevice = camera; + startCameraPreview(); + } + + @Override + public void onDisconnected(@NonNull CameraDevice camera) { + camera.close(); + mCameraDevice = null; + } + + @Override + public void onError(@NonNull CameraDevice camera, int error) { + logDebug("Camera error: " + error); + camera.close(); + mCameraDevice = null; + createNotification(NOTIFICATION_ERROR); + stopOnTheGo(true); + } + }, mHandler); + } catch (CameraAccessException e) { + logDebug("Failed to open camera: " + e.getMessage()); + } + } + + private void startCameraPreview() { + try { + SurfaceTexture texture = mTextureView.getSurfaceTexture(); + if (texture == null) { + logDebug("SurfaceTexture is null. Skipping preview."); + return; } + texture.setDefaultBufferSize(1080, 1920); + + Surface surface = new Surface(texture); + mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + mPreviewRequestBuilder.addTarget(surface); + + mCameraDevice.createCaptureSession( + Arrays.asList(surface), + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + mCaptureSession = session; + try { + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + null, mHandler); + } catch (CameraAccessException e) { + logDebug("Preview session error: " + e.getMessage()); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + logDebug("CaptureSession configuration failed"); + } + }, mHandler); - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } - }); - - mOverlay = new FrameLayout(this); - mOverlay.setLayoutParams(new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT) - ); - mOverlay.addView(mTextureView); - - final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); - final WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | - WindowManager.LayoutParams.FLAG_FULLSCREEN | - WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | - WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, - PixelFormat.TRANSLUCENT - ); - wm.addView(mOverlay, params); - - toggleOnTheGoAlpha(); + } catch (CameraAccessException e) { + logDebug("CameraAccessException in preview: " + e.getMessage()); + } + } + + private void closeCamera() { + if (mCaptureSession != null) { + mCaptureSession.close(); + mCaptureSession = null; + } + if (mCameraDevice != null) { + mCameraDevice.close(); + mCameraDevice = null; + } } private void resetViews() { - releaseCamera(); - final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); + closeCamera(); + WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); if (mOverlay != null) { + if (isOverlayAdded) { + wm.removeView(mOverlay); + isOverlayAdded = false; + } mOverlay.removeAllViews(); - wm.removeView(mOverlay); mOverlay = null; } } - private void releaseCamera() { - if (mCamera != null) { - mCamera.stopPreview(); - mCamera.release(); - mCamera = null; - } - } - private void createNotification(final int type) { final Resources r = getResources(); final Notification.Builder builder = new Notification.Builder(this, ONTHEGO_CHANNEL_ID) From c78826cc7eb54d2a891248d5893dd8a63b8b33e0 Mon Sep 17 00:00:00 2001 From: Chris Crump Date: Mon, 30 Sep 2019 16:47:42 -0400 Subject: [PATCH 0324/1315] Add Alert Slider user interface [SQUASHED] Ported from OxygenOS and reworked for our alert slider implementation. We target AudioManager instead of Zen, icons are also the same as aosp and the dialog uses the material theme as well as support for our themes. To use, the alert slider config must be enabled. By default, the dialog shows on the left side. To move it to the right side, set the location config to 1. Squashed: From: ZVNexus Date: Fri, 24 Jan 2020 17:00:41 -0500 Subject: AlertSlider: Make tri-state SystemUI dialog dimensions conditional Adjust this based on the left/right config. From: Pranav Vashi Date: Sun, 23 Feb 2020 17:13:34 +0530 Subject: AlertSlider: Work better with Key Handlers * Let Key Handler send intent to let know slider movement. * Helps not showing dialog every time ringer changes. From: Pranav Vashi Date: Sun, 23 Feb 2020 23:38:41 +0530 Subject: AlertSlider: Do not hardcode slider position based on ringer mode * Let Key Handler send position details. Different ringer options can be now assigned to different position. * Also increase dialog show timeout slightly to avoid race with Key Handler. From: Hikari-no-Tenshi Date: Mon, 24 Feb 2020 22:44:49 +0200 Subject: AlertSlider: Use default position behaviour if position not specified in intent From: Pranav Vashi Date: Sun, 1 Mar 2020 00:38:13 +0530 Subject: AlertSlider: Improve layout From: Ali B Date: Sat, 14 Mar 2020 00:19:59 +0300 Subject: AlertSlider: refactor to reflect slider state As there is the possibility of having more option values for the alertslider (such as various zen mode states) in addition to the current ringer mode states, update logic to reflect the value slider's position is set to instead of the ringer state we were using before. From: Ali B Date: Mon, 23 Mar 2020 22:49:23 +0300 Subject: AlertSlider: Update resources From: Pranav Vashi Date: Sun, 7 Feb 2021 12:07:58 +0530 Subject: AlertSlider: Fix layout for 180 rotation From: Pranav Vashi Date: Sun, 7 Feb 2021 09:20:22 +0530 Subject: AlertSlider: Add more resources From: Pranav Vashi Date: Sun, 7 Feb 2021 02:08:38 +0530 Subject: AlertSlider: Prevent crash in case of incomplete broadcast * In intent extra EXTRA_SLIDER_POSITION_VALUE is not received and slider is changed, SystemUI will crash "No resource found". This patch should prevent such ice-cold havoc. From: Pranav Vashi Date: Tue, 23 Mar 2021 07:38:35 +0530 Subject: AlertSlider: Update theme more swiftly From: Pranav Vashi Date: Fri, 28 Feb 2020 00:19:59 +0530 Subject: AlertSlider: Add toggle to disable notifications [1/2] From: Pranav Vashi Date: Sun, 7 Feb 2021 12:08:16 +0530 Subject: AlertSlider: Support slider actions without broadcast from device * Let's not lose purpose of original commit where intent broadcast was not introduced. Partially reverts 9241e52089404ead465c0ee5d48cdefa90b4809b From: idoybh Date: Sun, 8 Aug 2021 15:49:16 +0200 Subject: AlertSlider: check for existing dialog before creating new From: AnierinB Date: Sun, 18 Sep 2022 14:52:24 +0200 Subject: AlertSlider: Allow UI to work with multiple resolutions * Device side dimen overlays should reflect the maximum resolution. * Inspired by: https://review.lineageos.org/c/LineageOS/android_frameworks_base/+/339290 * Migrate to DisplayUtils.getScaleFactor @neobuddy89: Clean up redundancy chaos. From: Pranav Vashi Date: Wed, 1 Nov 2023 13:37:17 +0530 Subject: AlertSlider: Fixup implementation on A14 Signed-off-by: Pranav Vashi From: minaripenguin Date: Fri, 26 Apr 2024 06:22:50 +0800 Subject: AlertSlider: Use surface color for dialog background Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Sat, 2 Nov 2024 03:41:04 +0530 Subject: AlertSlider: Move few configs to SystemUI * These configs are really not required at platform level. At platform level, they (somehow) crashes PixelDisplayService. Signed-off-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + core/res/res/values/matrixx_config.xml | 3 + core/res/res/values/matrixx_symbols.xml | 3 + .../drawable/dialog_tri_state_middle_bg.xml | 30 + .../dialog_tri_state_navigation_bg.xml | 29 + .../dialog_tri_state_triangle_right.xml | 27 + .../dialog_tri_state_triangle_top.xml | 27 + .../drawable/ic_tristate_brightness_auto.xml | 10 + .../ic_tristate_brightness_bright.xml | 10 + .../drawable/ic_tristate_brightness_dark.xml | 10 + .../res/drawable/ic_tristate_flashlight.xml | 11 + .../drawable/ic_tristate_flashlight_off.xml | 10 + .../res/drawable/ic_tristate_rotate_auto.xml | 10 + .../drawable/ic_tristate_rotate_landscape.xml | 10 + .../drawable/ic_tristate_rotate_portrait.xml | 10 + .../left_dialog_tri_state_down_bg.xml | 30 + .../drawable/left_dialog_tri_state_up_bg.xml | 29 + .../right_dialog_tri_state_down_bg.xml | 30 + .../drawable/right_dialog_tri_state_up_bg.xml | 30 + .../SystemUI/res/layout/tri_state_dialog.xml | 65 ++ .../SystemUI/res/values/matrixx_attrs.xml | 61 -- .../SystemUI/res/values/matrixx_config.xml | 9 + .../SystemUI/res/values/matrixx_dimens.xml | 33 + .../SystemUI/res/values/matrixx_strings.xml | 16 + .../SystemUI/res/values/matrixx_styles.xml | 67 ++ .../SystemUI/res/values/matrixx_symbols.xml | 6 + .../tristate/TriStateUiController.java | 32 + .../tristate/TriStateUiControllerImpl.java | 671 ++++++++++++++++++ .../volume/VolumeDialogComponent.java | 22 +- 29 files changed, 1245 insertions(+), 62 deletions(-) create mode 100644 packages/SystemUI/res/drawable/dialog_tri_state_middle_bg.xml create mode 100644 packages/SystemUI/res/drawable/dialog_tri_state_navigation_bg.xml create mode 100644 packages/SystemUI/res/drawable/dialog_tri_state_triangle_right.xml create mode 100644 packages/SystemUI/res/drawable/dialog_tri_state_triangle_top.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_brightness_auto.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_brightness_bright.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_brightness_dark.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_flashlight.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_flashlight_off.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_rotate_auto.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_rotate_landscape.xml create mode 100644 packages/SystemUI/res/drawable/ic_tristate_rotate_portrait.xml create mode 100644 packages/SystemUI/res/drawable/left_dialog_tri_state_down_bg.xml create mode 100644 packages/SystemUI/res/drawable/left_dialog_tri_state_up_bg.xml create mode 100644 packages/SystemUI/res/drawable/right_dialog_tri_state_down_bg.xml create mode 100644 packages/SystemUI/res/drawable/right_dialog_tri_state_up_bg.xml create mode 100644 packages/SystemUI/res/layout/tri_state_dialog.xml create mode 100644 packages/SystemUI/src/com/android/systemui/tristate/TriStateUiController.java create mode 100644 packages/SystemUI/src/com/android/systemui/tristate/TriStateUiControllerImpl.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0c8fa12178cc..b356c7d822a2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7195,6 +7195,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String ON_THE_GO_CAMERA = "on_the_go_camera"; + /** + * Whether to show or hide alert slider notifications on supported devices + * @hide + */ + public static final String ALERT_SLIDER_NOTIFICATIONS = "alert_slider_notifications"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index 29e8042cd4b9..e2a083955963 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -74,4 +74,7 @@ com.gaana com.facebook.katana.LoginActivity + + + false diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 065d50d8c1e1..7afa344596f1 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -60,4 +60,7 @@ + + + diff --git a/packages/SystemUI/res/drawable/dialog_tri_state_middle_bg.xml b/packages/SystemUI/res/drawable/dialog_tri_state_middle_bg.xml new file mode 100644 index 000000000000..20343d5086ad --- /dev/null +++ b/packages/SystemUI/res/drawable/dialog_tri_state_middle_bg.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/dialog_tri_state_navigation_bg.xml b/packages/SystemUI/res/drawable/dialog_tri_state_navigation_bg.xml new file mode 100644 index 000000000000..93011a323a5b --- /dev/null +++ b/packages/SystemUI/res/drawable/dialog_tri_state_navigation_bg.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/dialog_tri_state_triangle_right.xml b/packages/SystemUI/res/drawable/dialog_tri_state_triangle_right.xml new file mode 100644 index 000000000000..ab90683bea54 --- /dev/null +++ b/packages/SystemUI/res/drawable/dialog_tri_state_triangle_right.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/dialog_tri_state_triangle_top.xml b/packages/SystemUI/res/drawable/dialog_tri_state_triangle_top.xml new file mode 100644 index 000000000000..e8566465d61b --- /dev/null +++ b/packages/SystemUI/res/drawable/dialog_tri_state_triangle_top.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_tristate_brightness_auto.xml b/packages/SystemUI/res/drawable/ic_tristate_brightness_auto.xml new file mode 100644 index 000000000000..c02b0eb6618f --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_brightness_auto.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_brightness_bright.xml b/packages/SystemUI/res/drawable/ic_tristate_brightness_bright.xml new file mode 100644 index 000000000000..5bcaaee69be2 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_brightness_bright.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_brightness_dark.xml b/packages/SystemUI/res/drawable/ic_tristate_brightness_dark.xml new file mode 100644 index 000000000000..f23a2d10cd5d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_brightness_dark.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_flashlight.xml b/packages/SystemUI/res/drawable/ic_tristate_flashlight.xml new file mode 100644 index 000000000000..b64964a32ba9 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_flashlight.xml @@ -0,0 +1,11 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_flashlight_off.xml b/packages/SystemUI/res/drawable/ic_tristate_flashlight_off.xml new file mode 100644 index 000000000000..42c27bf340c2 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_flashlight_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_rotate_auto.xml b/packages/SystemUI/res/drawable/ic_tristate_rotate_auto.xml new file mode 100644 index 000000000000..dbed7f58555a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_rotate_auto.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_rotate_landscape.xml b/packages/SystemUI/res/drawable/ic_tristate_rotate_landscape.xml new file mode 100644 index 000000000000..6ffe5e328e26 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_rotate_landscape.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_tristate_rotate_portrait.xml b/packages/SystemUI/res/drawable/ic_tristate_rotate_portrait.xml new file mode 100644 index 000000000000..55cc144738d1 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tristate_rotate_portrait.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/left_dialog_tri_state_down_bg.xml b/packages/SystemUI/res/drawable/left_dialog_tri_state_down_bg.xml new file mode 100644 index 000000000000..bdc35775bb5c --- /dev/null +++ b/packages/SystemUI/res/drawable/left_dialog_tri_state_down_bg.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/left_dialog_tri_state_up_bg.xml b/packages/SystemUI/res/drawable/left_dialog_tri_state_up_bg.xml new file mode 100644 index 000000000000..119471caf169 --- /dev/null +++ b/packages/SystemUI/res/drawable/left_dialog_tri_state_up_bg.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/right_dialog_tri_state_down_bg.xml b/packages/SystemUI/res/drawable/right_dialog_tri_state_down_bg.xml new file mode 100644 index 000000000000..884974da2e5c --- /dev/null +++ b/packages/SystemUI/res/drawable/right_dialog_tri_state_down_bg.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/right_dialog_tri_state_up_bg.xml b/packages/SystemUI/res/drawable/right_dialog_tri_state_up_bg.xml new file mode 100644 index 000000000000..1c26d6f6ad69 --- /dev/null +++ b/packages/SystemUI/res/drawable/right_dialog_tri_state_up_bg.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/tri_state_dialog.xml b/packages/SystemUI/res/layout/tri_state_dialog.xml new file mode 100644 index 000000000000..6d9856cbc01b --- /dev/null +++ b/packages/SystemUI/res/layout/tri_state_dialog.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_attrs.xml b/packages/SystemUI/res/values/matrixx_attrs.xml index 3c1c55db62ab..a2a2f90915aa 100644 --- a/packages/SystemUI/res/values/matrixx_attrs.xml +++ b/packages/SystemUI/res/values/matrixx_attrs.xml @@ -5,65 +5,4 @@ --> - - - - - - - - - - diff --git a/packages/SystemUI/res/values/matrixx_config.xml b/packages/SystemUI/res/values/matrixx_config.xml index 4029e839bf81..b689228a8e14 100644 --- a/packages/SystemUI/res/values/matrixx_config.xml +++ b/packages/SystemUI/res/values/matrixx_config.xml @@ -10,4 +10,13 @@ 1000 + + + + 0 + + + diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index 29438a5a212b..3fc44d0f1ba0 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -61,4 +61,37 @@ 1.0dp 52.0dp 45.0dp + + + 850.0px + 650.0px + 650.0px + 650.0px + 450.0px + 650.0px + 21.0px + 21.0px + 4.0dip + 24.0dip + 8.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 0.0dip + 24.0dip + 0.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 24.0dip + 0.0dip + 24.0dip + 0.0dip + 24.0dip + 24.0dip diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 64c04253b695..024fa803154b 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -80,4 +80,20 @@ Automatically restart service Main On-The-Go + + + Priority + Alarms + DnD + Torch + Torch on + Torch off + Torch blink + Max-brightness + Min-brightness + Auto-brightness + Auto-rotate + Portrait + Landscape (90°) + Landscape (270°) diff --git a/packages/SystemUI/res/values/matrixx_styles.xml b/packages/SystemUI/res/values/matrixx_styles.xml index a2a2f90915aa..06951eb6d00d 100644 --- a/packages/SystemUI/res/values/matrixx_styles.xml +++ b/packages/SystemUI/res/values/matrixx_styles.xml @@ -5,4 +5,71 @@ --> + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_symbols.xml b/packages/SystemUI/res/values/matrixx_symbols.xml index a2a2f90915aa..daaa67c69bf6 100644 --- a/packages/SystemUI/res/values/matrixx_symbols.xml +++ b/packages/SystemUI/res/values/matrixx_symbols.xml @@ -5,4 +5,10 @@ --> + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiController.java b/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiController.java new file mode 100644 index 000000000000..203522132a3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 CypherOS + * Copyright 2014-2019 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.tristate; + +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.VolumeDialog.Callback; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +@DependsOn(target = Callback.class) +@ProvidesInterface(action = "com.android.systemui.action.PLUGIN_TRI_STATE_UI", version = 1) +public interface TriStateUiController extends Plugin { + + public interface UserActivityListener { + void onTriStateUserActivity(); + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiControllerImpl.java b/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiControllerImpl.java new file mode 100644 index 000000000000..abd97e4bfd78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tristate/TriStateUiControllerImpl.java @@ -0,0 +1,671 @@ +/* + * Copyright (C) 2019 CypherOS + * (C) 2014-2020 Paranoid Android + * (C) 2020-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.tristate; + +import static android.view.Surface.ROTATION_90; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; + +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.hardware.display.DisplayManagerGlobal; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.provider.Settings; +import android.util.DisplayUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager.LayoutParams; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.res.R; +import com.android.systemui.tristate.TriStateUiController; +import com.android.systemui.tristate.TriStateUiController.UserActivityListener; +import com.android.systemui.plugins.VolumeDialogController; +import com.android.systemui.plugins.VolumeDialogController.Callbacks; +import com.android.systemui.plugins.VolumeDialogController.State; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.tuner.TunerService; + +public class TriStateUiControllerImpl implements TriStateUiController, + ConfigurationController.ConfigurationListener, TunerService.Tunable { + + + private static String TAG = "TriStateUiControllerImpl"; + + public static final String ALERT_SLIDER_NOTIFICATIONS = + "system:" + Settings.System.ALERT_SLIDER_NOTIFICATIONS; + + private static final int MSG_DIALOG_SHOW = 1; + private static final int MSG_DIALOG_DISMISS = 2; + private static final int MSG_RESET_SCHEDULE = 3; + private static final int MSG_STATE_CHANGE = 4; + + private static final int RINGER_MODE_NORMAL = AudioManager.RINGER_MODE_NORMAL; + private static final int RINGER_MODE_SILENT = AudioManager.RINGER_MODE_SILENT; + private static final int RINGER_MODE_VIBRATE = AudioManager.RINGER_MODE_VIBRATE; + + private static final int POSITION_TOP = 0; + private static final int POSITION_MIDDLE = 1; + private static final int POSITION_BOTTOM = 2; + + // Slider + private static final int MODE_TOTAL_SILENCE = 600; + private static final int MODE_ALARMS_ONLY = 601; + private static final int MODE_PRIORITY_ONLY = 602; + private static final int MODE_NONE = 603; + private static final int MODE_VIBRATE = 604; + private static final int MODE_RING = 605; + // Arbitrary value which hopefully doesn't conflict with upstream anytime soon + private static final int MODE_SILENT = 620; + private static final int MODE_FLASHLIGHT_ON = 621; + private static final int MODE_FLASHLIGHT_OFF = 622; + private static final int MODE_FLASHLIGHT_BLINK = 623; + private static final int MODE_BRIGHTNESS_BRIGHT = 630; + private static final int MODE_BRIGHTNESS_DARK = 631; + private static final int MODE_BRIGHTNESS_AUTO = 632; + private static final int MODE_ROTATION_AUTO = 640; + private static final int MODE_ROTATION_0 = 641; + private static final int MODE_ROTATION_90 = 642; + private static final int MODE_ROTATION_270 = 643; + + private static final String EXTRA_SLIDER_POSITION = "position"; + private static final String EXTRA_SLIDER_POSITION_VALUE = "position_value"; + + private static final int TRI_STATE_UI_POSITION_LEFT = 0; + private static final int TRI_STATE_UI_POSITION_RIGHT = 1; + + private static final long DIALOG_TIMEOUT = 2000; + private static final long DIALOG_DELAY = 300; + + private Context mContext; + private final VolumeDialogController mVolumeDialogController; + private final ConfigurationController mConfigurationController; + private final TunerService mTunerService; + + private final Callbacks mVolumeDialogCallback = new Callbacks() { + @Override + public void onShowRequested(int reason, boolean keyguardLocked, int lockTaskModeState) { } + + @Override + public void onDismissRequested(int reason) { } + + @Override + public void onScreenOff() { } + + @Override + public void onStateChanged(State state) { } + + @Override + public void onLayoutDirectionChanged(int layoutDirection) { } + + @Override + public void onShowVibrateHint() { } + + @Override + public void onShowSilentHint() { } + + @Override + public void onShowSafetyWarning(int flags) { } + + @Override + public void onShowCsdWarning(int csdWarning, int durationMs) { } + + @Override + public void onAccessibilityModeChanged(Boolean showA11yStream) { } + + @Override + public void onCaptionComponentStateChanged( + Boolean isComponentEnabled, Boolean fromTooltip) {} + + @Override + public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch) {} + + @Override + public void onVolumeChangedFromKey() {} + + @Override + public void onConfigurationChanged() { + updateTriStateLayout(); + } + }; + + private int mDensity; + private Dialog mDialog; + private int mDialogPosition; + private ViewGroup mDialogView; + private final H mHandler; + private UserActivityListener mListener; + OrientationEventListener mOrientationListener; + private int mOrientationType = 0; + private boolean mShowing = false; + private int mBackgroundColor = 0; + private ImageView mTriStateIcon; + private TextView mTriStateText; + private int mTriStateMode = -1; + private int mPosition = -1; + private int mPositionValue = -1; + private Window mWindow; + private LayoutParams mWindowLayoutParams; + private int mWindowType; + private String mIntentAction; + private boolean mIntentActionSupported; + private boolean mRingModeChanged; + private boolean mSliderPositionChanged; + private boolean mAlertSliderNotification; + + private final BroadcastReceiver mSliderStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mAlertSliderNotification) { + mRingModeChanged = false; + mSliderPositionChanged = false; + return; + } + + String action = intent.getAction(); + if (mIntentActionSupported && action.equals(mIntentAction)) { + Bundle extras = intent.getExtras(); + mPosition = extras.getInt(EXTRA_SLIDER_POSITION); + mPositionValue = extras.getInt(EXTRA_SLIDER_POSITION_VALUE); + mHandler.sendEmptyMessage(MSG_DIALOG_DISMISS); + mHandler.sendEmptyMessage(MSG_STATE_CHANGE); + mSliderPositionChanged = true; + Log.d(TAG, "received slider position " + mPosition + + " with value " + mPositionValue); + } else if (!mIntentActionSupported && action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + mHandler.sendEmptyMessage(MSG_DIALOG_DISMISS); + mHandler.sendEmptyMessage(MSG_STATE_CHANGE); + mRingModeChanged = true; + } + + if (mRingModeChanged || mSliderPositionChanged) { + mRingModeChanged = false; + mSliderPositionChanged = false; + if (mTriStateMode != -1) { + mHandler.sendEmptyMessageDelayed(MSG_DIALOG_SHOW, (long) DIALOG_DELAY); + } + } + } + }; + + private final class H extends Handler { + private TriStateUiControllerImpl mUiController; + + public H(TriStateUiControllerImpl uiController) { + super(Looper.getMainLooper()); + mUiController = uiController; + } + + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DIALOG_SHOW: + mUiController.handleShow(); + return; + case MSG_DIALOG_DISMISS: + mUiController.handleDismiss(); + return; + case MSG_RESET_SCHEDULE: + mUiController.handleResetTimeout(); + return; + case MSG_STATE_CHANGE: + mUiController.handleStateChanged(); + return; + default: + return; + } + } + } + + public TriStateUiControllerImpl( + Context context, + VolumeDialogController volumeDialogController, + ConfigurationController configurationController, + TunerService tunerService) { + mContext = + new ContextThemeWrapper(context, R.style.qs_theme); + mVolumeDialogController = volumeDialogController; + mConfigurationController = configurationController; + mTunerService = tunerService; + mHandler = new H(this); + mOrientationListener = new OrientationEventListener(mContext, 3) { + @Override + public void onOrientationChanged(int orientation) { + checkOrientationType(); + } + }; + mIntentAction = context.getResources().getString(R.string.config_alertSliderIntent); + mIntentActionSupported = mIntentAction != null && !mIntentAction.isEmpty(); + + IntentFilter filter = new IntentFilter(); + if (mIntentActionSupported) { + filter.addAction(mIntentAction); + } else { + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + } + mContext.registerReceiver(mSliderStateReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case ALERT_SLIDER_NOTIFICATIONS: + mAlertSliderNotification + = TunerService.parseIntegerSwitch(newValue, true); + mHandler.sendEmptyMessage(MSG_DIALOG_DISMISS); + break; + default: + break; + } + } + + @Override + public void onUiModeChanged() { + mContext.getTheme().applyStyle(mContext.getThemeResId(), true); + initDialog(); + } + + private void checkOrientationType() { + Display display = DisplayManagerGlobal.getInstance().getRealDisplay(0); + if (display != null) { + int rotation = display.getRotation(); + if (rotation != mOrientationType) { + mOrientationType = rotation; + updateTriStateLayout(); + } + } + } + + public void init(int windowType, UserActivityListener listener) { + mWindowType = windowType; + mDensity = mContext.getResources().getConfiguration().densityDpi; + mListener = listener; + mConfigurationController.addCallback(this); + mVolumeDialogController.addCallback(mVolumeDialogCallback, mHandler); + mTunerService.addTunable(this, ALERT_SLIDER_NOTIFICATIONS); + initDialog(); + } + + public void destroy() { + mTunerService.removeTunable(this); + mConfigurationController.removeCallback(this); + mVolumeDialogController.removeCallback(mVolumeDialogCallback); + mContext.unregisterReceiver(mSliderStateReceiver); + } + + private void initDialog() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + mDialog = new Dialog(mContext, R.style.qs_theme); + mShowing = false; + mWindow = mDialog.getWindow(); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + mWindow.clearFlags(LayoutParams.FLAG_DIM_BEHIND + | LayoutParams.FLAG_LAYOUT_INSET_DECOR); + mWindow.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE + | LayoutParams.FLAG_LAYOUT_IN_SCREEN + | LayoutParams.FLAG_NOT_TOUCH_MODAL + | LayoutParams.FLAG_SHOW_WHEN_LOCKED + | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | LayoutParams.FLAG_HARDWARE_ACCELERATED); + mWindow.setType(LayoutParams.TYPE_VOLUME_OVERLAY); + mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); + mDialog.setCanceledOnTouchOutside(false); + mWindowLayoutParams = mWindow.getAttributes(); + mWindowLayoutParams.type = mWindowType; + mWindowLayoutParams.format = -3; + mWindowLayoutParams.setTitle(TriStateUiControllerImpl.class.getSimpleName()); + mWindowLayoutParams.gravity = 53; + mWindowLayoutParams.y = mDialogPosition; + mWindow.setAttributes(mWindowLayoutParams); + mWindow.setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING); + mDialog.setContentView(R.layout.tri_state_dialog); + mDialogView = (ViewGroup) mDialog.findViewById(R.id.tri_state_layout); + mTriStateIcon = (ImageView) mDialog.findViewById(R.id.tri_state_icon); + mTriStateText = (TextView) mDialog.findViewById(R.id.tri_state_text); + } + + private void registerOrientationListener(boolean enable) { + if (mOrientationListener.canDetectOrientation() && enable) { + Log.v(TAG, "Can detect orientation"); + mOrientationListener.enable(); + return; + } + Log.v(TAG, "Cannot detect orientation"); + mOrientationListener.disable(); + } + + private void updateTriStateLayout() { + if (mContext != null) { + int iconId = 0; + int textId = 0; + int bg = 0; + Resources res = mContext.getResources(); + if (res != null) { + int positionY; + int positionY2 = mWindowLayoutParams.y; + int positionX = mWindowLayoutParams.x; + int gravity = mWindowLayoutParams.gravity; + switch (mTriStateMode) { + case MODE_RING: + case MODE_NONE: + case RINGER_MODE_NORMAL: + iconId = R.drawable.ic_volume_ringer; + textId = R.string.volume_ringer_status_normal; + break; + case MODE_VIBRATE: + case RINGER_MODE_VIBRATE: + iconId = R.drawable.ic_volume_ringer_vibrate; + textId = R.string.volume_ringer_status_vibrate; + break; + case MODE_SILENT: + case RINGER_MODE_SILENT: + iconId = R.drawable.ic_volume_ringer_mute; + textId = R.string.volume_ringer_status_silent; + break; + case MODE_PRIORITY_ONLY: + iconId = R.drawable.ic_qs_dnd_on; + textId = R.string.volume_ringer_priority_only; + break; + case MODE_ALARMS_ONLY: + iconId = R.drawable.ic_qs_dnd_on; + textId = R.string.volume_ringer_alarms_only; + break; + case MODE_TOTAL_SILENCE: + iconId = R.drawable.ic_qs_dnd_on; + textId = R.string.volume_ringer_dnd; + break; + case MODE_FLASHLIGHT_ON: + iconId = R.drawable.ic_tristate_flashlight; + textId = R.string.tristate_flashlight_on; + break; + case MODE_FLASHLIGHT_OFF: + iconId = R.drawable.ic_tristate_flashlight_off; + textId = R.string.tristate_flashlight_off; + break; + case MODE_FLASHLIGHT_BLINK: + iconId = R.drawable.ic_tristate_flashlight; + textId = R.string.tristate_flashlight_blink; + break; + case MODE_BRIGHTNESS_BRIGHT: + iconId = R.drawable.ic_tristate_brightness_bright; + textId = R.string.tristate_brightness_bright; + break; + case MODE_BRIGHTNESS_DARK: + iconId = R.drawable.ic_tristate_brightness_dark; + textId = R.string.tristate_brightness_dark; + break; + case MODE_BRIGHTNESS_AUTO: + iconId = R.drawable.ic_tristate_brightness_auto; + textId = R.string.tristate_brightness_auto; + break; + case MODE_ROTATION_AUTO: + iconId = R.drawable.ic_tristate_rotate_auto; + textId = R.string.tristate_rotation_auto; + break; + case MODE_ROTATION_0: + iconId = R.drawable.ic_tristate_rotate_portrait; + textId = R.string.tristate_rotation_0; + break; + case MODE_ROTATION_90: + iconId = R.drawable.ic_tristate_rotate_landscape; + textId = R.string.tristate_rotation_90; + break; + case MODE_ROTATION_270: + iconId = R.drawable.ic_tristate_rotate_landscape; + textId = R.string.tristate_rotation_270; + break; + } + + int triStatePos = res.getInteger(R.integer.config_alertSliderLocation); + boolean isTsKeyRight = true; + if (triStatePos == TRI_STATE_UI_POSITION_LEFT) { + isTsKeyRight = false; + } else if (triStatePos == TRI_STATE_UI_POSITION_RIGHT) { + isTsKeyRight = true; + } + switch (mOrientationType) { + case ROTATION_90: + if (isTsKeyRight) { + gravity = 51; + } else { + gravity = 83; + } + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_deep_land); + if (isTsKeyRight) { + positionY2 += res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); + } + if (mPosition == POSITION_TOP) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_l); + } else if (mPosition == POSITION_MIDDLE) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position_l); + } else if (mPosition == POSITION_BOTTOM) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_SILENT) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_VIBRATE) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_NORMAL) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position_l); + } + bg = R.drawable.dialog_tri_state_middle_bg; + break; + case ROTATION_180: + if (isTsKeyRight) { + gravity = 83; + } else { + gravity = 85; + } + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_deep); + positionY = res.getDimensionPixelSize(R.dimen.status_bar_height); + if (mPosition == POSITION_TOP) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position); + bg = !isTsKeyRight ? R.drawable.right_dialog_tri_state_down_bg : R.drawable.left_dialog_tri_state_down_bg; + } else if (mPosition == POSITION_MIDDLE) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position); + bg = R.drawable.dialog_tri_state_middle_bg; + } else if (mPosition == POSITION_BOTTOM) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position); + bg = !isTsKeyRight ? R.drawable.right_dialog_tri_state_up_bg : R.drawable.left_dialog_tri_state_up_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_SILENT) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position); + bg = !isTsKeyRight ? R.drawable.right_dialog_tri_state_down_bg : R.drawable.left_dialog_tri_state_down_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_VIBRATE) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position); + bg = R.drawable.dialog_tri_state_middle_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_NORMAL) { + positionY += res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position); + bg = !isTsKeyRight ? R.drawable.right_dialog_tri_state_up_bg : R.drawable.left_dialog_tri_state_up_bg; + } + positionY2 = positionY; + break; + case ROTATION_270: + if (isTsKeyRight) { + gravity = 85; + } else { + gravity = 53; + } + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_deep_land); + if (!isTsKeyRight) { + positionY2 += res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); + } + if (mPosition == POSITION_TOP) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_l); + } else if (mPosition == POSITION_MIDDLE) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position_l); + } else if (mPosition == POSITION_BOTTOM) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_SILENT) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_VIBRATE) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position_l); + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_NORMAL) { + positionX = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position_l); + } + bg = R.drawable.dialog_tri_state_middle_bg; + break; + default: + if (isTsKeyRight) { + gravity = 53; + } else { + gravity = 51; + } + positionX = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position_deep); + if (mPosition == POSITION_TOP) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position); + bg = isTsKeyRight ? R.drawable.right_dialog_tri_state_up_bg : R.drawable.left_dialog_tri_state_up_bg; + } else if (mPosition == POSITION_MIDDLE) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position); + bg = R.drawable.dialog_tri_state_middle_bg; + } else if (mPosition == POSITION_BOTTOM) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position); + bg = isTsKeyRight ? R.drawable.right_dialog_tri_state_down_bg : R.drawable.left_dialog_tri_state_down_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_SILENT) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_up_dialog_position); + bg = isTsKeyRight ? R.drawable.right_dialog_tri_state_up_bg : R.drawable.left_dialog_tri_state_up_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_VIBRATE) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_middle_dialog_position); + bg = R.drawable.dialog_tri_state_middle_bg; + } else if (!mIntentActionSupported && mTriStateMode == RINGER_MODE_NORMAL) { + positionY2 = res.getDimensionPixelSize(R.dimen.tri_state_down_dialog_position); + bg = isTsKeyRight ? R.drawable.right_dialog_tri_state_down_bg : R.drawable.left_dialog_tri_state_down_bg; + } + positionY2 += res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); + break; + } + if (mTriStateMode != -1) { + if (mTriStateIcon != null && iconId != 0) { + mTriStateIcon.setImageResource(iconId); + } + if (mTriStateText != null && textId != 0) { + String inputText = res.getString(textId); + if (inputText != null && mTriStateText.length() == inputText.length()) { + StringBuilder sb = new StringBuilder(); + sb.append(inputText); + sb.append(" "); + inputText = sb.toString(); + } + mTriStateText.setText(inputText); + } + if (mDialogView != null && bg != 0) { + mDialogView.setBackgroundDrawable(res.getDrawable(bg)); + mBackgroundColor = getAttrColor(com.android.internal.R.attr.colorSurface); + mDialogView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); + } + mDialogPosition = positionY2; + } + + final float scaleFactor = DisplayUtils.getScaleFactor(mContext); + + positionY = res.getDimensionPixelSize(R.dimen.tri_state_dialog_padding); + mWindowLayoutParams.gravity = gravity; + mWindowLayoutParams.y = (int) ((positionY2 - positionY) * scaleFactor); + mWindowLayoutParams.x = (int) ((positionX - positionY) * scaleFactor); + mWindow.setAttributes(mWindowLayoutParams); + mHandler.sendEmptyMessageDelayed(MSG_RESET_SCHEDULE, DIALOG_TIMEOUT); + } + } + } + + private void handleShow() { + mHandler.removeMessages(MSG_DIALOG_SHOW); + if (!mShowing) { + registerOrientationListener(true); + checkOrientationType(); + mShowing = true; + mDialog.show(); + if (mListener != null) { + mListener.onTriStateUserActivity(); + } + mHandler.sendEmptyMessageDelayed(MSG_RESET_SCHEDULE, DIALOG_TIMEOUT); + } + } + + private void handleDismiss() { + mHandler.removeMessages(MSG_DIALOG_DISMISS); + if (mShowing) { + registerOrientationListener(false); + mShowing = false; + mDialog.dismiss(); + } + } + + private void handleStateChanged() { + mHandler.removeMessages(MSG_STATE_CHANGE); + if (mIntentActionSupported && mPositionValue != mTriStateMode) { + mTriStateMode = mPositionValue; + updateTriStateLayout(); + if (mListener != null) { + mListener.onTriStateUserActivity(); + } + } else if (!mIntentActionSupported) { + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + int ringerMode = am.getRingerModeInternal(); + if (ringerMode != mTriStateMode) { + mTriStateMode = ringerMode; + updateTriStateLayout(); + if (mListener != null) { + mListener.onTriStateUserActivity(); + } + } + } + } + + public void handleResetTimeout() { + mHandler.removeMessages(MSG_RESET_SCHEDULE); + mHandler.sendEmptyMessage(MSG_DIALOG_DISMISS); + if (mListener != null) { + mListener.onTriStateUserActivity(); + } + } + + @Override + public void onDensityOrFontScaleChanged() { + mHandler.sendEmptyMessage(MSG_DIALOG_DISMISS); + initDialog(); + updateTriStateLayout(); + } + + public int getAttrColor(int attr) { + TypedArray ta = mContext.obtainStyledAttributes(new int[]{attr}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + return colorAccent; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java index a4df2fc50bfc..bf18b2cd7a93 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -30,11 +30,14 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.tristate.TriStateUiController; +import com.android.systemui.tristate.TriStateUiControllerImpl; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; +import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ExtensionController; import com.android.systemui.tuner.TunerService; @@ -49,7 +52,7 @@ */ @SysUISingleton public class VolumeDialogComponent implements VolumeComponent, TunerService.Tunable, - VolumeDialogControllerImpl.UserActivityListener{ + VolumeDialogControllerImpl.UserActivityListener, TriStateUiController.UserActivityListener { public static final String VOLUME_DOWN_SILENT = "sysui_volume_down_silent"; public static final String VOLUME_UP_SILENT = "sysui_volume_up_silent"; @@ -66,6 +69,7 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna protected final Context mContext; private final VolumeDialogControllerImpl mController; + private TriStateUiControllerImpl mTriStateController; private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_LOCALE | ActivityInfo.CONFIG_ASSETS_PATHS | ActivityInfo.CONFIG_UI_MODE); @@ -80,6 +84,7 @@ public VolumeDialogComponent( KeyguardViewMediator keyguardViewMediator, ActivityStarter activityStarter, VolumeDialogControllerImpl volumeDialogController, + ConfigurationController configurationController, DemoModeController demoModeController, PluginDependencyProvider pluginDependencyProvider, ExtensionController extensionController, @@ -90,6 +95,8 @@ public VolumeDialogComponent( mActivityStarter = activityStarter; mController = volumeDialogController; mController.setUserActivityListener(this); + boolean hasAlertSlider = mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_hasAlertSlider); // Allow plugins to reference the VolumeDialogController. pluginDependencyProvider.allowPluginDependency(VolumeDialogController.class); extensionController.newExtension(VolumeDialog.class) @@ -101,6 +108,14 @@ public VolumeDialogComponent( } mDialog = dialog; mDialog.init(LayoutParams.TYPE_VOLUME_OVERLAY, mVolumeDialogCallback); + if (hasAlertSlider) { + if (mTriStateController != null) { + mTriStateController.destroy(); + } + mTriStateController = new TriStateUiControllerImpl(mContext, + volumeDialogController, configurationController, tunerService); + mTriStateController.init(LayoutParams.TYPE_VOLUME_OVERLAY, this); + } }).build(); @@ -201,6 +216,11 @@ private void startSettings(Intent intent) { mActivityStarter.startActivity(intent, true /* onlyProvisioned */, true /* dismissShade */); } + @Override + public void onTriStateUserActivity() { + onUserActivity(); + } + private final VolumeDialogImpl.Callback mVolumeDialogCallback = new VolumeDialogImpl.Callback() { @Override public void onZenSettingsClicked() { From fcb5d36423c0894138fc1a0886c2e14861a9869d Mon Sep 17 00:00:00 2001 From: Oliver Scott Date: Thu, 26 Dec 2024 18:16:51 -0500 Subject: [PATCH 0325/1315] Support dark mode for default theme Theme.DeviceDefault.System is the default theme in Android. Fully support dark theme by inheriting Theme.DeviceDefault.DayNight in dark mode Issue: calyxos#2891 Change-Id: I1b8a81a0cf1ced6ea33faa5371aea2c6511bdd17 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/content/res/Resources.java | 2 +- core/res/AndroidManifest.xml | 2 +- core/res/res/values-night/themes_device_defaults.xml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 52958ee41a66..bb160f7ec23d 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -213,7 +213,7 @@ public static int selectDefaultTheme(int curTheme, int targetSdkVersion) { com.android.internal.R.style.Theme, com.android.internal.R.style.Theme_Holo, com.android.internal.R.style.Theme_DeviceDefault, - com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar); + com.android.internal.R.style.Theme_DeviceDefault_System); } /** @hide */ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 769c3d3110df..c18048f512ae 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -9505,7 +9505,7 @@ android:killAfterRestore="false" android:icon="@drawable/ic_launcher_android" android:supportsRtl="true" - android:theme="@style/Theme.DeviceDefault.Light.DarkActionBar" + android:theme="@style/Theme.DeviceDefault.System" android:defaultToDeviceProtectedStorage="true" android:forceQueryable="true" android:directBootAware="true"> diff --git a/core/res/res/values-night/themes_device_defaults.xml b/core/res/res/values-night/themes_device_defaults.xml index 7cfdba7a65be..88e474853bd0 100644 --- a/core/res/res/values-night/themes_device_defaults.xml +++ b/core/res/res/values-night/themes_device_defaults.xml @@ -104,4 +104,6 @@ easier. + + + + + diff --git a/core/res/res/values/matrixx_strings.xml b/core/res/res/values/matrixx_strings.xml index 3cc52bc95556..e9a9796d2f74 100644 --- a/core/res/res/values/matrixx_strings.xml +++ b/core/res/res/values/matrixx_strings.xml @@ -27,4 +27,12 @@ URL copied successfully An error occured while uploading the log to Pasty + + Press and hold power button to unlock + Pocket mode is on + To use your phone: + Check if the blue area of your phone is clear of any obstructions, and clean off any dirt or dust. + Long press the power button to force quit pocket mode. + 1.\u0020 + 2.\u0020 diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 7afa344596f1..dd9181d8aa2f 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -63,4 +63,16 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e652804323b5..a7260e4d6d6f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -94,6 +94,8 @@ import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.pocket.IPocketCallback; +import android.pocket.PocketManager; import android.provider.Settings; import android.service.dreams.IDreamManager; import android.telephony.CarrierConfigManager; @@ -241,6 +243,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, CoreSt private static final int MSG_SERVICE_PROVIDERS_UPDATED = 347; private static final int MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED = 348; + // Additional messages should be 600+ + private static final int MSG_POCKET_STATE_CHANGED = 600; + /** Biometric authentication state: Not listening. */ @VisibleForTesting protected static final int BIOMETRIC_STATE_STOPPED = 0; @@ -448,6 +453,26 @@ protected Handler getHandler() { } private final Handler mHandler; + private PocketManager mPocketManager; + private boolean mIsDeviceInPocket; + private final IPocketCallback mPocketCallback = new IPocketCallback.Stub() { + @Override + public void onStateChanged(boolean isDeviceInPocket, int reason) { + boolean wasInPocket = mIsDeviceInPocket; + if (reason == PocketManager.REASON_SENSOR) { + mIsDeviceInPocket = isDeviceInPocket; + } else { + mIsDeviceInPocket = false; + } + if (wasInPocket != mIsDeviceInPocket) { + mHandler.sendEmptyMessage(MSG_POCKET_STATE_CHANGED); + } + } + }; + + public boolean isPocketLockVisible(){ + return mPocketManager.isPocketLockVisible(); + } private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = new IBiometricEnabledOnKeyguardCallback.Stub() { @@ -2326,6 +2351,11 @@ protected KeyguardUpdateMonitor( mCommunalSceneInteractor = communalSceneInteractor; mKeyguardServiceShowLockscreenInteractor = keyguardServiceShowLockscreenInteractor; + mPocketManager = (PocketManager) context.getSystemService(Context.POCKET_SERVICE); + if (mPocketManager != null) { + mPocketManager.addCallback(mPocketCallback); + } + mHandler = new Handler(mainLooper) { @Override public void handleMessage(Message msg) { @@ -2430,6 +2460,9 @@ public void handleMessage(Message msg) { case MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED: notifyAboutEnrollmentChange(msg.arg1); break; + case MSG_POCKET_STATE_CHANGED: + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + break; default: super.handleMessage(msg); break; @@ -3163,7 +3196,7 @@ protected boolean shouldListenForFingerprint(boolean isUdfps) { boolean shouldListen = shouldListenKeyguardState && shouldListenUserState && shouldListenBouncerState && shouldListenUdfpsState && !mBiometricPromptShowing - && shouldListenSecureLockDeviceState && shouldListenFpsState; + && shouldListenSecureLockDeviceState && shouldListenFpsState && !mIsDeviceInPocket; logListenerModelData( new KeyguardFingerprintListenModel( System.currentTimeMillis(), diff --git a/services/core/java/com/android/server/pocket/PocketBridgeService.java b/services/core/java/com/android/server/pocket/PocketBridgeService.java new file mode 100644 index 000000000000..5fc5e2721cc4 --- /dev/null +++ b/services/core/java/com/android/server/pocket/PocketBridgeService.java @@ -0,0 +1,184 @@ +/** + * Copyright (C) 2017 The ParanoidAndroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pocket; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.UserHandle; +import android.pocket.IPocketCallback; +import android.pocket.PocketManager; +import android.provider.Settings.System; +import android.util.Slog; +import com.android.internal.util.FastPrintWriter; +import com.android.server.SystemService; + +import static android.provider.Settings.System.POCKET_JUDGE; + +/** + * This service communicates pocket state to the pocket judge kernel driver. + * It maintains the pocket state by binding to the pocket service. + * + * @author Chris Lahaye + * @hide + */ +public class PocketBridgeService extends SystemService { + + private static final String TAG = PocketBridgeService.class.getSimpleName(); + private static final int MSG_POCKET_STATE_CHANGED = 1; + + private Context mContext; + private boolean mEnabled; + private PocketBridgeHandler mHandler; + private PocketBridgeObserver mObserver; + + private PocketManager mPocketManager; + private boolean mIsDeviceInPocket; + private final IPocketCallback mPocketCallback = new IPocketCallback.Stub() { + @Override + public void onStateChanged(boolean isDeviceInPocket, int reason) { + boolean changed = false; + if (reason == PocketManager.REASON_SENSOR) { + if (isDeviceInPocket != mIsDeviceInPocket) { + mIsDeviceInPocket = isDeviceInPocket; + changed = true; + } + } else { + changed = isDeviceInPocket != mIsDeviceInPocket; + mIsDeviceInPocket = false; + } + if (changed) { + mHandler.sendEmptyMessage(MSG_POCKET_STATE_CHANGED); + } + } + }; + + // Custom methods + private boolean mSupportedByDevice; + + public PocketBridgeService(Context context) { + super(context); + mContext = context; + HandlerThread handlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + mHandler = new PocketBridgeHandler(handlerThread.getLooper()); + mPocketManager = (PocketManager) + context.getSystemService(Context.POCKET_SERVICE); + mSupportedByDevice = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_pocketModeSupported); + mObserver = new PocketBridgeObserver(mHandler); + if (mSupportedByDevice){ + mObserver.onChange(true); + mObserver.register(); + } + } + + @Override + public void onStart() { + } + + private void setEnabled(boolean enabled) { + if (enabled != mEnabled) { + mEnabled = enabled; + update(); + } + } + + private void update() { + if (!mSupportedByDevice || mPocketManager == null) return; + + if (mEnabled) { + mPocketManager.addCallback(mPocketCallback); + } else { + mPocketManager.removeCallback(mPocketCallback); + } + } + + private class PocketBridgeHandler extends Handler { + + private FileOutputStream mFileOutputStream; + private FastPrintWriter mPrintWriter; + + public PocketBridgeHandler(Looper looper) { + super(looper); + + try { + mFileOutputStream = new FileOutputStream( + mContext.getResources().getString( + com.android.internal.R.string.config_pocketBridgeSysfsInpocket) + ); + mPrintWriter = new FastPrintWriter(mFileOutputStream, true, 128); + } + catch(FileNotFoundException e) { + Slog.w(TAG, "Pocket bridge error occured", e); + setEnabled(false); + } + } + + @Override + public void handleMessage(android.os.Message msg) { + if (msg.what != MSG_POCKET_STATE_CHANGED) { + Slog.w(TAG, "Unknown message:" + msg.what); + return; + } + + if (mPrintWriter != null) { + mPrintWriter.println(mIsDeviceInPocket ? 1 : 0); + } + } + + } + + private class PocketBridgeObserver extends ContentObserver { + + private boolean mRegistered; + + public PocketBridgeObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + final boolean enabled = System.getIntForUser(mContext.getContentResolver(), + POCKET_JUDGE, 0 /* default */, UserHandle.USER_CURRENT) != 0; + setEnabled(enabled); + } + + public void register() { + if (!mRegistered) { + mContext.getContentResolver().registerContentObserver( + System.getUriFor(POCKET_JUDGE), true, this); + mRegistered = true; + } + } + + public void unregister() { + if (mRegistered) { + mContext.getContentResolver().unregisterContentObserver(this); + mRegistered = false; + } + } + + } + +} diff --git a/services/core/java/com/android/server/pocket/PocketService.java b/services/core/java/com/android/server/pocket/PocketService.java new file mode 100644 index 000000000000..28a757e5a7b3 --- /dev/null +++ b/services/core/java/com/android/server/pocket/PocketService.java @@ -0,0 +1,914 @@ +/** + * Copyright (C) 2016 The ParanoidAndroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pocket; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Binder; +import android.os.DeadObjectException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.pocket.IPocketService; +import android.pocket.IPocketCallback; +import android.pocket.PocketConstants; +import android.pocket.PocketManager; +import android.provider.Settings.System; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.server.SystemService; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +import static android.provider.Settings.System.POCKET_JUDGE; + +/** + * A service to manage multiple clients that want to listen for pocket state. + * The service is responsible for maintaining a list of clients and dispatching all + * pocket -related information. + * + * @author Carlo Savignano + * @hide + */ +public class PocketService extends SystemService implements IBinder.DeathRecipient { + + private static final String TAG = PocketService.class.getSimpleName(); + private static final boolean DEBUG = PocketConstants.DEBUG; + + /** + * Wheater we don't have yet a valid vendor sensor event or pocket service not running. + */ + private static final int VENDOR_SENSOR_UNKNOWN = 0; + + /** + * Vendor sensor has been registered, onSensorChanged() has been called and we have a + * valid event value from Vendor pocket sensor. + */ + private static final int VENDOR_SENSOR_IN_POCKET = 1; + + /** + * The rate proximity sensor events are delivered at. + */ + private static final int PROXIMITY_SENSOR_DELAY = 400000; + + /** + * Wheater we don't have yet a valid proximity sensor event or pocket service not running. + */ + private static final int PROXIMITY_UNKNOWN = 0; + + /** + * Proximity sensor has been registered, onSensorChanged() has been called and we have a + * valid event value which determined proximity sensor is covered. + */ + private static final int PROXIMITY_POSITIVE = 1; + + /** + * Proximity sensor has been registered, onSensorChanged() has been called and we have a + * valid event value which determined proximity sensor is not covered. + */ + private static final int PROXIMITY_NEGATIVE = 2; + + /** + * The rate light sensor events are delivered at. + */ + private static final int LIGHT_SENSOR_DELAY = 400000; + + /** + * Wheater we don't have yet a valid light sensor event or pocket service not running. + */ + private static final int LIGHT_UNKNOWN = 0; + + /** + * Light sensor has been registered, onSensorChanged() has been called and we have a + * valid event value which determined available light is in pocket range. + */ + private static final int LIGHT_POCKET = 1; + + /** + * Light sensor has been registered, onSensorChanged() has been called and we have a + * valid event value which determined available light is outside pocket range. + */ + private static final int LIGHT_AMBIENT = 2; + + /** + * Light sensor maximum value registered in pocket with up to semi-transparent fabric. + */ + private static final float POCKET_LIGHT_MAX_THRESHOLD = 3.0f; + + private final ArrayList mCallbacks= new ArrayList<>(); + + private Context mContext; + private boolean mEnabled; + private boolean mSystemReady; + private boolean mSystemBooted; + private boolean mInteractive; + private boolean mPending; + private PocketHandler mHandler; + private PocketObserver mObserver; + private SensorManager mSensorManager; + + // proximity + private int mProximityState = PROXIMITY_UNKNOWN; + private int mLastProximityState = PROXIMITY_UNKNOWN; + private float mProximityMaxRange; + private boolean mProximityRegistered; + private Sensor mProximitySensor; + + // light + private int mLightState = LIGHT_UNKNOWN; + private int mLastLightState = LIGHT_UNKNOWN; + private float mLightMaxRange; + private boolean mLightRegistered; + private Sensor mLightSensor; + + // vendor sensor + private int mVendorSensorState = VENDOR_SENSOR_UNKNOWN; + private int mLastVendorSensorState = VENDOR_SENSOR_UNKNOWN; + private String mVendorPocketSensor; + private boolean mVendorSensorRegistered; + private Sensor mVendorSensor; + + // Custom methods + private boolean mPocketLockVisible; + private boolean mSupportedByDevice; + + public PocketService(Context context) { + super(context); + mContext = context; + HandlerThread handlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + mHandler = new PocketHandler(handlerThread.getLooper()); + mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + mVendorPocketSensor = mContext.getResources().getString( + com.android.internal.R.string.config_pocketJudgeVendorSensorName); + String vendorProximitySensor = mContext.getResources().getString( + com.android.internal.R.string.config_pocketJudgeVendorProximitySensorName); + if (vendorProximitySensor != null && !vendorProximitySensor.isEmpty()) { + mProximitySensor = getSensor(mSensorManager, vendorProximitySensor); + } else { + mProximitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + } + if (mProximitySensor != null) { + mProximityMaxRange = mProximitySensor.getMaximumRange(); + } + mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); + if (mLightSensor != null) { + mLightMaxRange = mLightSensor.getMaximumRange(); + } + mVendorSensor = getSensor(mSensorManager, mVendorPocketSensor); + mSupportedByDevice = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_pocketModeSupported); + mObserver = new PocketObserver(mHandler); + if (mSupportedByDevice){ + mObserver.onChange(true); + mObserver.register(); + } + } + + private class PocketObserver extends ContentObserver { + + private boolean mRegistered; + + public PocketObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + final boolean enabled = System.getIntForUser(mContext.getContentResolver(), + POCKET_JUDGE, 0 /* default */, UserHandle.USER_CURRENT) != 0; + setEnabled(enabled); + } + + public void register() { + if (!mRegistered) { + mContext.getContentResolver().registerContentObserver( + System.getUriFor(POCKET_JUDGE), true, this); + mRegistered = true; + } + } + + public void unregister() { + if (mRegistered) { + mContext.getContentResolver().unregisterContentObserver(this); + mRegistered = false; + } + } + + } + + private class PocketHandler extends Handler { + + public static final int MSG_SYSTEM_READY = 0; + public static final int MSG_SYSTEM_BOOTED = 1; + public static final int MSG_DISPATCH_CALLBACKS = 2; + public static final int MSG_ADD_CALLBACK = 3; + public static final int MSG_REMOVE_CALLBACK = 4; + public static final int MSG_INTERACTIVE_CHANGED = 5; + public static final int MSG_SENSOR_EVENT_PROXIMITY = 6; + public static final int MSG_SENSOR_EVENT_LIGHT = 7; + public static final int MSG_UNREGISTER_TIMEOUT = 8; + public static final int MSG_SET_LISTEN_EXTERNAL = 9; + public static final int MSG_SET_POCKET_LOCK_VISIBLE = 10; + public static final int MSG_SENSOR_EVENT_VENDOR = 11; + + public PocketHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_SYSTEM_READY: + handleSystemReady(); + break; + case MSG_SYSTEM_BOOTED: + handleSystemBooted(); + break; + case MSG_DISPATCH_CALLBACKS: + handleDispatchCallbacks(); + break; + case MSG_ADD_CALLBACK: + handleAddCallback((IPocketCallback) msg.obj); + break; + case MSG_REMOVE_CALLBACK: + handleRemoveCallback((IPocketCallback) msg.obj); + break; + case MSG_INTERACTIVE_CHANGED: + handleInteractiveChanged(msg.arg1 != 0); + break; + case MSG_SENSOR_EVENT_PROXIMITY: + handleProximitySensorEvent((SensorEvent) msg.obj); + break; + case MSG_SENSOR_EVENT_LIGHT: + handleLightSensorEvent((SensorEvent) msg.obj); + break; + case MSG_SENSOR_EVENT_VENDOR: + handleVendorSensorEvent((SensorEvent) msg.obj); + break; + case MSG_UNREGISTER_TIMEOUT: + handleUnregisterTimeout(); + break; + case MSG_SET_LISTEN_EXTERNAL: + handleSetListeningExternal(msg.arg1 != 0); + break; + case MSG_SET_POCKET_LOCK_VISIBLE: + handleSetPocketLockVisible(msg.arg1 != 0); + break; + default: + Slog.w(TAG, "Unknown message:" + msg.what); + } + } + } + + @Override + public void onBootPhase(int phase) { + switch(phase) { + case PHASE_SYSTEM_SERVICES_READY: + mHandler.sendEmptyMessage(PocketHandler.MSG_SYSTEM_READY); + break; + case PHASE_BOOT_COMPLETED: + mHandler.sendEmptyMessage(PocketHandler.MSG_SYSTEM_BOOTED); + break; + default: + Slog.w(TAG, "Un-handled boot phase:" + phase); + break; + } + } + + @Override + public void onStart() { + publishBinderService(Context.POCKET_SERVICE, new PocketServiceWrapper()); + } + + @Override + public void binderDied() { + synchronized (mCallbacks) { + mProximityState = PROXIMITY_UNKNOWN; + int callbacksSize = mCallbacks.size(); + for (int i = callbacksSize - 1; i >= 0; i--) { + if (mCallbacks.get(i) != null) { + try { + mCallbacks.get(i).onStateChanged(false, PocketManager.REASON_RESET); + } catch (DeadObjectException e) { + Slog.w(TAG, "Death object while invoking sendPocketState: ", e); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to invoke sendPocketState: ", e); + } + } + } + mCallbacks.clear(); + } + unregisterSensorListeners(); + mObserver.unregister(); + } + + private final class PocketServiceWrapper extends IPocketService.Stub { + + @Override // Binder call + public void addCallback(final IPocketCallback callback) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_ADD_CALLBACK; + msg.obj = callback; + mHandler.sendMessage(msg); + } + + @Override // Binder call + public void removeCallback(final IPocketCallback callback) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_REMOVE_CALLBACK; + msg.obj = callback; + mHandler.sendMessage(msg); + } + + @Override // Binder call + public void onInteractiveChanged(final boolean interactive) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_INTERACTIVE_CHANGED; + msg.arg1 = interactive ? 1 : 0; + mHandler.sendMessage(msg); + } + + @Override // Binder call + public void setListeningExternal(final boolean listen) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_SET_LISTEN_EXTERNAL; + msg.arg1 = listen ? 1 : 0; + mHandler.sendMessage(msg); + } + + @Override // Binder call + public boolean isDeviceInPocket() { + final long ident = Binder.clearCallingIdentity(); + try { + if (!mSystemReady || !mSystemBooted) { + return false; + } + return PocketService.this.isDeviceInPocket(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void setPocketLockVisible(final boolean visible) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_SET_POCKET_LOCK_VISIBLE; + msg.arg1 = visible ? 1 : 0; + mHandler.sendMessage(msg); + } + + @Override // Binder call + public boolean isPocketLockVisible() { + final long ident = Binder.clearCallingIdentity(); + try { + if (!mSystemReady || !mSystemBooted) { + return false; + } + return PocketService.this.isPocketLockVisible(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump Pocket from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + final long ident = Binder.clearCallingIdentity(); + try { + dumpInternal(pw); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + } + + private final SensorEventListener mProximityListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent sensorEvent) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_SENSOR_EVENT_PROXIMITY; + msg.obj = sensorEvent; + mHandler.sendMessage(msg); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { } + }; + + private final SensorEventListener mLightListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent sensorEvent) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_SENSOR_EVENT_LIGHT; + msg.obj = sensorEvent; + mHandler.sendMessage(msg); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { } + }; + + private final SensorEventListener mVendorSensorListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent sensorEvent) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_SENSOR_EVENT_VENDOR; + msg.obj = sensorEvent; + mHandler.sendMessage(msg); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { } + }; + + private boolean isDeviceInPocket() { + if (!mSupportedByDevice){ + return false; + } + + if (mVendorSensorState != VENDOR_SENSOR_UNKNOWN) { + return mVendorSensorState == VENDOR_SENSOR_IN_POCKET; + } + + if (mLightState != LIGHT_UNKNOWN) { + return mProximityState == PROXIMITY_POSITIVE + && mLightState == LIGHT_POCKET; + } + return mProximityState == PROXIMITY_POSITIVE; + } + + private void setEnabled(boolean enabled) { + if (!mSupportedByDevice){ + return; + } + if (enabled != mEnabled) { + mEnabled = enabled; + mHandler.removeCallbacksAndMessages(null); + update(); + } + } + + private void update() { + if (!mSupportedByDevice){ + return; + } + if (!mEnabled || mInteractive) { + if (mEnabled && isDeviceInPocket()) { + // if device is judged to be in pocket while switching + // to interactive state, we need to keep monitoring. + return; + } + unregisterSensorListeners(); + } else { + mHandler.removeMessages(PocketHandler.MSG_UNREGISTER_TIMEOUT); + registerSensorListeners(); + } + } + + private void registerSensorListeners() { + if (!mSupportedByDevice){ + return; + } + startListeningForVendorSensor(); + startListeningForProximity(); + startListeningForLight(); + } + + private void unregisterSensorListeners() { + if (!mSupportedByDevice){ + return; + } + stopListeningForVendorSensor(); + stopListeningForProximity(); + stopListeningForLight(); + } + + private void startListeningForVendorSensor() { + if (DEBUG) { + Log.d(TAG, "startListeningForVendorSensor()"); + } + + if (mVendorSensor == null) { + Log.d(TAG, "Cannot detect Vendor pocket sensor, sensor is NULL"); + return; + } + + if (!mVendorSensorRegistered) { + mSensorManager.registerListener(mVendorSensorListener, mVendorSensor, + SensorManager.SENSOR_DELAY_NORMAL, mHandler); + mVendorSensorRegistered = true; + } + } + + private void stopListeningForVendorSensor() { + if (DEBUG) { + Log.d(TAG, "stopListeningForVendorSensor()"); + } + + if (mVendorSensorRegistered) { + mVendorSensorState = mLastVendorSensorState = VENDOR_SENSOR_UNKNOWN; + mSensorManager.unregisterListener(mVendorSensorListener); + mVendorSensorRegistered = false; + } + } + + private void startListeningForProximity() { + + if (mVendorSensor != null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "startListeningForProximity()"); + } + + if (!PocketConstants.ENABLE_PROXIMITY_JUDGE) { + return; + } + + if (mProximitySensor == null) { + Log.d(TAG, "Cannot detect proximity sensor, sensor is NULL"); + return; + } + + if (!mProximityRegistered) { + mSensorManager.registerListener(mProximityListener, mProximitySensor, + PROXIMITY_SENSOR_DELAY, mHandler); + mProximityRegistered = true; + } + } + + private void stopListeningForProximity() { + if (DEBUG) { + Log.d(TAG, "startListeningForProximity()"); + } + + if (mProximityRegistered) { + mLastProximityState = mProximityState = PROXIMITY_UNKNOWN; + mSensorManager.unregisterListener(mProximityListener); + mProximityRegistered = false; + } + } + + private void startListeningForLight() { + boolean mUseLightSensor = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_pocketUseLightSensor); + + if (mVendorSensor != null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "startListeningForLight()"); + } + + if (!mUseLightSensor) { + return; + } + + if (mLightSensor == null) { + Log.d(TAG, "Cannot detect light sensor, sensor is NULL"); + return; + } + + if (!mLightRegistered) { + mSensorManager.registerListener(mLightListener, mLightSensor, + LIGHT_SENSOR_DELAY, mHandler); + mLightRegistered = true; + } + } + + private void stopListeningForLight() { + if (DEBUG) { + Log.d(TAG, "stopListeningForLight()"); + } + + if (mLightRegistered) { + mLightState = mLastLightState = LIGHT_UNKNOWN; + mSensorManager.unregisterListener(mLightListener); + mLightRegistered = false; + } + } + + private void handleSystemReady() { + if (DEBUG) { + Log.d(TAG, "onBootPhase(): PHASE_SYSTEM_SERVICES_READY"); + Log.d(TAG, "onBootPhase(): VENDOR_SENSOR: " + mVendorPocketSensor); + } + mSystemReady = true; + + if (mPending) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_INTERACTIVE_CHANGED; + msg.arg1 = mInteractive ? 1 : 0; + mHandler.sendMessage(msg); + mPending = false; + } + } + + private void handleSystemBooted() { + if (DEBUG) { + Log.d(TAG, "onBootPhase(): PHASE_BOOT_COMPLETED"); + } + mSystemBooted = true; + if (mPending) { + final Message msg = new Message(); + msg.what = PocketHandler.MSG_INTERACTIVE_CHANGED; + msg.arg1 = mInteractive ? 1 : 0; + mHandler.sendMessage(msg); + mPending = false; + } + } + + private void handleDispatchCallbacks() { + synchronized (mCallbacks) { + final int N = mCallbacks.size(); + boolean cleanup = false; + for (int i = 0; i < N; i++) { + final IPocketCallback callback = mCallbacks.get(i); + try { + if (callback != null) { + callback.onStateChanged(isDeviceInPocket(), PocketManager.REASON_SENSOR); + } else { + cleanup = true; + } + } catch (RemoteException e) { + cleanup = true; + } + } + if (cleanup) { + cleanUpCallbacksLocked(null); + } + } + } + + private void cleanUpCallbacksLocked(IPocketCallback callback) { + synchronized (mCallbacks) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + IPocketCallback found = mCallbacks.get(i); + if (found == null || found == callback) { + mCallbacks.remove(i); + } + } + } + } + + private void handleSetPocketLockVisible(boolean visible) { + mPocketLockVisible = visible; + } + + private boolean isPocketLockVisible() { + return mPocketLockVisible; + } + + private void handleSetListeningExternal(boolean listen) { + if (listen) { + // should prevent external processes to register while interactive, + // while they are allowed to stop listening in any case as for example + // coming pocket lock will need to. + if (!mInteractive) { + registerSensorListeners(); + } + } else { + mHandler.removeCallbacksAndMessages(null); + unregisterSensorListeners(); + } + dispatchCallbacks(); + } + + private void handleAddCallback(IPocketCallback callback) { + synchronized (mCallbacks) { + if (!mCallbacks.contains(callback)) { + mCallbacks.add(callback); + } + } + } + + private void handleRemoveCallback(IPocketCallback callback) { + synchronized (mCallbacks) { + if (mCallbacks.contains(callback)) { + mCallbacks.remove(callback); + } + } + } + + private void handleInteractiveChanged(boolean interactive) { + // always update interactive state. + mInteractive = interactive; + + if (mPending) { + // working on it, waiting for proper system conditions. + return; + } else if (!mPending && (!mSystemBooted || !mSystemReady)) { + // we ain't ready, postpone till system is both booted AND ready. + mPending = true; + return; + } + + update(); + } + + private void handleVendorSensorEvent(SensorEvent sensorEvent) { + final boolean isDeviceInPocket = isDeviceInPocket(); + + mLastVendorSensorState = mVendorSensorState; + + if (DEBUG) { + final String sensorEventToString = sensorEvent != null ? sensorEvent.toString() : "NULL"; + Log.d(TAG, "VENDOR_SENSOR: onSensorChanged(), sensorEvent =" + sensorEventToString); + } + + try { + if (sensorEvent == null) { + if (DEBUG) Log.d(TAG, "Event is null!"); + mVendorSensorState = VENDOR_SENSOR_UNKNOWN; + } else if (sensorEvent.values == null || sensorEvent.values.length == 0) { + if (DEBUG) Log.d(TAG, "Event has no values! event.values null ? " + (sensorEvent.values == null)); + mVendorSensorState = VENDOR_SENSOR_UNKNOWN; + } else { + final boolean isVendorPocket = sensorEvent.values[0] == 1.0; + if (DEBUG) { + final long time = SystemClock.uptimeMillis(); + Log.d(TAG, "Event: time=" + time + ", value=" + sensorEvent.values[0] + + ", isInPocket=" + isVendorPocket); + } + mVendorSensorState = isVendorPocket ? VENDOR_SENSOR_IN_POCKET : VENDOR_SENSOR_UNKNOWN; + } + } catch (NullPointerException e) { + Log.e(TAG, "Event: something went wrong, exception caught, e = " + e); + mVendorSensorState = VENDOR_SENSOR_UNKNOWN; + } finally { + if (isDeviceInPocket != isDeviceInPocket()) { + dispatchCallbacks(); + } + } + } + + private void handleLightSensorEvent(SensorEvent sensorEvent) { + final boolean isDeviceInPocket = isDeviceInPocket(); + + mLastLightState = mLightState; + + if (DEBUG) { + final String sensorEventToString = sensorEvent != null ? sensorEvent.toString() : "NULL"; + Log.d(TAG, "LIGHT_SENSOR: onSensorChanged(), sensorEvent =" + sensorEventToString); + } + + try { + if (sensorEvent == null) { + if (DEBUG) Log.d(TAG, "Event is null!"); + mLightState = LIGHT_UNKNOWN; + } else if (sensorEvent.values == null || sensorEvent.values.length == 0) { + if (DEBUG) Log.d(TAG, "Event has no values! event.values null ? " + (sensorEvent.values == null)); + mLightState = LIGHT_UNKNOWN; + } else { + final float value = sensorEvent.values[0]; + final boolean isPoor = value >= 0 + && value <= POCKET_LIGHT_MAX_THRESHOLD; + if (DEBUG) { + final long time = SystemClock.uptimeMillis(); + Log.d(TAG, "Event: time= " + time + ", value=" + value + + ", maxRange=" + mLightMaxRange + ", isPoor=" + isPoor); + } + mLightState = isPoor ? LIGHT_POCKET : LIGHT_AMBIENT; + } + } catch (NullPointerException e) { + Log.e(TAG, "Event: something went wrong, exception caught, e = " + e); + mLightState = LIGHT_UNKNOWN; + } finally { + if (isDeviceInPocket != isDeviceInPocket()) { + dispatchCallbacks(); + } + } + } + + private void handleProximitySensorEvent(SensorEvent sensorEvent) { + final boolean isDeviceInPocket = isDeviceInPocket(); + + mLastProximityState = mProximityState; + + if (DEBUG) { + final String sensorEventToString = sensorEvent != null ? sensorEvent.toString() : "NULL"; + Log.d(TAG, "PROXIMITY_SENSOR: onSensorChanged(), sensorEvent =" + sensorEventToString); + } + + try { + if (sensorEvent == null) { + if (DEBUG) Log.d(TAG, "Event is null!"); + mProximityState = PROXIMITY_UNKNOWN; + } else if (sensorEvent.values == null || sensorEvent.values.length == 0) { + if (DEBUG) Log.d(TAG, "Event has no values! event.values null ? " + (sensorEvent.values == null)); + mProximityState = PROXIMITY_UNKNOWN; + } else { + final float value = sensorEvent.values[0]; + final boolean isPositive = sensorEvent.values[0] < mProximityMaxRange; + if (DEBUG) { + final long time = SystemClock.uptimeMillis(); + Log.d(TAG, "Event: time=" + time + ", value=" + value + + ", maxRange=" + mProximityMaxRange + ", isPositive=" + isPositive); + } + mProximityState = isPositive ? PROXIMITY_POSITIVE : PROXIMITY_NEGATIVE; + } + } catch (NullPointerException e) { + Log.e(TAG, "Event: something went wrong, exception caught, e = " + e); + mProximityState = PROXIMITY_UNKNOWN; + } finally { + if (isDeviceInPocket != isDeviceInPocket()) { + dispatchCallbacks(); + } + } + } + + private void handleUnregisterTimeout() { + mHandler.removeCallbacksAndMessages(null); + unregisterSensorListeners(); + } + + private static Sensor getSensor(SensorManager sm, String type) { + for (Sensor sensor : sm.getSensorList(Sensor.TYPE_ALL)) { + if (type.equals(sensor.getStringType())) { + return sensor; + } + } + return null; + } + + private void dispatchCallbacks() { + final boolean isDeviceInPocket = isDeviceInPocket(); + if (mInteractive) { + if (!isDeviceInPocket) { + mHandler.sendEmptyMessageDelayed(PocketHandler.MSG_UNREGISTER_TIMEOUT, 5000 /* ms */); + } else { + mHandler.removeMessages(PocketHandler.MSG_UNREGISTER_TIMEOUT); + } + } + mHandler.removeMessages(PocketHandler.MSG_DISPATCH_CALLBACKS); + mHandler.sendEmptyMessage(PocketHandler.MSG_DISPATCH_CALLBACKS); + } + + private void dumpInternal(PrintWriter pw) { + JSONObject dump = new JSONObject(); + try { + dump.put("service", "POCKET"); + dump.put("enabled", mEnabled); + dump.put("isDeviceInPocket", isDeviceInPocket()); + dump.put("interactive", mInteractive); + dump.put("proximityState", mProximityState); + dump.put("lastProximityState", mLastProximityState); + dump.put("proximityRegistered", mProximityRegistered); + dump.put("proximityMaxRange", mProximityMaxRange); + dump.put("lightState", mLightState); + dump.put("lastLightState", mLastLightState); + dump.put("lightRegistered", mLightRegistered); + dump.put("lightMaxRange", mLightMaxRange); + dump.put("VendorSensorState", mVendorSensorState); + dump.put("lastVendorSensorState", mLastVendorSensorState); + dump.put("VendorSensorRegistered", mVendorSensorRegistered); + } catch (JSONException e) { + Slog.e(TAG, "dump formatting failure", e); + } finally { + pw.println(dump); + } + } +} diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index c2efd10c4856..51f830e8440a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -192,6 +192,8 @@ import android.os.UserHandle; import android.os.VibrationAttributes; import android.os.Vibrator; +import android.pocket.IPocketCallback; +import android.pocket.PocketManager; import android.provider.MediaStore; import android.provider.Settings; import android.provider.Settings.Secure; @@ -263,6 +265,7 @@ import com.android.server.policy.keyguard.KeyguardServiceDelegate; import com.android.server.policy.keyguard.KeyguardServiceDelegate.DrawnListener; import com.android.server.policy.keyguard.KeyguardStateMonitor.StateCallback; +import com.android.server.policy.pocket.PocketLock; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.vr.VrManagerInternal; import com.android.server.wallpaper.WallpaperManagerInternal; @@ -304,6 +307,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final String TAG = "WindowManager"; static final boolean localLOGV = false; + static final boolean DEBUG = false; static final boolean DEBUG_INPUT = false; static final boolean DEBUG_KEYGUARD = false; static final boolean DEBUG_WAKEUP = false; @@ -344,6 +348,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final int LONG_PRESS_POWER_ASSISTANT = 5; // Settings.Secure.ASSISTANT static final int LONG_PRESS_POWER_GO_TO_SLEEP = 6; static final int LONG_PRESS_POWER_TORCH = 7; + static final int LONG_PRESS_POWER_HIDE_POCKET_LOCK = 8; // must match: config_veryLongPresOnPowerBehavior in config.xml // The config value can be overridden using Settings.Global.POWER_BUTTON_VERY_LONG_PRESS @@ -847,6 +852,30 @@ public void onDrawn() { private ThreeFingersSwipeListener mThreeFingersSwipe; private boolean mThreeFingersSwipeHasAction; + private PocketManager mPocketManager; + private PocketLock mPocketLock; + private boolean mPocketLockShowing; + private boolean mIsDeviceInPocket; + private final IPocketCallback mPocketCallback = new IPocketCallback.Stub() { + + @Override + public void onStateChanged(boolean isDeviceInPocket, int reason) { + boolean wasDeviceInPocket = mIsDeviceInPocket; + if (reason == PocketManager.REASON_SENSOR) { + mIsDeviceInPocket = isDeviceInPocket; + } else { + mIsDeviceInPocket = false; + } + if (wasDeviceInPocket != mIsDeviceInPocket) { + handleDevicePocketStateChanged(); + //if (mKeyHandler != null) { + //mKeyHandler.setIsInPocket(mIsDeviceInPocket); + //} + } + } + + }; + private class PolicyHandler extends Handler { private PolicyHandler(Looper looper) { @@ -1740,6 +1769,12 @@ private void powerLongPress(long eventTime) { msg.setAsynchronous(true); msg.sendToTarget(); break; + case LONG_PRESS_POWER_HIDE_POCKET_LOCK: + mPowerKeyHandled = true; + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Power - Long-Press - Hide Pocket Lock"); + hidePocketLock(true); + mPocketManager.setListeningExternal(false); + break; } } @@ -1855,6 +1890,10 @@ private int getResolvedLongPressOnPowerBehavior() { return LONG_PRESS_POWER_TORCH; } + if (mPocketLockShowing) { + return LONG_PRESS_POWER_HIDE_POCKET_LOCK; + } + // If the config indicates the assistant behavior but the device isn't yet provisioned, show // global actions instead. if (mLongPressOnPowerBehavior == LONG_PRESS_POWER_ASSISTANT && @@ -5083,6 +5122,24 @@ && isWakeKeyWhenScreenOff(keyCode)) { } final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; + + // Pre-basic policy based on interactive and pocket lock state. + if (mIsDeviceInPocket && (!interactive || mPocketLockShowing)) { + if (keyCode != KeyEvent.KEYCODE_POWER && + keyCode != KeyEvent.KEYCODE_VOLUME_UP && + keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && + keyCode != KeyEvent.KEYCODE_MEDIA_PLAY && + keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE && + keyCode != KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE && + keyCode != KeyEvent.KEYCODE_HEADSETHOOK && + keyCode != KeyEvent.KEYCODE_MEDIA_STOP && + keyCode != KeyEvent.KEYCODE_MEDIA_NEXT && + keyCode != KeyEvent.KEYCODE_MEDIA_PREVIOUS && + keyCode != KeyEvent.KEYCODE_VOLUME_MUTE) { + return 0; + } + } + final boolean canceled = event.isCanceled(); final int displayId = event.getDisplayId(); final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0; @@ -6294,6 +6351,9 @@ public void startedGoingToSleep(int displayGroupId, + displayGroupId); } } + if (mPocketManager != null) { + mPocketManager.onInteractiveChanged(false); + } } // Called on the PowerManager's Notifier thread. @@ -6373,6 +6433,10 @@ public void startedWakingUp(int displayGroupId, @WakeReason int pmWakeReason) { } mPowerButtonLaunchGestureTriggered = false; + + if (mPocketManager != null) { + mPocketManager.onInteractiveChanged(true); + } } // Called on the PowerManager's Notifier thread. @@ -6671,6 +6735,72 @@ private void enableScreen(ScreenOnListener listener, boolean report) { } } + /** + * Perform operations if needed on pocket mode state changed. + * @see com.android.server.pocket.PocketService + * @see PocketLock + * @see this.mPocketCallback; + * @author Carlo Savignano + */ + private void handleDevicePocketStateChanged() { + final boolean interactive = mPowerManager.isInteractive(); + if (mIsDeviceInPocket) { + showPocketLock(interactive); + } else { + hidePocketLock(interactive); + } + } + + /** + * Check if we can show pocket lock once requested. + * @see com.android.server.pocket.PocketService + * @see PocketLock + * @see this.mPocketCallback; + * @author Carlo Savignano + */ + private void showPocketLock(boolean animate) { + if (!mSystemReady || !mSystemBooted || !mKeyguardDrawnOnce + || mPocketLock == null || mPocketLockShowing) { + return; + } + + if (mPowerManager.isInteractive() && !isKeyguardShowingAndNotOccluded()){ + return; + } + + if (DEBUG) { + Log.d(TAG, "showPocketLock, animate=" + animate); + } + + mPocketLock.show(animate); + mPocketLockShowing = true; + + mPocketManager.setPocketLockVisible(true); + } + + /** + * Check if we can hide pocket lock once requested. + * @see com.android.server.pocket.PocketService + * @see PocketLock + * @see this.mPocketCallback; + * @author Carlo Savignano + */ + private void hidePocketLock(boolean animate) { + if (!mSystemReady || !mSystemBooted || !mKeyguardDrawnOnce + || mPocketLock == null || !mPocketLockShowing) { + return; + } + + if (DEBUG) { + Log.d(TAG, "hidePocketLock, animate=" + animate); + } + + mPocketLock.hide(animate); + mPocketLockShowing = false; + + mPocketManager.setPocketLockVisible(false); + } + private void handleHideBootMessage() { synchronized (mLock) { if (!mKeyguardDrawnOnce) { @@ -6838,6 +6968,10 @@ public void systemReady() { // So it is better not to bind keyguard here. mKeyguardDelegate.onSystemReady(); + mPocketManager = (PocketManager) mContext.getSystemService(Context.POCKET_SERVICE); + mPocketManager.addCallback(mPocketCallback); + mPocketLock = new PocketLock(mContext); + mVrManagerInternal = LocalServices.getService(VrManagerInternal.class); if (mVrManagerInternal != null) { mVrManagerInternal.addPersistentVrModeStateListener(mPersistentVrModeListener); diff --git a/services/core/java/com/android/server/policy/pocket/PocketLock.java b/services/core/java/com/android/server/policy/pocket/PocketLock.java new file mode 100644 index 000000000000..c7757cac9786 --- /dev/null +++ b/services/core/java/com/android/server/policy/pocket/PocketLock.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 The ParanoidAndroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.policy.pocket; + +import android.animation.Animator; +import android.content.Context; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; + +/** + * This class provides a fullscreen overlays view, displaying itself + * even on top of lock screen. While this view is displaying touch + * inputs are not passed to the the views below. + * @see android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY + * @author Carlo Savignano + */ +public class PocketLock { + + private final Context mContext; + private WindowManager mWindowManager; + private WindowManager.LayoutParams mLayoutParams; + private Handler mHandler; + private View mView; + private View mHintContainer; + + private boolean mAttached; + private boolean mAnimating; + + /** + * Creates pocket lock objects, inflate view and set layout parameters. + * @param context + */ + public PocketLock(Context context) { + mContext = context; + mHandler = new Handler(); + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mLayoutParams = getLayoutParams(); + mView = LayoutInflater.from(mContext).inflate( + com.android.internal.R.layout.pocket_lock_view, null); + } + + public void show(final boolean animate) { + final Runnable r = new Runnable() { + @Override + public void run() { + if (mAttached) { + return; + } + + if (mAnimating) { + mView.animate().cancel(); + } + + if (animate) { + mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mView.animate().alpha(1.0f).setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + mAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + mView.setLayerType(View.LAYER_TYPE_NONE, null); + mAnimating = false; + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }).withStartAction(new Runnable() { + @Override + public void run() { + mView.setAlpha(0.0f); + mView.setVisibility(View.VISIBLE); + addView(); + } + }).start(); + } else { + mView.setVisibility(View.VISIBLE); + mView.setAlpha(1.0f); + addView(); + } + } + }; + + mHandler.post(r); + } + + public void hide(final boolean animate) { + final Runnable r = new Runnable() { + @Override + public void run() { + if (!mAttached) { + return; + } + + if (mAnimating) { + mView.animate().cancel(); + } + + if (animate) { + mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mView.animate().alpha(0.0f).setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + mAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + mView.setVisibility(View.GONE); + mView.setLayerType(View.LAYER_TYPE_NONE, null); + mAnimating = false; + removeView(); + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }).start(); + } else { + mView.setVisibility(View.GONE); + mView.setAlpha(0.0f); + removeView(); + } + } + }; + + mHandler.post(r); + } + + private void addView() { + if (mWindowManager != null && !mAttached) { + mWindowManager.addView(mView, mLayoutParams); + mView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + mAttached = true; + } + } + + private void removeView() { + if (mWindowManager != null && mAttached) { + mView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + mWindowManager.removeView(mView); + mAnimating = false; + mAttached = false; + } + } + + private WindowManager.LayoutParams getLayoutParams() { + mLayoutParams = new WindowManager.LayoutParams(); + mLayoutParams.format = PixelFormat.TRANSLUCENT; + mLayoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; + mLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; + mLayoutParams.gravity = Gravity.CENTER; + mLayoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; + mLayoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + mLayoutParams.flags = WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + return mLayoutParams; + } + +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c5a764a7a2ac..519720799db7 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -235,6 +235,8 @@ import com.android.server.pm.permission.PermissionMigrationHelper; import com.android.server.pm.permission.PermissionMigrationHelperImpl; import com.android.server.pm.verify.domain.DomainVerificationService; +import com.android.server.pocket.PocketBridgeService; +import com.android.server.pocket.PocketService; import com.android.server.policy.AppOpsPolicy; import com.android.server.policy.PermissionPolicyService; import com.android.server.policy.PhoneWindowManager; @@ -529,6 +531,8 @@ public final class SystemServer implements Dumpable { private final long mRuntimeStartElapsedTime; private final long mRuntimeStartUptime; + public boolean safeMode = false; + private static final String START_HIDL_SERVICES = "StartHidlServices"; private static final String START_SENSOR_MANAGER_SERVICE = "StartISensorManagerService"; private static final String START_BLOB_STORE_SERVICE = "startBlobStoreManagerService"; @@ -1852,7 +1856,10 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { // Before things start rolling, be sure we have decided whether // we are in safe mode. - final boolean safeMode = wm.detectSafeMode(); + + if(wm != null) { + safeMode = wm.detectSafeMode(); + } if (safeMode) { // If yes, immediately turn on the global setting for airplane mode. // Note that this does not send broadcasts at this stage because @@ -2851,6 +2858,17 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(CrossProfileAppsService.class); t.traceEnd(); + t.traceBegin("StartPocketService"); + mSystemServiceManager.startService(PocketService.class); + t.traceEnd(); + + if (!context.getResources().getString( + com.android.internal.R.string.config_pocketBridgeSysfsInpocket).isEmpty()) { + t.traceBegin("StartPocketBridgeService"); + mSystemServiceManager.startService(PocketBridgeService.class); + t.traceEnd(); + } + t.traceBegin("StartPeopleService"); mSystemServiceManager.startService(PeopleService.class); t.traceEnd(); From 97cf8b2f50a52685a268e152e1e5c36e6e4614dd Mon Sep 17 00:00:00 2001 From: Jyotiraditya Panda Date: Tue, 20 Aug 2024 15:14:12 +0530 Subject: [PATCH 0485/1315] core: Refactor pocket mode interface code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New illustration created by Radosław Błędowski for Paranoid Android Change-Id: Ic224319a7c282158fc1a1855293afb02e056b6ef Signed-off-by: Jyotiraditya Panda Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/drawable/ic_pocket_lock.xml | 10 -- .../res/drawable/pocket_mode_illustration.xml | 19 +++ core/res/res/drawable/pocket_mode_img.xml | 30 ----- core/res/res/layout/pocket_lock_view.xml | 115 +++++++----------- core/res/res/values/matrixx_arrays.xml | 21 ++-- core/res/res/values/matrixx_dimens.xml | 27 ++-- core/res/res/values/matrixx_strings.xml | 13 +- 7 files changed, 94 insertions(+), 141 deletions(-) delete mode 100644 core/res/res/drawable/ic_pocket_lock.xml create mode 100644 core/res/res/drawable/pocket_mode_illustration.xml delete mode 100644 core/res/res/drawable/pocket_mode_img.xml diff --git a/core/res/res/drawable/ic_pocket_lock.xml b/core/res/res/drawable/ic_pocket_lock.xml deleted file mode 100644 index 3494a8f38b84..000000000000 --- a/core/res/res/drawable/ic_pocket_lock.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/core/res/res/drawable/pocket_mode_illustration.xml b/core/res/res/drawable/pocket_mode_illustration.xml new file mode 100644 index 000000000000..b263d8a37f3b --- /dev/null +++ b/core/res/res/drawable/pocket_mode_illustration.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/core/res/res/drawable/pocket_mode_img.xml b/core/res/res/drawable/pocket_mode_img.xml deleted file mode 100644 index 81bb727f0bd3..000000000000 --- a/core/res/res/drawable/pocket_mode_img.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/core/res/res/layout/pocket_lock_view.xml b/core/res/res/layout/pocket_lock_view.xml index 53eb560d5a04..41a3b915954d 100644 --- a/core/res/res/layout/pocket_lock_view.xml +++ b/core/res/res/layout/pocket_lock_view.xml @@ -1,109 +1,82 @@ + android:background="@android:color/black" /> + android:text="@string/pocket_mode_title" /> - - + android:id="@+id/pocket_mode_illustration" + android:layout_width="@dimen/pocket_mode_illustration_width" + android:layout_height="@dimen/pocket_mode_illustration_height" + android:layout_marginTop="@dimen/pocket_mode_illustration_margin_top" + android:contentDescription="@string/pocket_mode_illustration_description" + android:src="@drawable/pocket_mode_illustration" /> + android:layout_marginHorizontal="@dimen/pocket_mode_instructions_container_margin_horizontal" + android:layout_marginTop="@dimen/pocket_mode_instructions_container_margin_top" + android:orientation="vertical"> + android:text="@string/pocket_mode_instructions_header" /> - - + android:layout_marginTop="@dimen/pocket_mode_instructions_steps_margin_top" + android:orientation="vertical"> - - - + - + + diff --git a/core/res/res/values/matrixx_arrays.xml b/core/res/res/values/matrixx_arrays.xml index de60d838e075..4feac267917b 100644 --- a/core/res/res/values/matrixx_arrays.xml +++ b/core/res/res/values/matrixx_arrays.xml @@ -9,14 +9,15 @@ .7 - 350dp - 175dp - 56dp - 24sp - 56dp - 16sp - 56dp - 24dp - 14sp - 8dp + 175dp + 350dp + 56dp + 24sp + 56dp + 24dp + 56dp + 16sp + 8dp + 14sp + 8dp diff --git a/core/res/res/values/matrixx_dimens.xml b/core/res/res/values/matrixx_dimens.xml index 22d5f842fbd9..a6bebcd84dc8 100644 --- a/core/res/res/values/matrixx_dimens.xml +++ b/core/res/res/values/matrixx_dimens.xml @@ -6,21 +6,22 @@ - - - diff --git a/core/res/res/values/matrixx_strings.xml b/core/res/res/values/matrixx_strings.xml index e9a9796d2f74..122df69c74f2 100644 --- a/core/res/res/values/matrixx_strings.xml +++ b/core/res/res/values/matrixx_strings.xml @@ -28,11 +28,10 @@ An error occured while uploading the log to Pasty - Press and hold power button to unlock - Pocket mode is on - To use your phone: - Check if the blue area of your phone is clear of any obstructions, and clean off any dirt or dust. - Long press the power button to force quit pocket mode. - 1.\u0020 - 2.\u0020 + Pocket mode is active + Illustration of phone in pocket + To use your phone: + 1. Ensure the green area (proximity sensor) shown above is unobstructed and free from dirt or dust. + 2. Press and hold the power button to exit pocket mode. + From 0d3ce2390b9ad6c2ca73e076ef237878f0d252f1 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 12 Oct 2024 18:36:01 +0530 Subject: [PATCH 0486/1315] core: Use blue area for pocket mode illustration Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/drawable/pocket_mode_illustration.xml | 2 +- core/res/res/values/matrixx_strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/res/res/drawable/pocket_mode_illustration.xml b/core/res/res/drawable/pocket_mode_illustration.xml index b263d8a37f3b..3b55a10cbaf6 100644 --- a/core/res/res/drawable/pocket_mode_illustration.xml +++ b/core/res/res/drawable/pocket_mode_illustration.xml @@ -14,6 +14,6 @@ android:pathData="M933 506.1V164c0-44.2-35.8-80-80-80H97c-44.2 0-80 35.8-80 80v1573c0 44.2 35.8 80 80 80h756c44.2 0 80-35.8 80-80V930.9c9.5-0.7 17-8.7 17-18.4v-160c0-9.7-7.5-17.7-17-18.4V607.9c9.5-0.7 17-8.7 17-18.4v-65c0-9.7-7.5-17.7-17-18.4zM853 104H97c-33.1 0-60 26.9-60 60v1573c0 33.1 26.9 60 60 60h756c33.1 0 60-26.9 60-60V164c0-33.1-26.9-60-60-60z" /> diff --git a/core/res/res/values/matrixx_strings.xml b/core/res/res/values/matrixx_strings.xml index 122df69c74f2..a9f3bc5bc4f6 100644 --- a/core/res/res/values/matrixx_strings.xml +++ b/core/res/res/values/matrixx_strings.xml @@ -31,7 +31,7 @@ Pocket mode is active Illustration of phone in pocket To use your phone: - 1. Ensure the green area (proximity sensor) shown above is unobstructed and free from dirt or dust. + 1. Ensure the blue area (proximity sensor) shown above is unobstructed and free from dirt or dust. 2. Press and hold the power button to exit pocket mode. From 5439b84b37fdc3b11d340707e0128b41581ba466 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 12 Oct 2024 14:43:50 +0530 Subject: [PATCH 0487/1315] services: Start pocket mode service only if supported Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/SystemServiceRegistry.java | 3 +++ .../com/android/server/policy/PhoneWindowManager.java | 8 +++++--- services/java/com/android/server/SystemServer.java | 9 ++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 4ad783e789cd..a6d36fc80d8e 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -1140,6 +1140,9 @@ public AuthenticationPolicyManager createService(ContextImpl ctx) new CachedServiceFetcher() { @Override public PocketManager createService(ContextImpl ctx) { + if (!ctx.getResources().getBoolean(R.bool.config_pocketModeSupported)) { + return null; + } IBinder binder = ServiceManager.getService(Context.POCKET_SERVICE); IPocketService service = IPocketService.Stub.asInterface(binder); return new PocketManager(ctx.getOuterContext(), service); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 51f830e8440a..f672511908b5 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -6968,9 +6968,11 @@ public void systemReady() { // So it is better not to bind keyguard here. mKeyguardDelegate.onSystemReady(); - mPocketManager = (PocketManager) mContext.getSystemService(Context.POCKET_SERVICE); - mPocketManager.addCallback(mPocketCallback); - mPocketLock = new PocketLock(mContext); + if (mContext.getResources().getBoolean(com.android.internal.R.bool.config_pocketModeSupported)) { + mPocketManager = (PocketManager) mContext.getSystemService(Context.POCKET_SERVICE); + mPocketManager.addCallback(mPocketCallback); + mPocketLock = new PocketLock(mContext); + } mVrManagerInternal = LocalServices.getService(VrManagerInternal.class); if (mVrManagerInternal != null) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 519720799db7..8132d7110edf 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2858,9 +2858,12 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(CrossProfileAppsService.class); t.traceEnd(); - t.traceBegin("StartPocketService"); - mSystemServiceManager.startService(PocketService.class); - t.traceEnd(); + if (context.getResources().getBoolean( + com.android.internal.R.bool.config_pocketModeSupported)) { + t.traceBegin("StartPocketService"); + mSystemServiceManager.startService(PocketService.class); + t.traceEnd(); + } if (!context.getResources().getString( com.android.internal.R.string.config_pocketBridgeSysfsInpocket).isEmpty()) { From afa829084f7e1261cafb310fe9e5f7ebac982cfe Mon Sep 17 00:00:00 2001 From: Fabian Leutenegger Date: Wed, 7 Aug 2024 13:25:09 +0200 Subject: [PATCH 0488/1315] base: Allow to define custom pocket sensor value Not all devices report 1.0 on their pocket mode sensor, Xiaomi for example report 1 if possibly in pocket and 2 if definitely in pocket by their NonUI sensor Change-Id: Ided2497200153a411ee80484573009a527d34e50 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/values/matrixx_config.xml | 1 + core/res/res/values/matrixx_symbols.xml | 1 + .../core/java/com/android/server/pocket/PocketService.java | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index b83e18964c42..e73f8f588e82 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -88,5 +88,6 @@ + 1.0 diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index dd9181d8aa2f..40814b4d80d3 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -74,5 +74,6 @@ + diff --git a/services/core/java/com/android/server/pocket/PocketService.java b/services/core/java/com/android/server/pocket/PocketService.java index 28a757e5a7b3..639a41f720cd 100644 --- a/services/core/java/com/android/server/pocket/PocketService.java +++ b/services/core/java/com/android/server/pocket/PocketService.java @@ -157,6 +157,7 @@ public class PocketService extends SystemService implements IBinder.DeathRecipie private int mVendorSensorState = VENDOR_SENSOR_UNKNOWN; private int mLastVendorSensorState = VENDOR_SENSOR_UNKNOWN; private String mVendorPocketSensor; + private float mVendorPocketSensorValue; private boolean mVendorSensorRegistered; private Sensor mVendorSensor; @@ -173,6 +174,8 @@ public PocketService(Context context) { mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); mVendorPocketSensor = mContext.getResources().getString( com.android.internal.R.string.config_pocketJudgeVendorSensorName); + mVendorPocketSensorValue = mContext.getResources().getFloat( + com.android.internal.R.dimen.config_pocketJudgeVendorSensorValue); String vendorProximitySensor = mContext.getResources().getString( com.android.internal.R.string.config_pocketJudgeVendorProximitySensorName); if (vendorProximitySensor != null && !vendorProximitySensor.isEmpty()) { @@ -767,7 +770,7 @@ private void handleVendorSensorEvent(SensorEvent sensorEvent) { if (DEBUG) Log.d(TAG, "Event has no values! event.values null ? " + (sensorEvent.values == null)); mVendorSensorState = VENDOR_SENSOR_UNKNOWN; } else { - final boolean isVendorPocket = sensorEvent.values[0] == 1.0; + final boolean isVendorPocket = sensorEvent.values[0] == mVendorPocketSensorValue; if (DEBUG) { final long time = SystemClock.uptimeMillis(); Log.d(TAG, "Event: time=" + time + ", value=" + sensorEvent.values[0] From c63fcefcc011590e096d5e4f81868a96acf64691 Mon Sep 17 00:00:00 2001 From: Fabian Leutenegger Date: Wed, 7 Aug 2024 15:10:15 +0200 Subject: [PATCH 0489/1315] base: Ensure pocket sensor is wakeup Change-Id: I2784c23ad456e1ce5b042e5cc31f919f6b5a5436 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/pocket/PocketService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/pocket/PocketService.java b/services/core/java/com/android/server/pocket/PocketService.java index 639a41f720cd..5f1fd0756bc6 100644 --- a/services/core/java/com/android/server/pocket/PocketService.java +++ b/services/core/java/com/android/server/pocket/PocketService.java @@ -870,7 +870,7 @@ private void handleUnregisterTimeout() { private static Sensor getSensor(SensorManager sm, String type) { for (Sensor sensor : sm.getSensorList(Sensor.TYPE_ALL)) { - if (type.equals(sensor.getStringType())) { + if (type.equals(sensor.getStringType()) && sensor.isWakeUpSensor()) { return sensor; } } From 94c80e7c67abee233c434369901d4739db8f5240 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 8 Feb 2025 18:55:52 +0530 Subject: [PATCH 0490/1315] SystemUI: Add less boring heads up option * This is complete rewrite for refactored code. Original Ref: https://github.com/crdroidandroid/android_frameworks_base/commit/f267ab0aaaea53b5940d9a0beafb0be62c82e3ff Co-authored-by: ezio84 Co-authored-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++ .../phone/StatusBarNotificationPresenter.java | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 17c17ed4dece..e96444342fe9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7358,6 +7358,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String POCKET_JUDGE = "pocket_judge"; + /** + * Whether to show heads up only for dialer and sms apps + * @hide + */ + public static final String LESS_BORING_HEADS_UP = "less_boring_heads_up"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 6ce3f435ca07..5b6fbee0a64d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -22,12 +22,17 @@ import android.app.KeyguardManager; import android.content.Context; +import android.database.ContentObserver; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Telephony.Sms; import android.service.notification.StatusBarNotification; import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; +import android.telecom.TelecomManager; import android.util.Log; import android.util.Slog; import android.view.View; @@ -76,6 +81,9 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import javax.inject.Inject; @@ -109,8 +117,11 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu private final DeviceUnlockedInteractor mDeviceUnlockedInteractor; private final QuickSettingsController mQsController; private final NTForbiddenSwipeDownQSController mNTForbiddenSwipeDownQSController; + private final TelecomManager mTm; + private final List mHeadsUpWhitelistPackages = new ArrayList<>(); protected boolean mVrMode; + private boolean mLessBoringHeadsUp; @Inject StatusBarNotificationPresenter( @@ -170,6 +181,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu mNotifListContainer = notificationListContainer; mDeviceUnlockedInteractor = deviceUnlockedInteractor; mNTForbiddenSwipeDownQSController = forbiddenSwipeDownQSController; + mTm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService( Context.VR_SERVICE)); @@ -192,8 +204,10 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu visualInterruptionDecisionProvider.addCondition(mVrModeCondition); visualInterruptionDecisionProvider.addFilter(mNeedsRedactionFilter); visualInterruptionDecisionProvider.addCondition(mPanelsDisabledCondition); + visualInterruptionDecisionProvider.addLegacySuppressor(mLessBoringSuppressor); } else { visualInterruptionDecisionProvider.addLegacySuppressor(mInterruptSuppressor); + visualInterruptionDecisionProvider.addLegacySuppressor(mLessBoringSuppressor); } mLockscreenUserManager.setUpWithPresenter(this); mGutsManager.setUpWithPresenter( @@ -201,6 +215,47 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu onUserSwitched(mLockscreenUserManager.getCurrentUserId()); }); + + ContentObserver headsUpObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + mLessBoringHeadsUp = Settings.System.getIntForUser( + context.getContentResolver(), + Settings.System.LESS_BORING_HEADS_UP, + 0, UserHandle.USER_CURRENT) == 1; + } + }; + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.LESS_BORING_HEADS_UP), + true, + headsUpObserver); + headsUpObserver.onChange(true); // set up + + String defaultDialerPackage = getDefaultDialerPackage(mTm); + if (defaultDialerPackage != null && !defaultDialerPackage.isEmpty()) { + mHeadsUpWhitelistPackages.add(defaultDialerPackage.toLowerCase()); + } + + String defaultSmsPackage = getDefaultSmsPackage(context); + if (defaultSmsPackage != null && !defaultSmsPackage.isEmpty()) { + mHeadsUpWhitelistPackages.add(defaultSmsPackage.toLowerCase()); + } + + mHeadsUpWhitelistPackages.addAll(Arrays.asList( + "dialer", + "messaging", + "messenger", + "clock" + )); + } + + private static String getDefaultSmsPackage(Context ctx) { + // for reference, there's also a new RoleManager api with getDefaultSmsPackage(context, userid) + return Sms.getDefaultSmsPackage(ctx); + } + + private static String getDefaultDialerPackage(TelecomManager tm) { + return tm != null ? tm.getDefaultDialerPackage() : ""; } /** Called when the shade has been emptied to attempt to close the shade */ @@ -429,4 +484,18 @@ public boolean shouldSuppress() { return !mCommandQueue.panelsEnabled(); } }; + + private final NotificationInterruptSuppressor mLessBoringSuppressor = + new NotificationInterruptSuppressor() { + @Override + public String getName() { + return TAG; + } + + @Override + public boolean suppressAwakeHeadsUp(NotificationEntry entry) { + if (!mLessBoringHeadsUp) return false; + return !mHeadsUpWhitelistPackages.contains(entry.getSbn().getPackageName().toLowerCase()); + } + }; } From 69162cf25278afbe5845483aa1665d86352d0853 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Sun, 5 May 2024 00:03:24 +0300 Subject: [PATCH 0491/1315] SystemUI: Default to true for HeadsUp notifications WTF google??? this was always true by default. you have a config set to 1 as well. Please stop smoking. Consistency was killed with this change introduced in QPR2: https://android.googlesource.com/platform/frameworks/base/+/14a03f2418f4c2c74a9346f435094358b41af308 I expect it to be fixed by google down the road. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../interruption/CommonVisualInterruptionSuppressors.kt | 3 ++- .../interruption/NotificationInterruptStateProviderImpl.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 5e087f16bae5..7ebdc16d1478 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -42,6 +42,7 @@ import android.os.SystemProperties import android.provider.Settings import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED import android.provider.Settings.Global.HEADS_UP_OFF +import android.provider.Settings.Global.HEADS_UP_ON import android.service.notification.Flags import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger @@ -87,7 +88,7 @@ class PeekDisabledSuppressor( val wasEnabled = isEnabled isEnabled = - globalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_OFF) != + globalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON) != HEADS_UP_OFF // QQQ: Do we want to log this even if it hasn't changed? diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 76dd6ebe0a0d..bd7681b2adf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -18,6 +18,7 @@ import static android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED; import static android.provider.Settings.Global.HEADS_UP_OFF; +import static android.provider.Settings.Global.HEADS_UP_ON; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; @@ -158,7 +159,7 @@ public NotificationInterruptStateProviderImpl( public void onChange(boolean selfChange) { final boolean wasUsing = mUseHeadsUp; final boolean settingEnabled = HEADS_UP_OFF - != mGlobalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_OFF); + != mGlobalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON); mUseHeadsUp = ENABLE_HEADS_UP && settingEnabled; mLogger.logHeadsUpFeatureChanged(mUseHeadsUp); if (wasUsing != mUseHeadsUp) { From 0e84e2225c381ef8b1ba50116a26d61fbe77d158 Mon Sep 17 00:00:00 2001 From: ezio84 Date: Sun, 16 Jan 2022 13:20:36 +0400 Subject: [PATCH 0492/1315] Allow to suppress notifications sound/vibration if screen is ON [1/2] Squashed: From: someone5678 Date: Sun, 30 Jun 2024 18:35:59 +0900 Subject: fixup! Allow to suppress notifications sound/vibration if screen is ON [1/2] * Also apply it to NotificationAttentionHelper Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Tue, 23 Jul 2024 01:12:58 +0530 Subject: fixup! Allow to suppress notifications sound/vibration if screen is ON [1/2] * We forgot to load settings on boot. Signed-off-by: Pranav Vashi Change-Id: I279b202682939d797d3116089f50d65e3dd3eb01 Signed-off-by: Jabiyeff Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 8 ++++++++ .../NotificationAttentionHelper.java | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e96444342fe9..5b5b46c61ce9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7364,6 +7364,14 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String LESS_BORING_HEADS_UP = "less_boring_heads_up"; + /** + * Whether to play notification sound and vibration if screen is ON + * 0 - never + * 1 - always + * @hide + */ + public static final String NOTIFICATION_SOUND_VIB_SCREEN_ON = "notification_sound_vib_screen_on"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 89abfcd6ffdc..d7ec270bf360 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -214,6 +214,8 @@ public final class NotificationAttentionHelper { private final PolitenessStrategy mStrategy; private int mCurrentWorkProfileId = UserHandle.USER_NULL; + private boolean mSoundVibScreenOn; + public NotificationAttentionHelper(Context context, Object lock, LightsManager lightsManager, AccessibilityManager accessibilityManager, PackageManager packageManager, UserManager userManager, NotificationUsageStats usageStats, @@ -385,6 +387,9 @@ public void onCallStateChanged(int state, String incomingNumber) { mContext.getContentResolver().registerContentObserver( SettingsObserver.NOTIFICATION_LIGHT_PULSE_URI, false, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + SettingsObserver.NOTIFICATION_SOUND_VIB_SCREEN_ON_URI, false, mSettingsObserver, + UserHandle.USER_ALL); if (Flags.politeNotifications()) { mContext.getContentResolver().registerContentObserver( SettingsObserver.NOTIFICATION_COOLDOWN_ENABLED_URI, false, mSettingsObserver, @@ -490,7 +495,8 @@ && isNotificationForCurrentUser(record, signals)) { } if (aboveThreshold && isNotificationForCurrentUser(record, signals)) { - if (mSystemReady && mAudioManager != null) { + boolean skipSound = mScreenOn && !mSoundVibScreenOn; + if (!skipSound && mSystemReady && mAudioManager != null) { Uri soundUri = record.getSound(); hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri); VibrationEffect vibration = record.getVibration(); @@ -1821,6 +1827,8 @@ private final class SettingsObserver extends ContentObserver { Settings.System.NOTIFICATION_COOLDOWN_ALL); private static final Uri NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED_URI = Settings.System.getUriFor(Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED); + private static final Uri NOTIFICATION_SOUND_VIB_SCREEN_ON_URI = + Settings.System.getUriFor(Settings.System.NOTIFICATION_SOUND_VIB_SCREEN_ON); public SettingsObserver() { super(null); } @@ -1840,6 +1848,12 @@ public void onChange(boolean selfChange, Uri uri) { } } } + if (uri == null || NOTIFICATION_SOUND_VIB_SCREEN_ON_URI.equals(uri)) { + mSoundVibScreenOn = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.NOTIFICATION_SOUND_VIB_SCREEN_ON, 1, + UserHandle.USER_CURRENT) == 1; + } if (Flags.politeNotifications()) { if (uri == null || NOTIFICATION_COOLDOWN_ENABLED_URI.equals(uri)) { mNotificationCooldownEnabled = Settings.System.getIntForUser( From dd95d6aa088b6c3e11bf5d936ee495f38694003c Mon Sep 17 00:00:00 2001 From: Lars Greiss Date: Sat, 26 Mar 2016 21:18:14 -0400 Subject: [PATCH 0493/1315] HeadsUp: add timeout option (1/2) Forward ported to marshmallow/nougat By: BeansTown106 this was basically a rewrite as everything about it changed Squashed: From: Pranav Vashi Date: Fri, 15 Apr 2022 21:20:04 +0530 Subject: HeadsUp: Change heads up timeout to seconds Change-Id: I9c88d67d25440dff8f545e1330da672be5d36913 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++++++ .../notification/headsup/HeadsUpManagerImpl.java | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5b5b46c61ce9..1b36cf606636 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7372,6 +7372,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String NOTIFICATION_SOUND_VIB_SCREEN_ON = "notification_sound_vib_screen_on"; + /** + * Heads up timeout configuration + * @hide + */ + public static final String HEADS_UP_TIMEOUT = "heads_up_timeout"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 28b5874acf5a..3dcad66bc258 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -24,6 +24,8 @@ import android.database.ContentObserver; import android.graphics.Region; import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -122,8 +124,9 @@ public class HeadsUpManagerImpl private final int mMinimumDisplayTimeDefault; private final int mMinimumDisplayTimeForUserInitiated; private final int mStickyForSomeTimeAutoDismissTime; - private final int mAutoDismissTime; + private int mAutoDismissTime; private final DelayableExecutor mExecutor; + private final int mDecayDefault; private final int mExtensionTime; @@ -225,7 +228,10 @@ public HeadsUpManagerImpl( R.integer.heads_up_notification_minimum_time_for_user_initiated); mStickyForSomeTimeAutoDismissTime = resources.getInteger( R.integer.sticky_heads_up_notification_time); - mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); + mDecayDefault = resources.getInteger(R.integer.heads_up_notification_decay) / 1000; + mAutoDismissTime = Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.HEADS_UP_TIMEOUT, + mDecayDefault, UserHandle.USER_CURRENT) * 1000; mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time); mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); mSnoozedPackages = new ArrayMap<>(); @@ -243,12 +249,18 @@ public void onChange(boolean selfChange) { mSnoozeLengthMs = packageSnoozeLengthMs; mLogger.logSnoozeLengthChange(packageSnoozeLengthMs); } + mAutoDismissTime = Settings.System.getIntForUser( + context.getContentResolver(), Settings.System.HEADS_UP_TIMEOUT, + mDecayDefault, UserHandle.USER_CURRENT) * 1000; } }; globalSettings.registerContentObserverAsync( globalSettings.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), /* notifyForDescendants= */ false, settingsObserver); + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.HEADS_UP_TIMEOUT), false, + settingsObserver); statusBarStateController.addCallback(mStatusBarStateListener); updateResources(); From 2b8d3d3d8c221ea65056e0cf9eed0aff1112c57d Mon Sep 17 00:00:00 2001 From: Daniel Koman Date: Tue, 27 Nov 2018 15:31:39 -0700 Subject: [PATCH 0494/1315] Add kill button to notification guts [1/2] Change-Id: Id2c2f3259ae2ff45ab28288375bc14148a0e9f1e Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 7 ++ data/etc/com.android.systemui.xml | 1 + packages/SystemUI/AndroidManifest.xml | 3 + .../SystemUI/res/drawable/ic_force_stop.xml | 25 +++++++ .../notification_2025_conversation_info.xml | 11 +++ .../res/layout/notification_2025_info.xml | 11 +++ .../layout/notification_conversation_info.xml | 11 +++ .../SystemUI/res/layout/notification_info.xml | 12 ++++ .../SystemUI/res/values/matrixx_strings.xml | 4 ++ .../row/NotificationConversationInfo.java | 72 +++++++++++++++++++ .../notification/row/NotificationInfo.java | 72 +++++++++++++++++++ 11 files changed, 229 insertions(+) create mode 100644 packages/SystemUI/res/drawable/ic_force_stop.xml diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1b36cf606636..df45324bd6cb 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7378,6 +7378,13 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String HEADS_UP_TIMEOUT = "heads_up_timeout"; + /** + * Whether to show the kill app button in notification guts + * @hide + */ + public static final String NOTIFICATION_GUTS_KILL_APP_BUTTON = + "notification_guts_kill_app_button"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index b632df559ff8..8b0b9e639160 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -29,6 +29,7 @@ + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 16669c8227e4..d35d5a46aaf3 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -409,6 +409,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_force_stop.xml b/packages/SystemUI/res/drawable/ic_force_stop.xml new file mode 100644 index 000000000000..4385b3e4c52e --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_force_stop.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/SystemUI/res/layout/notification_2025_conversation_info.xml b/packages/SystemUI/res/layout/notification_2025_conversation_info.xml index d9ec42c68f81..2ce48c85bc05 100644 --- a/packages/SystemUI/res/layout/notification_2025_conversation_info.xml +++ b/packages/SystemUI/res/layout/notification_2025_conversation_info.xml @@ -157,6 +157,17 @@ + + + + + + + used this week Data Wi-Fi + + + Force stop? + If you force stop an app, it may misbehave. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 057d75d03c3b..837d2adbc17c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -35,14 +35,18 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.Flags; import android.app.INotificationManager; +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; @@ -52,6 +56,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.text.Annotation; @@ -82,6 +87,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.wmshell.BubblesManager; import java.lang.annotation.Retention; @@ -279,6 +286,28 @@ public void bindNotification( done.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate()); } + private boolean isForceStopAllowed() { + if (TextUtils.isEmpty(mPackageName)) return false; + + if ("android".equals(mPackageName)) return false; + if ("com.android.systemui".equals(mPackageName)) return false; + + try { + ApplicationInfo ai = mPm.getApplicationInfo(mPackageName, 0); + final boolean isSystem = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + if (isSystem) return false; + } catch (PackageManager.NameNotFoundException ignored) { + return false; + } + + return true; + } + + private boolean isKeyguardLocked() { + KeyguardManager km = mContext.getSystemService(KeyguardManager.class); + return km != null && km.isKeyguardLocked(); + } + private void bindActions() { TextView defaultSummaryTextView = findViewById(R.id.default_summary); if (mAppBubble == BUBBLE_PREFERENCE_ALL @@ -298,12 +327,55 @@ private void bindActions() { settingsButton.setOnClickListener(getSettingsOnClickListener()); settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE); + // Force stop button + final View killButton = findViewById(R.id.force_stop); + if (killButton != null) { + boolean killButtonEnabled = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.NOTIFICATION_GUTS_KILL_APP_BUTTON, 0, + UserHandle.USER_CURRENT) != 0; + killButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (isKeyguardLocked()) { + return; + } + final SystemUIDialog killDialog = new SystemUIDialog(mContext); + killDialog.setTitle(mContext.getText(R.string.force_stop_dlg_title)); + killDialog.setMessage(mContext.getText(R.string.force_stop_dlg_text)); + killDialog.setPositiveButton( + android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final int userId = mSbn != null ? + mSbn.getUser().getIdentifier() : UserHandle.myUserId(); + try { + ActivityManager.getService().forceStopPackage(mPackageName, userId); + } catch (Exception re) { + Log.w(TAG, "Force stop failed for " + mPackageName, re); + } finally { + closeGutsIfOpen(); + } + } + }); + killDialog.setNegativeButton(android.R.string.cancel, null); + killDialog.show(); + } + }); + killButton.setVisibility(killButtonEnabled + && isForceStopAllowed() ? View.VISIBLE : View.GONE); + } + bindFeedback(); updateToggleActions(mSelectedAction == -1 ? getPriority() : mSelectedAction, false); } + private void closeGutsIfOpen() { + if (mGutsContainer != null) { + mGutsContainer.closeControls(this, true); + } + } + private void bindHeader() { bindConversationDetails(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index b2ff01279268..a34ee1b89543 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -34,16 +34,20 @@ import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Flags; import android.app.INotificationManager; +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; @@ -51,6 +55,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.service.notification.NotificationAssistantService; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; @@ -84,6 +89,8 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; +import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.phone.SystemUIDialog; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -389,6 +396,49 @@ private void bindHeader() { final View settingsButton = findViewById(R.id.info); settingsButton.setOnClickListener(getSettingsOnClickListener()); settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE); + + // Force stop button + final View killButton = findViewById(R.id.force_stop); + if (killButton != null) { + boolean killButtonEnabled = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.NOTIFICATION_GUTS_KILL_APP_BUTTON, 0, + UserHandle.USER_CURRENT) != 0; + killButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (isKeyguardLocked()) { + return; + } + final SystemUIDialog killDialog = new SystemUIDialog(mContext); + killDialog.setTitle(mContext.getText(R.string.force_stop_dlg_title)); + killDialog.setMessage(mContext.getText(R.string.force_stop_dlg_text)); + killDialog.setPositiveButton( + android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final int userId = mSbn != null ? + mSbn.getUser().getIdentifier() : UserHandle.myUserId(); + try { + ActivityManager.getService().forceStopPackage(mPackageName, userId); + } catch (Exception re) { + Log.w(TAG, "Force stop failed for " + mPackageName, re); + } finally { + closeGutsIfOpen(); + } + } + }); + killDialog.setNegativeButton(android.R.string.cancel, null); + killDialog.show(); + } + }); + killButton.setVisibility(killButtonEnabled + && isForceStopAllowed() ? View.VISIBLE : View.GONE); + } + } + + private void closeGutsIfOpen() { + if (mGutsContainer != null) { + mGutsContainer.closeControls(this, true); + } } private void bindFeedback() { @@ -575,6 +625,28 @@ private void bindGroup() throws RemoteException { } } + private boolean isForceStopAllowed() { + if (TextUtils.isEmpty(mPackageName)) return false; + + if ("android".equals(mPackageName)) return false; + if ("com.android.systemui".equals(mPackageName)) return false; + + try { + ApplicationInfo ai = mPm.getApplicationInfo(mPackageName, 0); + final boolean isSystem = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + if (isSystem) return false; + } catch (PackageManager.NameNotFoundException ignored) { + return false; + } + + return true; + } + + private boolean isKeyguardLocked() { + KeyguardManager km = mContext.getSystemService(KeyguardManager.class); + return km != null && km.isKeyguardLocked(); + } + private void saveImportance() { if (!mIsNonblockable) { if (mChosenImportance == null) { From aae9b93f869edbb42e7c13169eb254ba78717044 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Mon, 24 Oct 2022 00:24:27 +0530 Subject: [PATCH 0495/1315] SystemUI: Allow disabling clipboard overlay [1/2] Change-Id: Ia755ca9021468a53c8f17413ca3faa787b00ceea Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 7 +++++++ .../systemui/clipboardoverlay/ClipboardListener.java | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index df45324bd6cb..1c25349334ee 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14072,6 +14072,13 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NAVBAR_IME_SPACE = "navbar_ime_space"; + /** + * Whether to show an overlay in the bottom corner of the screen on copying stuff + * into the clipboard. + * @hide + */ + public static final String SHOW_CLIPBOARD_OVERLAY = "show_clipboard_overlay"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index e05bfd96107f..fdc612a9530c 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -32,6 +32,7 @@ import android.content.Context; import android.os.Build; import android.os.UserHandle; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -129,6 +130,10 @@ public void onPrimaryClipChanged() { if (!mClipboardManagerForUser.hasPrimaryClip()) { return; } + if (Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.SHOW_CLIPBOARD_OVERLAY, 1, UserHandle.USER_CURRENT) == 0) { + return; + } String clipSource = mClipboardManagerForUser.getPrimaryClipSource(); ClipData clipData = mClipboardManagerForUser.getPrimaryClip(); From 28ed04d61e62ad9e111a9eae66cd3c4b9c3c1b23 Mon Sep 17 00:00:00 2001 From: lishengxu Date: Mon, 10 Mar 2025 17:17:27 +0800 Subject: [PATCH 0496/1315] Use IntArray instead of ArraySet to store int lists Analysis: 1.The mapping structure of mUsesPermissionToUids and mPermissionToUids in AppsFilterImpl that stores permission to uid is Map>, ArraySet stores int data in boxing, which takes up slightly more memory than IntArray. Due to the large number of permissions, which is about 2000, Map> occupies a large amount of memory 2.IntArray stores int data without boxing, which saves more memory 3.The efficiency of adding, deleting and checking 10,000 data is not lower than that of ArraySet 4.Use IntArray instead of ArraySet to store uid int data Change-Id: I69a5cbc6eaecb4a0de8d2143291aefc115ea9afe Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/pm/AppsFilterImpl.java | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 135fb7b8b22e..f27e94daa80b 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -55,6 +55,7 @@ import android.provider.DeviceConfig; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; import android.util.Slog; import android.util.SparseBooleanArray; import android.util.SparseSetArray; @@ -115,7 +116,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ @GuardedBy("mQueryableViaUsesPermissionLock") @NonNull - private final ArrayMap> mPermissionToUids; + private final ArrayMap mPermissionToUids; /** * A cache that maps parsed {@link android.R.styleable#AndroidManifestUsesPermission @@ -125,7 +126,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ @GuardedBy("mQueryableViaUsesPermissionLock") @NonNull - private final ArrayMap> mUsesPermissionToUids; + private final ArrayMap mUsesPermissionToUids; /** * Ensures an observer is in the list, exactly once. The observer cannot be null. The @@ -630,10 +631,10 @@ && isSystemSigned(mSystemSigningDetails, newPkgSetting))) { // Lookup in the mPermissionToUids cache if installed packages have // defined this permission. if (mPermissionToUids.containsKey(usesPermissionName)) { - final ArraySet permissionDefiners = + final IntArray permissionDefiners = mPermissionToUids.get(usesPermissionName); for (int j = 0; j < permissionDefiners.size(); j++) { - final int targetAppId = permissionDefiners.valueAt(j); + final int targetAppId = permissionDefiners.get(j); if (targetAppId != newPkgSetting.getAppId()) { mQueryableViaUsesPermission.add(newPkgSetting.getAppId(), targetAppId); @@ -643,9 +644,12 @@ && isSystemSigned(mSystemSigningDetails, newPkgSetting))) { // Record in mUsesPermissionToUids that a permission was requested // by a new package if (!mUsesPermissionToUids.containsKey(usesPermissionName)) { - mUsesPermissionToUids.put(usesPermissionName, new ArraySet<>()); + mUsesPermissionToUids.put(usesPermissionName, new IntArray()); + } + IntArray intArray = mUsesPermissionToUids.get(usesPermissionName); + if (!intArray.contains(newPkgSetting.getAppId())) { + intArray.add(newPkgSetting.getAppId()); } - mUsesPermissionToUids.get(usesPermissionName).add(newPkgSetting.getAppId()); } } } @@ -657,10 +661,10 @@ && isSystemSigned(mSystemSigningDetails, newPkgSetting))) { // Lookup in the mUsesPermissionToUids cache if installed packages have // requested this permission. if (mUsesPermissionToUids.containsKey(permissionName)) { - final ArraySet permissionUsers = mUsesPermissionToUids.get( + final IntArray permissionUsers = mUsesPermissionToUids.get( permissionName); for (int j = 0; j < permissionUsers.size(); j++) { - final int queryingAppId = permissionUsers.valueAt(j); + final int queryingAppId = permissionUsers.get(j); if (queryingAppId != newPkgSetting.getAppId()) { mQueryableViaUsesPermission.add(queryingAppId, newPkgSetting.getAppId()); @@ -669,9 +673,12 @@ && isSystemSigned(mSystemSigningDetails, newPkgSetting))) { } // Record in mPermissionToUids that a permission was defined by a new package if (!mPermissionToUids.containsKey(permissionName)) { - mPermissionToUids.put(permissionName, new ArraySet<>()); + mPermissionToUids.put(permissionName, new IntArray()); + } + IntArray intArray = mPermissionToUids.get(permissionName); + if (!intArray.contains(newPkgSetting.getAppId())) { + intArray.add(newPkgSetting.getAppId()); } - mPermissionToUids.get(permissionName).add(newPkgSetting.getAppId()); } } } @@ -1160,8 +1167,12 @@ private void removePackageInternal(Computer snapshot, PackageStateInternal setti for (ParsedPermission permission : setting.getPkg().getPermissions()) { String permissionName = permission.getName(); if (mPermissionToUids.containsKey(permissionName)) { - mPermissionToUids.get(permissionName).remove(setting.getAppId()); - if (mPermissionToUids.get(permissionName).isEmpty()) { + IntArray intArray = mPermissionToUids.get(permissionName); + int index = intArray.indexOf(setting.getAppId()); + if (index >= 0) { + intArray.remove(index); + } + if (mPermissionToUids.get(permissionName).size() <= 0) { mPermissionToUids.remove(permissionName); } } @@ -1172,8 +1183,12 @@ private void removePackageInternal(Computer snapshot, PackageStateInternal setti setting.getPkg().getUsesPermissionMapping().values()) { String usesPermissionName = usesPermission.getName(); if (mUsesPermissionToUids.containsKey(usesPermissionName)) { - mUsesPermissionToUids.get(usesPermissionName).remove(setting.getAppId()); - if (mUsesPermissionToUids.get(usesPermissionName).isEmpty()) { + IntArray intArray = mUsesPermissionToUids.get(usesPermissionName); + int index = intArray.indexOf(setting.getAppId()); + if (index >= 0) { + intArray.remove(index); + } + if (mUsesPermissionToUids.get(usesPermissionName).size() <= 0) { mUsesPermissionToUids.remove(usesPermissionName); } } From a4dc42188bbc52508100091f290745111537dd09 Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:18:25 +0900 Subject: [PATCH 0497/1315] SystemUI: Add switch for compact HUN [1/2] * Now we can always enable it Change-Id: Ia27310b05df6b7ec222d91c0e46a068e4e8004e7 Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/notification/row/HeadsUpStyleProvider.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt index d567a94a1641..efeccad65e52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row +import android.os.SystemProperties import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import javax.inject.Inject @@ -33,7 +34,8 @@ constructor(private val statusBarModeRepositoryStore: StatusBarModeRepositorySto HeadsUpStyleProvider { override fun shouldApplyCompactStyle(): Boolean { - return isInImmersiveMode() + return isInImmersiveMode() || + SystemProperties.getBoolean("persist.sys.compact_heads_up_notification.always_show", false) } private fun isInImmersiveMode() = From d65a58e46e02467e1ad5da5b13d097f48ea1c4bd Mon Sep 17 00:00:00 2001 From: AshutoshSundresh Date: Sun, 6 Feb 2022 19:05:02 +0530 Subject: [PATCH 0498/1315] SystemUI: Integrate Google Lens into Screenshot UI - shares the screenshot taken to Google Lens - informs the user to install Google Lens if it is not installed Squashed: From: cjh1249131356 Date: Wed, 6 Jul 2022 15:35:31 +0800 Subject: SystemUI: Make Lens work without independent package installed Reference: https://github.com/crdroidandroid/android_packages_apps_Launcher3/commit/35169a68be308b16aa833dfc68ec3f25b6fbf191 Signed-off-by: cjh1249131356 Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Sun, 26 Feb 2023 00:16:39 +0530 Subject: SystemUI: Do not add lens screenshot without google package enabled Signed-off-by: Pranav Vashi From: someone5678 Date: Thu, 27 Jun 2024 09:33:41 +0900 Subject: LensScreenshotReceiver: Return when failed to start activity Change-Id: Iab29516dae6414a5c36168af3643e23006b99ac9 Signed-off-by: Pranav Vashi From: someone5678 Date: Fri, 12 Jul 2024 13:39:38 +0900 Subject: fixup! SystemUI: Integrate Google Lens into Screenshot UI * Make Lens activity works on Work Profile Signed-off-by: Pranav Vashi Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/AndroidManifest.xml | 4 + .../res/drawable/ic_screenshot_lens.xml | 7 ++ .../SystemUI/res/values/matrixx_strings.xml | 3 + .../DefaultBroadcastReceiverBinder.java | 10 ++ .../screenshot/ActionIntentCreator.kt | 15 +++ .../screenshot/LensScreenshotReceiver.java | 114 ++++++++++++++++++ .../screenshot/ScreenshotActionsProvider.kt | 18 +++ .../systemui/screenshot/ScreenshotEvent.java | 2 + 8 files changed, 173 insertions(+) create mode 100644 packages/SystemUI/res/drawable/ic_screenshot_lens.xml create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/LensScreenshotReceiver.java diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index d35d5a46aaf3..20e3cf95f198 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -639,6 +639,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_screenshot_lens.xml b/packages/SystemUI/res/drawable/ic_screenshot_lens.xml new file mode 100644 index 000000000000..58e3da5840a4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_screenshot_lens.xml @@ -0,0 +1,7 @@ + + + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index c01bafc1c6a9..1df4664620ad 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -106,4 +106,7 @@ Force stop? If you force stop an app, it may misbehave. + + + Lens diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 2459541b91b3..57ac23dceab1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -25,6 +25,7 @@ import com.android.systemui.people.widget.PeopleSpaceWidgetPinnedReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; import com.android.systemui.screenshot.DeleteScreenshotReceiver; +import com.android.systemui.screenshot.LensScreenshotReceiver; import com.android.systemui.screenshot.SmartActionsReceiver; import com.android.systemui.user.UserDialogReceiver; @@ -47,6 +48,15 @@ public abstract class DefaultBroadcastReceiverBinder { public abstract BroadcastReceiver bindDeleteScreenshotReceiver( DeleteScreenshotReceiver broadcastReceiver); + /** + * + */ + @Binds + @IntoMap + @ClassKey(LensScreenshotReceiver.class) + public abstract BroadcastReceiver bindLensScreenshotReceiver( + LensScreenshotReceiver broadcastReceiver); + /** * */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 8af4917a7f97..8f864344a2ee 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -145,6 +145,21 @@ constructor( ) } + fun createLens(rawUri: Uri, context: Context, owner: UserHandle): PendingIntent { + return PendingIntent.getBroadcast(context, rawUri.toString().hashCode(), + Intent(context, LensScreenshotReceiver::class.java) + .putExtra( + LensScreenshotReceiver.EXTRA_LENS_SCREENSHOT_URI_ID, + rawUri.toString()) + .putExtra( + LensScreenshotReceiver.EXTRA_SCREENSHOT_USER_HANDLE, + owner) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), + (PendingIntent.FLAG_CANCEL_CURRENT + or PendingIntent.FLAG_ONE_SHOT + or PendingIntent.FLAG_IMMUTABLE)) + } + /** @return an Intent to start the LongScreenshotActivity */ fun createLongScreenshotIntent(owner: UserHandle): Intent { return Intent(context, LongScreenshotActivity::class.java) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LensScreenshotReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/LensScreenshotReceiver.java new file mode 100644 index 000000000000..39006201786f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LensScreenshotReceiver.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 ShapeShiftOS + * (C) 2024-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static com.android.systemui.screenshot.SmartActionsReceiver.EXTRA_ACTION_TYPE; +import static com.android.systemui.screenshot.SmartActionsReceiver.EXTRA_ID; +import static com.android.systemui.screenshot.SmartActionsReceiver.EXTRA_SMART_ACTIONS_ENABLED; + +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Process; +import android.os.UserHandle; + +import com.android.internal.util.matrixx.Utils; + +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.res.R; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +public class LensScreenshotReceiver extends BroadcastReceiver { + + protected static final String EXTRA_LENS_SCREENSHOT_URI_ID = "lens_screenshot_uri_id"; + private static final String GSA_PACKAGE = "com.google.android.googlequicksearchbox"; + private static final String LENS_ACTIVITY = "com.google.android.apps.lens.MainActivity"; + private static final String LENS_SHARE_ACTIVITY = "com.google.android.apps.search.lens.LensShareEntryPointActivity"; + private static final String LENS_URI = "google://lens"; + protected static final String EXTRA_SCREENSHOT_USER_HANDLE = "lens_screenshot_userhandle"; + + private final ActionIntentExecutor mActionExecutor; + private final Executor mBackgroundExecutor; + private final ScreenshotSmartActions mScreenshotSmartActions; + private UserHandle mScreenshotUserHandle; + + @Inject + public LensScreenshotReceiver( + ActionIntentExecutor actionExecutor, + @Background Executor bgExecutor, + ScreenshotSmartActions screenshotSmartActions) { + mActionExecutor = actionExecutor; + mBackgroundExecutor = bgExecutor; + mScreenshotSmartActions = screenshotSmartActions; + } + + public static boolean isGSAEnabled(Context context) { + return Utils.isPackageInstalled(context, GSA_PACKAGE, false /* ignoreState */); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.hasExtra(EXTRA_LENS_SCREENSHOT_URI_ID)) { + return; + } + + mScreenshotUserHandle = intent.getParcelableExtra(EXTRA_SCREENSHOT_USER_HANDLE, + UserHandle.class); + if (mScreenshotUserHandle == null) { + mScreenshotUserHandle = Process.myUserHandle(); + } + + final Uri uri = Uri.parse(intent.getStringExtra(EXTRA_LENS_SCREENSHOT_URI_ID)); + mBackgroundExecutor.execute(() -> { + // action to execute goes here + ClipData clipdata = new ClipData(new ClipDescription("content", + new String[]{"image/png"}), + new ClipData.Item(uri)); + Intent lensIntent = new Intent(); + lensIntent.setAction(Intent.ACTION_SEND) + .setComponent(new ComponentName(GSA_PACKAGE, LENS_SHARE_ACTIVITY)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setType("image/png") + .putExtra(Intent.EXTRA_STREAM, uri) + .setClipData(clipdata); + try { + mActionExecutor.launchIntentAsync( + lensIntent, mScreenshotUserHandle, false, + /* activityOptions */ null, /* transitionCoordinator */ null); + + } catch (Exception e) { + return; + } + }); + + if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { + mScreenshotSmartActions.notifyScreenshotAction( + intent.getStringExtra(EXTRA_ID), intent.getStringExtra(EXTRA_ACTION_TYPE), + false, null); + + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index a13f57976982..3a21a6d5abbe 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -27,6 +27,7 @@ import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DELETE_TAPPED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_LENS_TAPPED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance @@ -159,6 +160,23 @@ constructor( ) } } + + actionsCallback.provideActionButton( + ActionButtonAppearance( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_lens), + context.resources.getString(R.string.screenshot_lens_label), + context.resources.getString(R.string.screenshot_lens_label), + ), + showDuringEntrance = true, + ) { + debugLog(LogConfig.DEBUG_ACTIONS) { "Lens tapped" } + uiEventLogger.log(SCREENSHOT_LENS_TAPPED, 0, request.packageNameString) + onDeferrableActionTapped { result -> + actionExecutor.sendPendingIntent( + actionIntentCreator.createLens(result.uri, context, result.user) + ) + } + } } override fun onScrollChipReady(onClick: Runnable) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java index 51ec515cd165..cf39f5a90803 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -59,6 +59,8 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { SCREENSHOT_SHARE_TAPPED(309), @UiEvent(doc = "screenshot delete button tapped") SCREENSHOT_DELETE_TAPPED(369), + @UiEvent(doc = "screenshot lens button tapped") + SCREENSHOT_LENS_TAPPED(370), @UiEvent(doc = "screenshot smart action chip tapped") SCREENSHOT_SMART_ACTION_TAPPED(374), @UiEvent(doc = "screenshot scroll tapped") From 3e082fefe25519dde006130f84a40bb088ccf9f1 Mon Sep 17 00:00:00 2001 From: Park Ju Hyung Date: Sat, 9 Oct 2021 03:22:39 -0400 Subject: [PATCH 0499/1315] SystemUI: Implement burn-in protection for statusbar Devices with OLED display suffer from status-bar's notification items and nagivation bar's software keys causing permanent burn-ins when used long-term. Moving all items in the area both horizontally and vertically workarounds this problem. SystemUI: rework statusbar burn-in protection controller * Turns out that this controller was instantiated twice resulting in two timers running simultaneously which resulted in views to shift abruptly. Since the shift amount was too low it was not noticeable at all. So now we instantiate it once with all final dependencies and inject PhoneStatusBarView in fragment transaction. * Finalized many instance variables and a reference to the main handler is kept instead of creating new ones in each cycle * simplified / generalised the shift algorithm a bit so that it's easily configurable * added a callback to reload shift vars on screen density changes * additional changes: * use the same controller for navigation handle, saves some cpu time Signed-off-by: jhonboy121 SystemUI: inject BurnInProtectionController Signed-off-by: jhonboy121 [jhonboy121]: use the scoped SysUISingleton annotation SystemUI: BurnInProtectionController: rewrite in kotlin and improvements * ditched TimerTask in favor of coroutines Signed-off-by: jhonboy121 [jhonboy121]: * adapted to A13 * use BurnInHelper util functions for calculating offset Squashed: From: jhonboy121 Date: Sun, 11 Sep 2022 20:36:57 +0530 Subject: SystemUI: BurnInProtectionController: offset less aggressively Change-Id: Ib37b0fde6edfc34cad8876d2e01ba4f37f323036 Signed-off-by: jhonboy121 Signed-off-by: Pranav Vashi From: Fabian Leutenegger Date: Wed, 2 Aug 2023 20:06:20 +0200 Subject: SystemUI: Make setNavigationBarView and setPhoneStatusBarView nullable * this fixes a potential npe on devices without navbar or statusbar Change-Id: Ia8e0ff844e24f67685ba20ac61a88d3256c9c648 Signed-off-by: Pranav Vashi Co-authored-by: jhonboy121 Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/values/matrixx_dimens.xml | 3 + .../systemui/doze/util/BurnInHelper.kt | 17 +- .../statusbar/phone/CentralSurfacesImpl.java | 11 +- .../statusbar/phone/PhoneStatusBarView.java | 17 +- .../phone/PhoneStatusBarViewController.kt | 4 + .../policy/BurnInProtectionController.kt | 172 ++++++++++++++++++ 6 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index 3fc44d0f1ba0..021751b2a7de 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -94,4 +94,7 @@ 0.0dip 24.0dip 24.0dip + + + 4dp diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt index 888785510c1e..3796af8a8b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt @@ -32,13 +32,18 @@ private const val BURN_IN_PREVENTION_PERIOD_PROGRESS = 89f * @param amplitude Maximum translation that will be interpolated. * @param xAxis If we're moving on X or Y. */ -fun getBurnInOffset(amplitude: Int, xAxis: Boolean): Int { +@JvmOverloads +fun getBurnInOffset( + amplitude: Int, + xAxis: Boolean, + periodX: Float = BURN_IN_PREVENTION_PERIOD_X, + periodY: Float = BURN_IN_PREVENTION_PERIOD_Y +): Int { return zigzag( - System.currentTimeMillis() / MILLIS_PER_MINUTES, - amplitude.toFloat(), - if (xAxis) BURN_IN_PREVENTION_PERIOD_X else BURN_IN_PREVENTION_PERIOD_Y, - ) - .toInt() + System.currentTimeMillis() / MILLIS_PER_MINUTES, + amplitude.toFloat(), + if (xAxis) periodX else periodY + ).toInt() } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index a6bb23b081cf..6e5e5527dd77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -224,6 +224,7 @@ import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.statusbar.policy.BurnInProtectionController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -613,6 +614,8 @@ public int getId() { private final QuickAccessWalletController mWalletController; + private final BurnInProtectionController mBurnInProtectionController; + /** * Public constructor for CentralSurfaces. * @@ -732,7 +735,8 @@ public CentralSurfacesImpl( WindowManagerProvider windowManagerProvider, MediaViewController mediaViewController, PulseViewController pulseViewController, - EdgeLightViewController edgeLightViewController + EdgeLightViewController edgeLightViewController, + BurnInProtectionController burnInProtectionController ) { mContext = context; mNotificationsController = notificationsController; @@ -846,6 +850,7 @@ public CentralSurfacesImpl( statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged); } mScreenOffAnimationController = screenOffAnimationController; + mBurnInProtectionController = burnInProtectionController; ShadeExpansionListener shadeExpansionListener = this::onPanelExpansionChanged; ShadeExpansionChangeEvent currentState = @@ -1258,6 +1263,7 @@ protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) { mShadeSurface.updateExpansionAndVisibility(); setBouncerShowingForStatusBarComponents(mBouncerShowing); checkBarModes(); + mBurnInProtectionController.setPhoneStatusBarView(mPhoneStatusBarViewController.getPhoneStatusBarView()); }); } if (!StatusBarRootModernization.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) { @@ -2579,6 +2585,8 @@ public void onFinishedGoingToSleep() { updateNotificationPanelTouchState(); getNotificationShadeWindowViewController().cancelCurrentTouch(); + + mBurnInProtectionController.stopShiftTimer(); if (mLaunchCameraOnFinishedGoingToSleep) { mLaunchCameraOnFinishedGoingToSleep = false; @@ -2718,6 +2726,7 @@ && launchWalletViaSysuiCallbacks()) { } } updateScrimController(); + mBurnInProtectionController.startShiftTimer(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 0cb222df52ce..01227c43ea07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -19,6 +19,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; @@ -46,6 +47,7 @@ import com.android.systemui.shade.StatusBarLongPressGestureDetector; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; +import com.android.systemui.statusbar.policy.Offset; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; @@ -84,6 +86,9 @@ public class PhoneStatusBarView extends FrameLayout { private int mStatusBarPaddingTop = 0; private int mStatusBarPaddingEnd = 0; + @Nullable + private ViewGroup mStatusBarContents = null; + /** * Draw this many pixels into the left/right side of the cutout to optimally use the space */ @@ -129,10 +134,20 @@ public void updateTouchableRegion(Region touchableRegion) { getViewRootImpl().setTouchableRegion(touchableRegion); } + public void offsetStatusBar(Offset offset) { + if (mStatusBarContents == null) { + return; + } + mStatusBarContents.setTranslationX(offset.getX()); + mStatusBarContents.setTranslationY(offset.getY()); + invalidate(); + } + @Override public void onFinishInflate() { super.onFinishInflate(); mCutoutSpace = findViewById(R.id.cutout_space_view); + mStatusBarContents = (ViewGroup) findViewById(R.id.status_bar_contents); updateResources(); } @@ -367,7 +382,7 @@ private void updatePaddings() { int statusBarPaddingStart = getResources().getDimensionPixelSize( R.dimen.status_bar_padding_start); - findViewById(R.id.status_bar_contents).setPaddingRelative( + mStatusBarContents.setPaddingRelative( statusBarPaddingStart + mStatusBarPaddingStart, getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top) + mStatusBarPaddingTop, getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end) + mStatusBarPaddingEnd, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index efff4ef82842..63be7d4d88e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -350,6 +350,10 @@ private constructor( darkIconDispatcher.removeDarkReceiver(clockRight) } + fun getPhoneStatusBarView(): PhoneStatusBarView { + return mView + } + inner class PhoneStatusBarViewTouchHandler : Gefingerpoken { private val touchSlop = ViewConfiguration.get(mView.context).scaledTouchSlop private var initialTouchX = 0f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt new file mode 100644 index 000000000000..472b19798245 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2017-2018 Paranoid Android + * Copyright (C) 2022 FlamingoOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.content.Context +import android.util.Log + +import com.android.systemui.res.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.doze.util.getBurnInOffset +import com.android.systemui.statusbar.phone.PhoneStatusBarView +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener + +import javax.inject.Inject + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +private const val BURN_IN_PREVENTION_PERIOD = 83f +private const val UPDATE_INTERVAL = 1000 * 10L + +private val TAG = BurnInProtectionController::class.simpleName + +@SysUISingleton +class BurnInProtectionController @Inject constructor( + private val context: Context, + configurationController: ConfigurationController, +) : ConfigurationListener { + + private val coroutineScope = CoroutineScope(Dispatchers.Main) + + private val shiftEnabled = context.resources.getBoolean(com.android.internal.R.bool.config_enableBurnInProtection) + + private var phoneStatusBarView: PhoneStatusBarView? = null + + private var shiftJob: Job? = null + + private var maxStatusBarOffsetX = 0 + private var maxStatusBarOffsetY = 0 + + private var statusBarOffset = Offset.Zero + + init { + logD { + "shiftEnabled = $shiftEnabled}" + } + configurationController.addCallback(this) + loadResources() + } + + private fun loadResources() { + with(context.resources) { + maxStatusBarOffsetX = minOf( + getDimensionPixelSize(R.dimen.status_bar_padding_start), + getDimensionPixelSize(R.dimen.status_bar_padding_end) + ) / 2 + maxStatusBarOffsetY = getDimensionPixelSize(R.dimen.status_bar_offset_max_y) / 2 + } + logD { + "maxStatusBarOffsetX = $maxStatusBarOffsetX, maxStatusBarOffsetY = $maxStatusBarOffsetY" + } + } + + fun setPhoneStatusBarView(phoneStatusBarView: PhoneStatusBarView?) { + this.phoneStatusBarView = phoneStatusBarView + } + + fun startShiftTimer() { + if (!shiftEnabled || (shiftJob?.isActive == true)) return + shiftJob = coroutineScope.launch { + while (isActive) { + val sbOffset = Offset( + getBurnInOffsetX(maxStatusBarOffsetX), + getBurnInOffsetY(maxStatusBarOffsetY) + ) + logD { + "new offsets: sbOffset = $sbOffset" + } + updateViews(sbOffset) + delay(UPDATE_INTERVAL) + } + } + logD { + "Started shift job" + } + } + + private fun updateViews(sbOffset: Offset) { + if (sbOffset != statusBarOffset) { + logD { + "Translating statusbar" + } + phoneStatusBarView?.offsetStatusBar(sbOffset) + statusBarOffset = sbOffset + } + } + + fun stopShiftTimer() { + if (!shiftEnabled || (shiftJob?.isActive != true)) return + logD { + "Cancelling shift job" + } + coroutineScope.launch { + shiftJob?.cancelAndJoin() + updateViews(Offset.Zero) + logD { + "Cancelled shift job" + } + } + } + + override fun onDensityOrFontScaleChanged() { + logD { + "onDensityOrFontScaleChanged" + } + loadResources() + } +} + +private fun getBurnInOffsetX(maxOffset: Int): Int { + return maxOffset - getBurnInOffset( + amplitude = maxOffset * 2, + xAxis = true, + periodX = BURN_IN_PREVENTION_PERIOD, + periodY = BURN_IN_PREVENTION_PERIOD + ) +} + +private fun getBurnInOffsetY(maxOffset: Int): Int { + return maxOffset - getBurnInOffset( + amplitude = maxOffset * 2, + xAxis = false, + periodX = BURN_IN_PREVENTION_PERIOD, + periodY = BURN_IN_PREVENTION_PERIOD + ) +} + +private inline fun logD(crossinline msg: () -> String) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, msg()) + } +} + +data class Offset( + val x: Int, + val y: Int +) { + companion object { + val Zero = Offset(0, 0) + } +} From 65b2ee677cffb74a5bf12e3d82e66d168388e9bc Mon Sep 17 00:00:00 2001 From: Anushek Prasal Date: Thu, 6 Jun 2019 14:18:24 +0530 Subject: [PATCH 0500/1315] Add toggle to disable charging animation [1/2] @neobuddy89: Use same toggle for wireless charging animation too. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++++++ .../java/com/android/server/power/Notifier.java | 16 ++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1c25349334ee..056df8ff23dd 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7385,6 +7385,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean public static final String NOTIFICATION_GUTS_KILL_APP_BUTTON = "notification_guts_kill_app_button"; + /** + * Whether to show charging animation + * @hide + */ + public static final String CHARGING_ANIMATION = "charging_animation"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 4a2d777b0319..52f2409be0ee 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -174,10 +174,6 @@ public class Notifier { // True if the device should suspend when the screen is off due to proximity. private final boolean mSuspendWhenScreenOffDueToProximityConfig; - // True if the device should show the wireless charging animation when the device - // begins charging wirelessly - private final boolean mShowWirelessChargingAnimationConfig; - // Encapsulates interactivity information about a particular display group. private static class Interactivity { public boolean isInteractive = true; @@ -258,8 +254,6 @@ public Notifier(Looper looper, Context context, IBatteryStats batteryStats, mSuspendWhenScreenOffDueToProximityConfig = context.getResources().getBoolean( com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity); - mShowWirelessChargingAnimationConfig = context.getResources().getBoolean( - com.android.internal.R.bool.config_showBuiltinWirelessChargingAnim); mFullWakeLockLog = mInjector.getWakeLockLog(context); mPartialWakeLockLog = mInjector.getWakeLockLog(context); @@ -1226,21 +1220,27 @@ private void playChargingStartedFeedback(@UserIdInt int userId, boolean wireless } private void showWirelessChargingStarted(int batteryLevel, @UserIdInt int userId) { + final boolean animationEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.CHARGING_ANIMATION, 1, userId) == 1; + // play sounds + haptics playChargingStartedFeedback(userId, true /* wireless */); // show animation - if (mShowWirelessChargingAnimationConfig && mStatusBarManagerInternal != null) { + if (animationEnabled && mStatusBarManagerInternal != null) { mStatusBarManagerInternal.showChargingAnimation(batteryLevel); } mSuspendBlocker.release(); } private void showWiredChargingStarted(int batteryLevel, @UserIdInt int userId) { + final boolean animationEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.CHARGING_ANIMATION, 1, userId) == 1; + playChargingStartedFeedback(userId, false /* wireless */); // show animation - if (mStatusBarManagerInternal != null) { + if (animationEnabled && mStatusBarManagerInternal != null) { mStatusBarManagerInternal.showChargingAnimation(batteryLevel); } mSuspendBlocker.release(); From 64fd28596340d9412d1639eb13044bdc6d9a7823 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 8 Apr 2017 20:45:42 +0530 Subject: [PATCH 0501/1315] Allow tuning ambient display with sensors [1/3] Squashed: From: Hikari-no-Tenshi Date: Fri, 7 Feb 2020 23:39:42 +0200 Subject: [PATCH] Allow devices to set proximity sensor type for ambient display [1/2] true - wake-up false - non wake-up From: Pranav Vashi Date: Wed, 29 Dec 2021 11:30:11 +0530 Subject: [PATCH] Allow to wake the screen instead of pulsing [1/2] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../display/AmbientDisplayConfiguration.java | 31 ++++++++++++++++++ core/java/android/provider/Settings.java | 32 ++++++++++++++++++- core/res/res/values/matrixx_config.xml | 5 +++ core/res/res/values/matrixx_symbols.xml | 5 +++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index 7a60306b755b..5e3cf941a0c2 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -98,6 +98,9 @@ public boolean enabled(int user) { || wakeLockScreenGestureEnabled(user) || wakeDisplayGestureEnabled(user) || pickupGestureEnabled(user) + || tiltGestureEnabled(user) + || handwaveGestureEnabled(user) + || pocketGestureEnabled(user) || tapGestureEnabled(user) || doubleTapGestureEnabled(user) || quickPickupSensorEnabled(user) @@ -129,6 +132,34 @@ public boolean dozePickupSensorAvailable() { return mContext.getResources().getBoolean(R.bool.config_dozePulsePickup); } + /** {@hide} */ + public boolean tiltGestureEnabled(int user) { + return boolSettingDefaultOff(Settings.Secure.DOZE_TILT_GESTURE, user) + && dozeTiltSensorAvailable(); + } + + /** {@hide} */ + public boolean dozeTiltSensorAvailable() { + return mContext.getResources().getBoolean(R.bool.config_dozePulseTilt); + } + + /** {@hide} */ + public boolean handwaveGestureEnabled(int user) { + return boolSettingDefaultOff(Settings.Secure.DOZE_HANDWAVE_GESTURE, user) + && dozeProximitySensorAvailable(); + } + + /** {@hide} */ + public boolean pocketGestureEnabled(int user) { + return boolSettingDefaultOff(Settings.Secure.DOZE_POCKET_GESTURE, user) + && dozeProximitySensorAvailable(); + } + + /** {@hide} */ + public boolean dozeProximitySensorAvailable() { + return mContext.getResources().getBoolean(R.bool.config_dozePulseProximity); + } + /** @hide */ public boolean edgeLightEnabled(int user) { return Settings.System.getIntForUser(mContext.getContentResolver(), diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 056df8ff23dd..66032bab56fa 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11324,7 +11324,7 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val * @hide */ @Readable - public static final String DOZE_PICK_UP_GESTURE = "doze_pulse_on_pick_up"; + public static final String DOZE_PICK_UP_GESTURE = "doze_pick_up_gesture"; /** * Whether the device should pulse on long press gesture. @@ -11376,6 +11376,36 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Readable public static final String SUPPRESS_DOZE = "suppress_doze"; + /** + * Pulse notifications on tilt + * @hide + */ + public static final String DOZE_TILT_GESTURE = "doze_tilt_gesture"; + + /** + * Pulse notifications on hand wave + * @hide + */ + public static final String DOZE_HANDWAVE_GESTURE = "doze_handwave_gesture"; + + /** + * Pulse notifications on removal from pocket + * @hide + */ + public static final String DOZE_POCKET_GESTURE = "doze_pocket_gesture"; + + /** + * Wake up instead of pulsing notifications + * @hide + */ + public static final String RAISE_TO_WAKE_GESTURE = "raise_to_wake_gesture"; + + /** + * Vibrate when pulsing notifications on gesture + * @hide + */ + public static final String DOZE_GESTURE_VIBRATE = "doze_gesture_vibrate"; + /** * Gesture that skips media. * @hide diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index e73f8f588e82..178075dedd90 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -90,4 +90,9 @@ 1.0 + + + false + false + false diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 40814b4d80d3..295dfc904772 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -76,4 +76,9 @@ + + + + + From 6f49cb7dceb86de453a0bb5984a4bbd5fa29bbf0 Mon Sep 17 00:00:00 2001 From: darkobas Date: Mon, 7 Oct 2019 20:31:48 +0200 Subject: [PATCH 0502/1315] base: Add Doze-on-charge customization [1/2] @idoybh edits: Adapted to A11's settings backup [jhonboy121]: adapt to A12 changes in DozeParameters. Also in the og commit DOZE_ALWAYS_ON was being read from System namespace instead of Secure, fixed it here. Make use of SettingsProxy utlity classes for settings / getting values Change-Id: I831583fde68de15788e3d7ecab55d864726d140f Commit message #2: base: check whether device is charging when alwaysOnChargingEnabled is called * Using settings for this is quite redundant. And if you toggle aod on charge after plugging in then aod won't turn on unless you unplug and plug again (since DOZE_ON_CHARGE_NOW is set only if aod on charge is enabled and device is plugged in" Signed-off-by: jhonboy121 Commit message #3: base: fix deadlock between activity manager and power manager * Using the battery manager intent to query plugged in status was the root cause of deadlock, so inside power manager, user mIsPowered and setting value instead of using the intent based power status Signed-off-by: jhonboy121 [jhonboy121]: adapted to A13 Co-authored-by: jhonboy121 Signed-off-by: jhonboy121 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../display/AmbientDisplayConfiguration.java | 34 ++++++++++++++++--- core/java/android/provider/Settings.java | 6 ++++ .../statusbar/phone/DozeParameters.java | 6 ++-- .../server/power/PowerManagerService.java | 4 ++- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index 5e3cf941a0c2..777f48693996 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -20,7 +20,10 @@ import android.annotation.TestApi; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.biometrics.Flags; +import android.os.BatteryManager; import android.os.Build; import android.os.SystemProperties; import android.provider.Settings; @@ -40,7 +43,8 @@ */ @TestApi public class AmbientDisplayConfiguration { - private static final String TAG = "AmbientDisplayConfig"; + private static final IntentFilter sIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + private final Context mContext; private final boolean mAlwaysOnByDefault; private final boolean mPickupGestureEnabledByDefault; @@ -58,7 +62,8 @@ public class AmbientDisplayConfiguration { Settings.Secure.DOZE_DOUBLE_TAP_GESTURE, Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE, Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE, - Settings.Secure.DOZE_TAP_SCREEN_GESTURE + Settings.Secure.DOZE_TAP_SCREEN_GESTURE, + Settings.Secure.DOZE_ON_CHARGE }; /** Non-user configurable doze settings */ @@ -285,8 +290,29 @@ private boolean pulseOnLongPressAvailable() { */ @TestApi public boolean alwaysOnEnabled(int user) { - return boolSetting(Settings.Secure.DOZE_ALWAYS_ON, user, mAlwaysOnByDefault ? 1 : 0) - && alwaysOnAvailable() && !accessibilityInversionEnabled(user); + return alwaysOnEnabledSetting(user) || alwaysOnChargingEnabled(user); + } + + public boolean alwaysOnEnabledSetting(int user) { + final boolean alwaysOnEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.DOZE_ALWAYS_ON, + mAlwaysOnByDefault ? 1 : 0, user) == 1; + return alwaysOnEnabled && alwaysOnAvailable() && !accessibilityInversionEnabled(user); + } + + public boolean alwaysOnChargingEnabledSetting(int user) { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.DOZE_ON_CHARGE, 0, user) == 1; + } + + private boolean alwaysOnChargingEnabled(int user) { + if (alwaysOnChargingEnabledSetting(user)) { + final Intent intent = mContext.registerReceiver(null, sIntentFilter, Context.RECEIVER_NOT_EXPORTED); + if (intent != null) { + return intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; + } + } + return false; } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 66032bab56fa..e3b365fe1b63 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14115,6 +14115,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String SHOW_CLIPBOARD_OVERLAY = "show_clipboard_overlay"; + /** + * Whether to enable DOZE only when charging + * @hide + */ + public static final String DOZE_ON_CHARGE = "doze_on_charge"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index 2b5813eca47d..412dc32f7939 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -96,7 +96,6 @@ public class DozeParameters implements private final SecureSettings mSecureSettings; private final Optional mMinModeManager; - private boolean mDozeAlwaysOn; private boolean mControlScreenOffAnimation; private boolean mIsQuickPickupEnabled; @@ -289,7 +288,8 @@ public boolean isMinModeActive() { * @return {@code true} if enabled and available. */ public boolean getAlwaysOn() { - return (mDozeAlwaysOn && !mBatteryController.isAodPowerSave()) || isMinModeActive(); + return (mAmbientDisplayConfiguration.alwaysOnEnabled(mUserTracker.getUserId()) + && !mBatteryController.isAodPowerSave()) || isMinModeActive(); } /** @@ -448,8 +448,6 @@ public String[] brightnessNames() { @Override public void onTuningChanged(String key, String newValue) { - mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(mUserTracker.getUserId()); - if (key.equals(Settings.Secure.DOZE_ALWAYS_ON)) { updateControlScreenOff(); } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 95d3948dc2d0..a5da8365142b 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1739,7 +1739,9 @@ private void updateSettingsLocked() { mWakeUpWhenPluggedOrUnpluggedSetting = LineageSettings.Global.getInt(resolver, LineageSettings.Global.WAKE_WHEN_PLUGGED_OR_UNPLUGGED, (mWakeUpWhenPluggedOrUnpluggedConfig ? 1 : 0)) == 1; - mAlwaysOnEnabled = mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT); + mAlwaysOnEnabled = mAmbientDisplayConfiguration.alwaysOnEnabledSetting(UserHandle.USER_CURRENT) + || (mAmbientDisplayConfiguration.alwaysOnChargingEnabledSetting( + UserHandle.USER_CURRENT) && mIsPowered); if (mSupportsDoubleTapWakeConfig) { boolean doubleTapWakeEnabled = Settings.Secure.getIntForUser(resolver, From 7021aee65d8a89ba7cfa11f283b471ef97c19b23 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 1 Apr 2023 13:30:59 +0530 Subject: [PATCH 0503/1315] Doze-on-charge: Add few improvements and fixes * Update detection logic when device is plugged in and is charging. * Fix issue in PowerManagerService - mIsPowered is updated dynamically and so it should be checked everytime, not just in init. * When plugging out, let device wake up. This should fix odd blinking issues in some devices. * Add DOZE_ON_CHARGE to content observer, duh. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../display/AmbientDisplayConfiguration.java | 11 ++++++++++- .../server/power/PowerManagerService.java | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index 777f48693996..364ed990f760 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -293,6 +293,7 @@ public boolean alwaysOnEnabled(int user) { return alwaysOnEnabledSetting(user) || alwaysOnChargingEnabled(user); } + /** @hide */ public boolean alwaysOnEnabledSetting(int user) { final boolean alwaysOnEnabled = Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.DOZE_ALWAYS_ON, @@ -300,6 +301,7 @@ public boolean alwaysOnEnabledSetting(int user) { return alwaysOnEnabled && alwaysOnAvailable() && !accessibilityInversionEnabled(user); } + /** @hide */ public boolean alwaysOnChargingEnabledSetting(int user) { return Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.DOZE_ON_CHARGE, 0, user) == 1; @@ -309,7 +311,14 @@ private boolean alwaysOnChargingEnabled(int user) { if (alwaysOnChargingEnabledSetting(user)) { final Intent intent = mContext.registerReceiver(null, sIntentFilter, Context.RECEIVER_NOT_EXPORTED); if (intent != null) { - return intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL; + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + boolean isPlugged = plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB || + plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS; + return isPlugged && isCharging; } } return false; diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index a5da8365142b..8be588d3ce82 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -733,6 +733,9 @@ public final class PowerManagerService extends SystemService // True if always on display is enabled private boolean mAlwaysOnEnabled; + // True if always on charging is enabled + private boolean mAlwaysOnChargingEnabled; + // True if double tap to wake is enabled private boolean mDoubleTapWakeEnabled; @@ -1598,6 +1601,9 @@ private void systemReady() { resolver.registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.DOZE_ALWAYS_ON), false, mSettingsObserver, UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.DOZE_ON_CHARGE), + false, mSettingsObserver, UserHandle.USER_ALL); resolver.registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.DOUBLE_TAP_TO_WAKE), false, mSettingsObserver, UserHandle.USER_ALL); @@ -1739,9 +1745,9 @@ private void updateSettingsLocked() { mWakeUpWhenPluggedOrUnpluggedSetting = LineageSettings.Global.getInt(resolver, LineageSettings.Global.WAKE_WHEN_PLUGGED_OR_UNPLUGGED, (mWakeUpWhenPluggedOrUnpluggedConfig ? 1 : 0)) == 1; - mAlwaysOnEnabled = mAmbientDisplayConfiguration.alwaysOnEnabledSetting(UserHandle.USER_CURRENT) - || (mAmbientDisplayConfiguration.alwaysOnChargingEnabledSetting( - UserHandle.USER_CURRENT) && mIsPowered); + mAlwaysOnEnabled = mAmbientDisplayConfiguration.alwaysOnEnabledSetting(UserHandle.USER_CURRENT); + mAlwaysOnChargingEnabled = mAmbientDisplayConfiguration.alwaysOnChargingEnabledSetting( + UserHandle.USER_CURRENT); if (mSupportsDoubleTapWakeConfig) { boolean doubleTapWakeEnabled = Settings.Secure.getIntForUser(resolver, @@ -3027,6 +3033,11 @@ && getGlobalWakefulnessLocked() == WAKEFULNESS_DREAMING return false; } + // On Always On Charging, SystemUI shows the charging indicator + if (mAlwaysOnChargingEnabled && mIsPowered && getGlobalWakefulnessLocked() == WAKEFULNESS_DOZING) { + return false; + } + // Otherwise wake up! return true; } From 70203bf58e617e977aebdf59ba41dd8053038784 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Sat, 27 Feb 2021 01:52:33 +0200 Subject: [PATCH 0504/1315] base: Allow scheduling always on display [1/2] From sunset to sunrise or at a custom time Squashed: From: Ido Ben-Hur Date: Tue, 16 Mar 2021 13:53:27 +0200 Subject: AutoAODService: Add support for mixed time & sun modes [1/2] Also refactor some code Change-Id: I3a78dec88f532766d00e0d1d276c27dc9d3dc68f Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Fri, 26 Mar 2021 08:45:31 +0300 Subject: AutoAODService: Account for disabled doze Change-Id: I30f52c4e3db2a27f9cde01662a25eee33e225414 Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Fri, 9 Apr 2021 13:33:41 +0300 Subject: AutoAODService: Slightly improve code and docs * Correctly link local vars and functions in docs * Make vars we can final * Don't use String.valueOf() where we don't have to * Should be Integer.parseInt() and not Integer.valueOf() Change-Id: I5892858c7142113ad3c9c87ddf00e58b18508207 Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Wed, 2 Mar 2022 19:05:43 +0200 Subject: AutoAODService: Improve some code And call using the right handler on settings change Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Thu, 31 Mar 2022 20:59:38 +0300 Subject: AutoAODService: Use Calendar.add instead of Calendar.roll Roll doesn't always give the intended result which can cause an alarm to be set to the distance past/future on the end of the month Also make sure we cancel previous alarms before setting new ones, just incase Signed-off-by: Pranav Vashi From: cjh1249131356 Date: Fri, 20 May 2022 22:41:49 +0800 Subject: base: Fix scheduled AOD - Always init state when settings change observed, as we need to update scheduled time, not only auto mode. - Fix an edge case which can be reproduced with following steps: Set phone to 00:00 Set an overnight schedule time (23:00 ~ 07:00 etc) Then three Calendar instances hold following dates: current: 2022/05/20 00:00 since: 2022/05/20 23:00 till: 2022/05/20 07:00 According to the first if condition, till date will be 2022/05/21 07:00 Then issue comes, till time can never be reached until you set a till time after since time. Signed-off-by: cjh1249131356 Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Sun, 11 Dec 2022 15:52:01 +0200 Subject: AutoAODService: Properly handle reboots & fix some logic Use shared preferences to find whether the user aborted current scheduled session In case that happens - don't automatically toggle AOD on boot until we passed the next alarm In addition: * Don't toggle AOD onTwilightStateChanged nor in mTimeChangedReceiver - they should just change the alarms * Properly re-init on ALL setting changes, not just mode * Never disable AOD on boot just because the setting is disabled * Never trigger doze / screen on when interactive Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Mon, 9 Jan 2023 06:55:56 +0200 Subject: AutoAODService: Better check for doze enablement Signed-off-by: Pranav Vashi Change-Id: Ib76ecb1855ac215de6d7cd2f2346abfce6cc2214 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 22 + .../server/display/AutoAODService.java | 517 ++++++++++++++++++ .../java/com/android/server/SystemServer.java | 7 + 3 files changed, 546 insertions(+) create mode 100644 services/core/java/com/android/server/display/AutoAODService.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e3b365fe1b63..62cb02939e18 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11309,6 +11309,28 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Readable public static final String DOZE_ALWAYS_ON = "doze_always_on"; + /** + * Indicates whether doze turns on automatically + * 0 = disabled (default) + * 1 = from sunset to sunrise + * 2 = custom time + * 3 = from sunset till a time + * 4 = from a time till sunrise + * @hide + */ + @Readable + public static final String DOZE_ALWAYS_ON_AUTO_MODE = "doze_always_on_auto_mode"; + + /** + * The custom time {@link DOZE_ALWAYS_ON} should be on at + * Only relevant when {@link DOZE_ALWAYS_ON_AUTO_MODE} is set to 2 and above + * 0 = Disabled (default) + * format: HH:mm,HH:mm (since,till) + * @hide + */ + @Readable + public static final String DOZE_ALWAYS_ON_AUTO_TIME = "doze_always_on_auto_time"; + /** * Indicates whether ambient wallpaper is visible with AOD. *

diff --git a/services/core/java/com/android/server/display/AutoAODService.java b/services/core/java/com/android/server/display/AutoAODService.java new file mode 100644 index 000000000000..89d8557dd195 --- /dev/null +++ b/services/core/java/com/android/server/display/AutoAODService.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2021 Yet Another AOSP Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.display; + +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.hardware.display.AmbientDisplayConfiguration; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; + +import com.android.server.SystemService; +import com.android.server.twilight.TwilightListener; +import com.android.server.twilight.TwilightManager; +import com.android.server.twilight.TwilightState; + +import java.io.File; +import java.lang.IllegalArgumentException; +import java.util.Calendar; + +public class AutoAODService extends SystemService { + + private static final String TAG = "AutoAODService"; + private static final String PULSE_ACTION = "com.android.systemui.doze.pulse"; + private static final String PREF_DIR_NAME = "shared_prefs"; + private static final String PREF_FILE_NAME = TAG + "_preferences.xml"; + private static final String PREF_STATE_KEY = TAG + "_last_state"; + private static final String PREF_TIME_KEY = TAG + "_last_time"; + private static final int WAKELOCK_TIMEOUT_MS = 3000; + + /** + * Disabled state (default) + */ + private static final int MODE_DISABLED = 0; + /** + * Active from sunset to sunrise + */ + private static final int MODE_NIGHT = 1; + /** + * Active at a user set time + */ + private static final int MODE_TIME = 2; + /** + * Active from sunset till a time + */ + private static final int MODE_MIXED_SUNSET = 3; + /** + * Active from a time till sunrise + */ + private static final int MODE_MIXED_SUNRISE = 4; + + private final AlarmManager mAlarmManager; + private final Context mContext; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private TwilightManager mTwilightManager; + private TwilightState mTwilightState; + private SharedPreferences mSharedPreferences; + private AmbientDisplayConfiguration mAmbientConfig; + + /** + * Current operation mode + * Can either be {@link #MODE_DISABLED}, {@link #MODE_NIGHT} or {@link #MODE_TIME} + */ + private int mMode = MODE_DISABLED; + /** + * Whether AOD is currently active + */ + private boolean mActive = false; + /** + * Whether next alarm should enable or disable AOD + */ + private boolean mIsNextActivate = false; + + private boolean mTwilightRegistered = false; + private boolean mTimeRegistered = false; + private boolean mSelfChange = false; + private boolean mOverrideOnce = false; + private long mLastSetTime = 0; + + private final TwilightListener mTwilightListener = new TwilightListener() { + @Override + public void onTwilightStateChanged(@Nullable TwilightState state) { + if (mMode != MODE_NIGHT && mMode < MODE_MIXED_SUNSET) { + // just incase + setTwilightListener(false); + return; + } + Slog.v(TAG, "onTwilightStateChanged state: " + state); + if (state == null) return; + mTwilightState = state; + if (mMode < MODE_MIXED_SUNSET) mHandler.post(() -> maybeActivateNight(false)); + else mHandler.post(() -> maybeActivateTime(false)); + } + }; + + private final BroadcastReceiver mTimeChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mMode != MODE_TIME && mMode < MODE_MIXED_SUNSET) { + // just incase + setTimeReciever(false); + return; + } + Slog.v(TAG, "mTimeChangedReceiver onReceive"); + mHandler.post(() -> maybeActivateTime(false)); + } + }; + + /** + * A class to manage and handle alarms + */ + private class Alarm implements AlarmManager.OnAlarmListener { + @Override + public void onAlarm() { + Slog.v(TAG, "onAlarm"); + mHandler.post(() -> setAutoAODActive(mIsNextActivate)); + if (mMode == MODE_TIME || mMode >= MODE_MIXED_SUNSET) + mHandler.post(() -> maybeActivateTime(false)); + else + maybeActivateNight(false); + } + + /** + * Set a new alarm using a Calendar + * @param time time as Calendar + */ + public void set(Calendar time) { + set(time.getTimeInMillis()); + } + + /** + * Set a new alarm using ms since epoch + * @param time time as ms since epoch + */ + public void set(long time) { + cancel(); // making sure there is no more than 1 + mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, + time, TAG, this, mHandler); + mLastSetTime = time; + Slog.v(TAG, "new alarm set to " + time + + " mIsNextActivate=" + mIsNextActivate); + } + + public void cancel() { + mAlarmManager.cancel(this); + mLastSetTime = 0; + Slog.v(TAG, "alarm cancelled"); + } + } + + private final Alarm mAlarm = new Alarm(); + + private class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.DOZE_ALWAYS_ON_AUTO_MODE), + false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.DOZE_ALWAYS_ON_AUTO_TIME), + false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.DOZE_ALWAYS_ON), + false, this, UserHandle.USER_ALL); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri.getLastPathSegment().equals(Settings.Secure.DOZE_ALWAYS_ON)) { + if (mSelfChange) { + mSelfChange = false; + return; + } + + mActive = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.DOZE_ALWAYS_ON, 0, + UserHandle.USER_CURRENT) == 1; + + if (mLastSetTime != 0 && mActive == mIsNextActivate) { + // we have a future alarm set and user left current state + // save the next alarm + Slog.v(TAG, "user abandoned state. active: " + mActive); + mSharedPreferences.edit() + .putLong(PREF_TIME_KEY, mLastSetTime).apply(); + return; + } + Slog.v(TAG, "removing PREF_TIME_KEY. active: " + mActive); + mSharedPreferences.edit().remove(PREF_TIME_KEY).apply(); + return; + } + mHandler.post(() -> initState()); + } + } + + private final SettingsObserver mSettingsObserver; + + public AutoAODService(Context context) { + super(context); + mContext = context; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mSettingsObserver = new SettingsObserver(mHandler); + mActive = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.DOZE_ALWAYS_ON, 0, + UserHandle.USER_CURRENT) == 1; + } + + @Override + public void onStart() { + Slog.v(TAG, "Starting " + TAG); + publishLocalService(AutoAODService.class, this); + mSettingsObserver.observe(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { + Slog.v(TAG, "onBootPhase PHASE_SYSTEM_SERVICES_READY"); + mTwilightManager = getLocalService(TwilightManager.class); + } else if (phase == SystemService.PHASE_BOOT_COMPLETED) { + Slog.v(TAG, "onBootPhase PHASE_BOOT_COMPLETED"); + // we want to access our shared preferences before unlock + // use device encrypted storage & context for that + // also make sure to not store any sensitive data there + final File prefsFile = new File( + new File(Environment.getDataSystemDeDirectory( + UserHandle.USER_SYSTEM), PREF_DIR_NAME), PREF_FILE_NAME); + mSharedPreferences = mContext.createDeviceProtectedStorageContext() + .getSharedPreferences(prefsFile, Context.MODE_PRIVATE); + mHandler.post(() -> initState(true)); + } + } + + /** + * Registers or unregisters {@link #mTimeChangedReceiver} + * @param register Register when true, unregister when false + */ + private void setTimeReciever(boolean register) { + if (register) { + Slog.v(TAG, "Registering mTimeChangedReceiver"); + final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED); + intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + mContext.registerReceiver(mTimeChangedReceiver, intentFilter); + mTimeRegistered = true; + return; + } + try { + mContext.unregisterReceiver(mTimeChangedReceiver); + Slog.v(TAG, "Unregistered mTimeChangedReceiver"); + } catch (IllegalArgumentException e) { + // nothing to do. Already unregistered + } + mTimeRegistered = false; + } + + /** + * Registers or unregisters {@link #mTwilightListener} + * @param register Register when true, unregister when false + */ + private void setTwilightListener(boolean register) { + if (register) { + Slog.v(TAG, "Registering mTwilightListener"); + mTwilightManager.registerListener(mTwilightListener, mHandler); + mTwilightState = mTwilightManager.getLastTwilightState(); + mTwilightRegistered = true; + return; + } + try { + mTwilightManager.unregisterListener(mTwilightListener); + Slog.v(TAG, "Unregistered mTwilightListener"); + } catch (IllegalArgumentException e) { + // nothing to do. Already unregistered + } + mTwilightRegistered = false; + } + + /** + * See {@link #initState(boolean)} + */ + private void initState() { + initState(false); + } + + /** + * Initiates the state according to user settings + * Registers or unregisters listeners and calls {@link #maybeActivateAOD()} + * @param boot true if triggered by boot + */ + private void initState(boolean boot) { + if (boot && mSharedPreferences.contains(PREF_TIME_KEY)) { + final long prefTime = mSharedPreferences.getLong(PREF_TIME_KEY, 0); + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(prefTime); + // skip setting AOD once if we left the state before a reboot + if (cal.after(Calendar.getInstance())) + mOverrideOnce = true; + } + + final int mode = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.DOZE_ALWAYS_ON_AUTO_MODE, MODE_DISABLED, + UserHandle.USER_CURRENT); + mMode = mode; + mAlarm.cancel(); // cancelling set alarm + // unregister all registered listeners + if (mTimeRegistered) setTimeReciever(false); + if (mTwilightRegistered) setTwilightListener(false); + // erase shared preferences + if (!boot) mSharedPreferences.edit().remove(PREF_TIME_KEY).apply(); + switch (mMode) { + default: + case MODE_DISABLED: + return; + case MODE_TIME: + setTimeReciever(true); + break; + case MODE_NIGHT: + setTwilightListener(true); + break; + case MODE_MIXED_SUNSET: + case MODE_MIXED_SUNRISE: + setTwilightListener(true); + setTimeReciever(true); + break; + } + maybeActivateAOD(); + } + + /** + * Calls the correct function to set the next alarm according to {@link #mMode} + */ + private void maybeActivateAOD() { + switch (mMode) { + default: + case MODE_DISABLED: + break; + case MODE_NIGHT: + maybeActivateNight(); + break; + case MODE_TIME: + case MODE_MIXED_SUNSET: + case MODE_MIXED_SUNRISE: + maybeActivateTime(); + break; + } + } + + /** + * See {@link #maybeActivateNight(boolean)} + */ + private void maybeActivateNight() { + maybeActivateNight(true); + } + + /** + * Sets the next alarm for {@link #MODE_NIGHT} + * @param setActive Whether to set activation state. + * When false only updates the alarm + */ + private void maybeActivateNight(boolean setActive) { + if (mTwilightState == null) { + Slog.e(TAG, "aborting maybeActivateNight(). mTwilightState is null"); + return; + } + mIsNextActivate = !mTwilightState.isNight(); + mAlarm.set(mIsNextActivate ? mTwilightState.sunsetTimeMillis() + : mTwilightState.sunriseTimeMillis()); + if (setActive) mHandler.post(() -> setAutoAODActive(!mIsNextActivate)); + } + + /** + * See {@link #maybeActivateTime(boolean)} + */ + private void maybeActivateTime() { + maybeActivateTime(true); + } + + /** + * Sets the next alarm for {@link #MODE_TIME}, {@link #MODE_MIXED_SUNSET} and + * {@link #MODE_MIXED_SUNRISE} + * @param setActive Whether to set activation state + * When false only updates the alarm + */ + private void maybeActivateTime(boolean setActive) { + Calendar currentTime = Calendar.getInstance(); + Calendar since = Calendar.getInstance(); + Calendar till = Calendar.getInstance(); + String value = Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.DOZE_ALWAYS_ON_AUTO_TIME, UserHandle.USER_CURRENT); + if (value == null || value.equals("")) value = "20:00,07:00"; + String[] times = value.split(",", 0); + String[] sinceValues = times[0].split(":", 0); + String[] tillValues = times[1].split(":", 0); + since.set(Calendar.HOUR_OF_DAY, Integer.parseInt(sinceValues[0])); + since.set(Calendar.MINUTE, Integer.parseInt(sinceValues[1])); + since.set(Calendar.SECOND, 0); + till.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tillValues[0])); + till.set(Calendar.MINUTE, Integer.parseInt(tillValues[1])); + till.set(Calendar.SECOND, 0); + + // handle mixed modes + if (mMode >= MODE_MIXED_SUNSET) { + if (mTwilightState == null) { + Slog.e(TAG, "aborting maybeActivateTime(). mTwilightState is null"); + return; + } + if (mMode == MODE_MIXED_SUNSET) { + since.setTimeInMillis(mTwilightState.sunsetTimeMillis()); + } else { // MODE_MIXED_SUNRISE + till.setTimeInMillis(mTwilightState.sunriseTimeMillis()); + if (!mTwilightState.isNight()) till.add(Calendar.DATE, 1); + } + } + + if (currentTime.before(since) && currentTime.before(till) && till.compareTo(since) < 0) { + since.add(Calendar.DATE, -1); + } + // roll to the next day if needed be + if (since.after(till)) till.add(Calendar.DATE, 1); + if (currentTime.after(since) && currentTime.compareTo(till) >= 0) { + since.add(Calendar.DATE, 1); + till.add(Calendar.DATE, 1); + } + // abort if the user was dumb enough to set the same time + if (since.compareTo(till) == 0) { + Slog.e(TAG, "Aborting maybeActivateTime(). Time diff is 0"); + return; + } + + // update the next alarm + mIsNextActivate = currentTime.before(since); + mAlarm.set(mIsNextActivate ? since : till); + + // activate or disable according to current time + if (setActive) setAutoAODActive(currentTime.compareTo(since) >= 0 + && currentTime.before(till)); + } + + /** + * Activates or inactivates AOD + * @param active Whether to enable or disable AOD + */ + private void setAutoAODActive(boolean active) { + if (mOverrideOnce) { + Slog.v(TAG, "setAutoAODActive: user abandoned this session before, skipping"); + mOverrideOnce = false; + return; + } + mSharedPreferences.edit().remove(PREF_TIME_KEY).apply(); + + if (mActive == active) return; + mActive = active; + Slog.v(TAG, "setAutoAODActive: active=" + active); + mSelfChange = true; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.DOZE_ALWAYS_ON, active ? 1 : 0, + UserHandle.USER_CURRENT); + + // update the screen state + PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + if (powerManager.isInteractive()) return; // no need if the screen is already on + WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + wakeLock.acquire(WAKELOCK_TIMEOUT_MS); + if (isDozeEnabled()) { + // trigger doze if it's enabled + final Intent intent = new Intent(PULSE_ACTION); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } else { + // turn the screen on if we have to + powerManager.wakeUp(SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_APPLICATION, TAG); + } + } + + private boolean isDozeEnabled() { + return getAmbientConfig().pulseOnNotificationEnabled(UserHandle.USER_CURRENT); + } + + private AmbientDisplayConfiguration getAmbientConfig() { + if (mAmbientConfig == null) { + mAmbientConfig = new AmbientDisplayConfiguration(mContext); + } + + return mAmbientConfig; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 8132d7110edf..cfbf3b9b2c2b 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -173,6 +173,7 @@ import com.android.server.criticalevents.CriticalEventLog; import com.android.server.devicepolicy.DevicePolicyManagerService; import com.android.server.devicestate.DeviceStateManagerService; +import com.android.server.display.AutoAODService; import com.android.server.display.DisplayManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; @@ -2891,6 +2892,12 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { t.traceBegin("StartCustomDeviceConfigService"); mSystemServiceManager.startService(CustomDeviceConfigService.class); t.traceEnd(); + + if (context.getResources().getBoolean(R.bool.config_dozeAlwaysOnDisplayAvailable)) { + t.traceBegin("AutoAODService"); + mSystemServiceManager.startService(AutoAODService.class); + t.traceEnd(); + } } t.traceBegin("StartMediaProjectionManager"); From 352112af5c20bfed4c1d6e6babd545ffde8ec246 Mon Sep 17 00:00:00 2001 From: ezio84 Date: Sat, 2 Nov 2019 17:46:14 +0100 Subject: [PATCH 0505/1315] SystemUI: Allow to pulse on new tracks * @neobuddy89: Forward Port to Android 11/12/13/14/16 neobuddy89: * Remove now playing package usage. Rely on metadata changes. * Improve doze pulse broadcast usage. * Add minor fixes and NPE guards. * Use BG executor for smooth UX. Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../display/AmbientDisplayConfiguration.java | 6 ++++ core/java/android/provider/Settings.java | 6 ++++ packages/SystemUI/AndroidManifest.xml | 1 + .../keyguard/KeyguardSliceProvider.java | 35 +++++++++++++++++-- .../statusbar/phone/CentralSurfacesImpl.java | 28 ++++++++++++++- 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index 364ed990f760..6f5a9b6fc039 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -100,6 +100,7 @@ public boolean enabled(int user) { || pulseOnLongPressEnabled(user) || alwaysOnEnabled(user) || edgeLightEnabled(user) + || isAmbientTickerEnabled(user) || wakeLockScreenGestureEnabled(user) || wakeDisplayGestureEnabled(user) || pickupGestureEnabled(user) @@ -125,6 +126,11 @@ public boolean pulseOnNotificationAvailable() { && ambientDisplayAvailable(); } + /** @hide */ + public boolean isAmbientTickerEnabled(int user) { + return boolSettingDefaultOff(Settings.Secure.PULSE_ON_NEW_TRACKS, user); + } + /** @hide */ public boolean pickupGestureEnabled(int user) { return boolSetting(Settings.Secure.DOZE_PICK_UP_GESTURE, user, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 62cb02939e18..9fbe33f109d0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14143,6 +14143,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String DOZE_ON_CHARGE = "doze_on_charge"; + /** + * Whether to pulse ambient on new music tracks + * @hide + */ + public static final String PULSE_ON_NEW_TRACKS = "pulse_on_new_tracks"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 20e3cf95f198..5e7394a28b36 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -417,6 +417,7 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 216d2c47674a..800b1fd4a570 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -32,7 +32,9 @@ import android.media.session.PlaybackState; import android.net.Uri; import android.os.Handler; +import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.text.TextUtils; @@ -97,6 +99,7 @@ public class KeyguardSliceProvider extends SliceProvider implements "content://com.android.systemui.keyguard/media"; public static final String KEYGUARD_ACTION_URI = "content://com.android.systemui.keyguard/action"; + private static final String PULSE_ACTION = "com.android.systemui.doze.pulse"; /** * Only show alarms that will ring within N hours. @@ -107,6 +110,8 @@ public class KeyguardSliceProvider extends SliceProvider implements private static final Object sInstanceLock = new Object(); private static KeyguardSliceProvider sInstance; + private static final long MIN_PULSE_INTERVAL_MS = 3000; + protected final Uri mSliceUri; protected final Uri mHeaderUri; protected final Uri mDateUri; @@ -151,6 +156,9 @@ public class KeyguardSliceProvider extends SliceProvider implements protected boolean mDozing; private int mStatusBarState; private boolean mMediaIsVisible; + private boolean mPulseOnNewTracks; + private long mLastPulseUptimeMs = 0L; + private ContentProviderContextAvailableCallback mContextAvailableCallback; @Inject WakeLockLogger mWakeLockLogger; @@ -328,7 +336,9 @@ protected boolean isDndOn() { @Override public boolean onCreateSliceProvider() { - mContextAvailableCallback.onContextAvailable(getContext()); + if (mContextAvailableCallback != null) { + mContextAvailableCallback.onContextAvailable(getContext()); + } if (mMediaManager == null) { Log.e(TAG, "Dagger injection failed, cannot start. See any above warnings with string: " + "\"No injector for class\""); @@ -406,7 +416,7 @@ private boolean withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, i } long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours); - return mNextAlarmInfo.getTriggerTime() <= limit; + return alarmClockInfo.getTriggerTime() <= limit; } /** @@ -531,6 +541,27 @@ private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State mMediaArtist = artist; mMediaIsVisible = nextVisible; notifyChange(); + if (mPulseOnNewTracks && mMediaIsVisible + && !mDozeParameters.getAlwaysOn() && mDozing + && !TextUtils.isEmpty(mMediaTitle)) { + final long now = SystemClock.uptimeMillis(); + if (now - mLastPulseUptimeMs >= MIN_PULSE_INTERVAL_MS) { + mLastPulseUptimeMs = now; + mBgHandler.post(() -> { + try { + Intent intent = new Intent(PULSE_ACTION) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + getContext().sendBroadcastAsUser(intent, UserHandle.CURRENT); + } catch (Exception e) { + Log.e(TAG, "Error on sendBroadcastAsUser()", e); + } + }); + } + } + } + + public void setPulseOnNewTracks(boolean enabled) { + mPulseOnNewTracks = enabled; } protected void notifyChange() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 6e5e5527dd77..2629d3936025 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -140,6 +140,7 @@ import com.android.systemui.fragments.ExtensionFragmentListener; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; @@ -236,6 +237,7 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; import com.android.systemui.topui.TopUiController; +import com.android.systemui.tuner.TunerService; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.WallpaperController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -279,7 +281,11 @@ * {@link ActivityStarterImpl} */ @SysUISingleton -public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { +public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces, + TunerService.Tunable { + + private static final String PULSE_ON_NEW_TRACKS = + Settings.Secure.PULSE_ON_NEW_TRACKS; private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003; // 1020-1040 reserved for BaseStatusBar @@ -467,6 +473,7 @@ public QSPanelController getQSPanelController() { private final MessageRouter mMessageRouter; private final WallpaperManager mWallpaperManager; private final UserTracker mUserTracker; + private final TunerService mTunerService; private final ActivityStarter mActivityStarter; private final MediaViewController mMediaViewController; private final PulseViewController mPulseViewController; @@ -726,6 +733,7 @@ public CentralSurfacesImpl( LightRevealScrim lightRevealScrim, AlternateBouncerInteractor alternateBouncerInteractor, UserTracker userTracker, + TunerService tunerService, ActivityStarter activityStarter, BrightnessMirrorShowingRepository brightnessMirrorShowingRepository, GlanceableHubContainerController glanceableHubContainerController, @@ -828,6 +836,7 @@ public CentralSurfacesImpl( mCameraLauncherLazy = cameraLauncherLazy; mAlternateBouncerInteractor = alternateBouncerInteractor; mUserTracker = userTracker; + mTunerService = tunerService; mActivityStarter = activityStarter; mBrightnessMirrorShowingRepository = brightnessMirrorShowingRepository; if (!SceneContainerFlag.isEnabled()) { @@ -939,6 +948,8 @@ public void start() { createAndAddWindows(result); + mTunerService.addTunable(this, PULSE_ON_NEW_TRACKS); + // Set up the initial notification state. This needs to happen before CommandQueue.disable() setUpPresenter(); @@ -2923,6 +2934,21 @@ public boolean shouldIgnoreTouch() { || mScreenOffAnimationController.shouldIgnoreKeyguardTouches(); } + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case PULSE_ON_NEW_TRACKS: + boolean showPulseOnNewTracks = + TunerService.parseIntegerSwitch(newValue, false); + KeyguardSliceProvider sliceProvider = KeyguardSliceProvider.getAttachedInstance(); + if (sliceProvider != null) + sliceProvider.setPulseOnNewTracks(showPulseOnNewTracks); + break; + default: + break; + } + } + // Begin Extra BaseStatusBar methods. protected final CommandQueue mCommandQueue; From 790fca00a86aa85ae2ee264721176f6436f09dd6 Mon Sep 17 00:00:00 2001 From: jhenrique09 Date: Sat, 3 Oct 2020 14:44:59 +0000 Subject: [PATCH 0506/1315] fwb: Implement cutout force full screen [1/2] Inspired by MIUI and Essential CutoutFullscreenController: Adapted from https://github.com/LineageOS/android_lineage-sdk/blob/lineage-16.0/sdk/src/java/org/lineageos/internal/applications/LongScreen.java Change-Id: I37a6c0a29e7a5fbd9bded530f5de947cce5c7c25 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/IActivityManager.aidl | 5 + core/java/android/provider/Settings.java | 6 + .../android/internal/policy/PhoneWindow.java | 11 ++ .../cutout/CutoutFullscreenController.java | 118 ++++++++++++++++++ .../server/am/ActivityManagerService.java | 5 + .../com/android/server/wm/ActivityRecord.java | 4 + .../server/wm/ActivityTaskManagerService.java | 10 ++ .../server/wm/AppCompatAspectRatioPolicy.java | 2 + 8 files changed, 161 insertions(+) create mode 100644 core/java/com/android/internal/util/matrixx/cutout/CutoutFullscreenController.java diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 4cb2a15b81f7..8cfdc8ad041d 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1063,4 +1063,9 @@ interface IActivityManager { boolean isThreeFingersSwipeActive(); void setThreeFingersSwipeActive(boolean active); void setThreeGestureStateActive(boolean active); + + /** + * Force full screen for devices with cutout + */ + boolean shouldForceCutoutFullscreen(in String packageName); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 9fbe33f109d0..3a9ac80ad1f9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7391,6 +7391,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String CHARGING_ANIMATION = "charging_animation"; + /** + * Force full screen for devices with cutout + * @hide + */ + public static final String FORCE_FULLSCREEN_CUTOUT_APPS = "force_full_screen_cutout_apps"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 7cb7035f0735..92a01b06efb9 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -34,6 +34,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; @@ -2731,6 +2732,16 @@ protected ViewGroup generateLayout(DecorView decor) { params.layoutInDisplayCutoutMode = mode; } + if (ActivityManager.isSystemReady()) { + try { + String packageName = context.getBasePackageName(); + if (ActivityManager.getService().shouldForceCutoutFullscreen(packageName)){ + params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + } catch (RemoteException e) { + } + } + if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { if (a.getBoolean( diff --git a/core/java/com/android/internal/util/matrixx/cutout/CutoutFullscreenController.java b/core/java/com/android/internal/util/matrixx/cutout/CutoutFullscreenController.java new file mode 100644 index 000000000000..81ae41774b91 --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/cutout/CutoutFullscreenController.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2018 The LineageOS project + * Copyright (C) 2019 The PixelExperience project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util.matrixx.cutout; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import android.provider.Settings; + +public class CutoutFullscreenController { + private Set mApps = new HashSet<>(); + private Context mContext; + + private final boolean isAvailable; + + public CutoutFullscreenController(Context context) { + mContext = context; + final Resources resources = mContext.getResources(); + + final String displayCutout = resources.getString(com.android.internal.R.string.config_mainBuiltInDisplayCutout); + isAvailable = !TextUtils.isEmpty(displayCutout); + + if (!isAvailable) { + return; + } + + SettingsObserver observer = new SettingsObserver( + new Handler(Looper.getMainLooper())); + observer.observe(); + } + + public boolean isSupported() { + return isAvailable; + } + + public boolean shouldForceCutoutFullscreen(String packageName) { + return isSupported() && mApps.contains(packageName); + } + + public Set getApps() { + return mApps; + } + + public void addApp(String packageName) { + mApps.add(packageName); + Settings.System.putString(mContext.getContentResolver(), + Settings.System.FORCE_FULLSCREEN_CUTOUT_APPS, String.join(",", mApps)); + } + + public void removeApp(String packageName) { + mApps.remove(packageName); + Settings.System.putString(mContext.getContentResolver(), + Settings.System.FORCE_FULLSCREEN_CUTOUT_APPS, String.join(",", mApps)); + } + + public void setApps(Set apps) { + mApps = apps; + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + + resolver.registerContentObserver(Settings.System.getUriFor( + Settings.System.FORCE_FULLSCREEN_CUTOUT_APPS), false, this, + UserHandle.USER_ALL); + + update(); + } + + @Override + public void onChange(boolean selfChange) { + update(); + } + + public void update() { + ContentResolver resolver = mContext.getContentResolver(); + + String apps = Settings.System.getStringForUser(resolver, + Settings.System.FORCE_FULLSCREEN_CUTOUT_APPS, + UserHandle.USER_CURRENT); + if (apps != null) { + setApps(new HashSet<>(Arrays.asList(apps.split(",")))); + } else { + setApps(new HashSet<>()); + } + } + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 39c852c444f1..41502a4d4ebe 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19963,4 +19963,9 @@ public void setThreeFingersSwipeActive(boolean active) { public void setThreeGestureStateActive(boolean active) { mThreeFingerGestureActive = active; } + + @Override + public boolean shouldForceCutoutFullscreen(String packageName) { + return mActivityTaskManager.shouldForceCutoutFullscreen(packageName); + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 67474b85e172..139a5d939adb 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -9789,4 +9789,8 @@ ActivityRecord build() { public boolean shouldForceLongScreen() { return mAtmService.shouldForceLongScreen(packageName); } + + public boolean shouldForceCutoutFullscreen() { + return mAtmService.shouldForceCutoutFullscreen(packageName); + } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index bf0a08b5ae36..5a1974ac0cc3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -277,6 +277,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.matrixx.cutout.CutoutFullscreenController; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; @@ -830,6 +831,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private SystemSensorManager mSystemSensorManager; + private CutoutFullscreenController mCutoutFullscreenController; + private final class SettingObserver extends ContentObserver { private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE); private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS); @@ -929,6 +932,9 @@ public void installSystemProviders() { // Block sensor usage per app mSystemSensorManager = new SystemSensorManager(mContext, mContext.getMainLooper()); + + // Force full screen for devices with cutout + mCutoutFullscreenController = new CutoutFullscreenController(mContext); } public void retrieveSettings(ContentResolver resolver) { @@ -8176,4 +8182,8 @@ static boolean isPip2ExperimentEnabled() { public boolean shouldForceLongScreen(String packageName) { return mLineageActivityManager.shouldForceLongScreen(packageName); } + + public boolean shouldForceCutoutFullscreen(String packageName) { + return mCutoutFullscreenController.shouldForceCutoutFullscreen(packageName); + } } diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index 92e62033fc2a..cdfd9d1522e4 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -328,6 +328,8 @@ private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, if (containingRatio - aspectRatioToApply > ASPECT_RATIO_ROUNDING_TOLERANCE) { if (mActivityRecord.shouldForceLongScreen()) { // Use containingAppWidth/Height for maxActivityWidth/Height when force long screen + } else if (mActivityRecord.shouldForceCutoutFullscreen()) { + // Use containingAppWidth/Height for maxActivityWidth/Height when force cutout force full screen } else if (containingAppWidth < containingAppHeight) { // Width is the shorter side, so we use that to figure-out what the max. height // should be given the aspect ratio. From de737c8d14bc85cce39aa400cd0b7f724cee6c69 Mon Sep 17 00:00:00 2001 From: Joe Maples Date: Wed, 3 Jan 2018 21:53:01 -0500 Subject: [PATCH 0507/1315] SystemUI: Smart Pixels [1/2] Disables a percentage of pixels on screen to reduce power consumption. If enabled with battery saver, don't scale brightness at 0.5f for UX. Includes: - Option to enable on battery saver - User chosen grid - Burn-in protection Configurable via overlay and disabled by defualt: "config_supportSmartPixels" Squashed: From: Sergii Pylypenko Date: Sun, 8 Apr 2018 17:55:02 -0700 Subject: SystemUI: Screen-dimmer-pixel-filter Major credits to Sergii Pylypenko Change-Id: Ib2d7e18ad8fe2313dbf7593bf55a2cfec03ce567 Signed-off-by: Pranav Vashi From: Adin Kwok Date: Wed, 18 Apr 2018 01:05:27 -0700 Subject: Smart Pixels: Switch to registered receiver Switching to a registered receiver allows to properly handle updates on enabling of battery saver mode and switching of users. Also only update screen filter with burn-in protection when the device is in an interactive state. Test: Service starts after rebooting with it enabled Service starts on battery saver mode (user toggle) Service starts on battery saver mode (auto-enabled) Service re-adjusts on user switch to current user settings Filter updates after selected timeout Change-Id: Iced17fd5cc49e0163754bf75782f8465b54e859b Signed-off-by: Pranav Vashi From: Adin Kwok Date: Sat, 21 Apr 2018 01:46:50 -0700 Subject: Smart Pixels: Dynamically register receiver Don't keep the receiver registered if it isn't enabled. Change-Id: If6975df536598ee19d0ee17ec4150ae1b055e18c Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Sun, 26 Mar 2023 11:49:54 +0530 Subject: SmartPixels: Use CoreStartable interface for receiver * Also clean up and add check whether smart pixels is supported. Signed-off-by: Pranav Vashi From: Adin Kwok Date: Mon, 22 Oct 2018 13:00:13 -0700 Subject: Smart Pixels: Update default grid pattern Change-Id: I826a5a2fdc3aaa9c64f59fbe8b28c8757ca31c58 Signed-off-by: Pranav Vashi From: Anay Wadhera Date: Mon, 28 Feb 2022 17:03:54 -0800 Subject: SystemUI: mark smartpixels as a trusted overlay Change-Id: I1b5e17f5b4397e61350746b161d58366a19a1fc9 Signed-off-by: Pranav Vashi Change-Id: Id3c78548cb090ab2da11f543da31c5a408fb9fe9 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 24 ++ core/res/res/values/matrixx_config.xml | 3 + core/res/res/values/matrixx_symbols.xml | 3 + packages/SystemUI/AndroidManifest.xml | 5 + .../dagger/SystemUICoreStartableModule.kt | 7 + .../android/systemui/smartpixels/Grids.java | 148 +++++++++++ .../smartpixels/SmartPixelsReceiver.java | 168 ++++++++++++ .../smartpixels/SmartPixelsService.java | 244 ++++++++++++++++++ 8 files changed, 602 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/Grids.java create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsReceiver.java create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsService.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3a9ac80ad1f9..930002fff018 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7397,6 +7397,30 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String FORCE_FULLSCREEN_CUTOUT_APPS = "force_full_screen_cutout_apps"; + /** + * Whether to enable Smart Pixels + * @hide + */ + public static final String SMART_PIXELS_ENABLE = "smart_pixels_enable"; + + /** + * Smart Pixels pattern + * @hide + */ + public static final String SMART_PIXELS_PATTERN = "smart_pixels_pattern"; + + /** + * Smart Pixels Shift Timeout + * @hide + */ + public static final String SMART_PIXELS_SHIFT_TIMEOUT = "smart_pixels_shift_timeout"; + + /** + * Whether Smart Pixels should enable on power saver mode + * @hide + */ + public static final String SMART_PIXELS_ON_POWER_SAVE = "smart_pixels_on_power_save"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index 178075dedd90..9a2ccb14b9d5 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -95,4 +95,7 @@ false false false + + + false diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 295dfc904772..9e92ddb11e4f 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -81,4 +81,7 @@ + + + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 5e7394a28b36..104128f3df94 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -591,6 +591,11 @@ + + + Date: Thu, 15 Dec 2022 15:23:45 +0300 Subject: [PATCH 0508/1315] SystemUI: Allow devices to disable Smart Pixels on UDFPS With SmartPixels enabled, UDFPS does not work well. The higher the percentage of pixels to be disabled, the worse UDFPS works. Fix this by disabling SmartPixels when UDFPS is working. Change-Id: Ic478aa5d3a541d1ce533cfce7dacfd8ddec99ad0 Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/cr_config.xml | 3 + .../systemui/biometrics/UdfpsController.java | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index 9af2ad104f0d..68d4f134e2d5 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -14,4 +14,7 @@ 3000 6000 + + + false diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 8dac750462c6..19d23bfcfb63 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -244,6 +244,11 @@ public class UdfpsController implements DozeReceiver, Dumpable { private UdfpsAnimation mUdfpsAnimation; private boolean mKeyguardCallbackRegistered = false; + private boolean mDisableSmartPixels; + private boolean mSmartPixelsFlag; + private boolean mSmartPixelsEnabled; + private boolean mSmartPixelsOnPowerSave; + @VisibleForTesting public static final VibrationAttributes UDFPS_VIBRATION_ATTRIBUTES = new VibrationAttributes.Builder() @@ -265,6 +270,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override public void onScreenTurnedOn() { + if (mDisableSmartPixels) { + isSmartPixelsEnabled(); + } mScreenOn = true; if (mAodInterruptRunnable != null) { mAodInterruptRunnable.run(); @@ -874,6 +882,8 @@ public UdfpsController(@NonNull @Main Context context, mWakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver); } + mDisableSmartPixels = mContext.getResources().getBoolean(com.android.systemui.res.R.bool.config_disableSmartPixelsOnUDFPS); + if (com.android.internal.util.matrixx.Utils.isPackageInstalled(mContext, "com.matrixx.udfps.animations")) { updateUdfpsAnimation(); @@ -892,6 +902,47 @@ private void updateUdfpsAnimation() { } } + private void isSmartPixelsEnabled() { + if (!mSmartPixelsFlag) { + mSmartPixelsEnabled = Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, + 0, mContext.getUserId()) != 0; + Log.i(TAG, "SmartPixels: SmartPixels enabled - " + mSmartPixelsEnabled); + mSmartPixelsOnPowerSave = Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 0, mContext.getUserId()) != 0; + Log.i(TAG, "SmartPixels: SmartPixels on Power Save enabled - " + mSmartPixelsOnPowerSave); + } + } + + private void disableSmartPixels() { + Log.i(TAG, "SmartPixels: Disable SmartPixels"); + if (mSmartPixelsEnabled) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ENABLE, + 0, mContext.getUserId()); + } + if (mSmartPixelsOnPowerSave) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 0, mContext.getUserId()); + } + } + + private void enableSmartPixels() { + Log.i(TAG, "SmartPixels: Enable SmartPixels"); + if (mSmartPixelsEnabled) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ENABLE, + 1, mContext.getUserId()); + } + if (mSmartPixelsOnPowerSave) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 1, mContext.getUserId()); + } + } + /** * If a11y touchExplorationEnabled, play haptic to signal UDFPS scanning started. */ @@ -1191,6 +1242,12 @@ private void onFingerDown( + " current: " + mOverlay.getRequestId()); return; } + if (mDisableSmartPixels) { + if (!mSmartPixelsFlag && (mSmartPixelsEnabled || mSmartPixelsOnPowerSave)) { + disableSmartPixels(); + } + mSmartPixelsFlag = true; + } if (isOptical()) { mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE); } @@ -1260,6 +1317,14 @@ private void onFingerUp( mExecution.assertIsMainThread(); mActivePointerId = MotionEvent.INVALID_POINTER_ID; mAcquiredReceived = false; + + if (mDisableSmartPixels) { + if (mSmartPixelsFlag && (mSmartPixelsEnabled || mSmartPixelsOnPowerSave)) { + enableSmartPixels(); + } + mSmartPixelsFlag = false; + } + if (mOnFingerDown) { mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x, y, minor, major, orientation, time, gestureStart, isAod); From 3fb1b4b0e00a289af8e0e7c5d0d0e95c6496c4a2 Mon Sep 17 00:00:00 2001 From: Kshitij Gupta Date: Tue, 4 Jan 2022 16:10:15 +0530 Subject: [PATCH 0509/1315] fwb: Screen off animations [1/2] Fade (default), CRT, and Scale Credit and respect to xplodwild for paving the way back in the KitKat days! History of the ElectronBeam class can be found here https://github.com/DirtyUnicorns/android_frameworks_base/commits/kitkat/services/java/com/android/server/power/ElectronBeam.java *** Changes for Android 10 by bigrushdog *** Various updates needed for Surface related API in the ElectronBeam animation class [AgentFabulous | POSP] - Rebase and rewrite ElectronBeam class on top of ColorFade - Add usage of class-common Transaction instance - Add support for respecting wide-color and protected-content - Fixup for new API @neobuddy89: Updated for A14, A15, A16. Change-Id: I58d269d44c901a8c0471807e3cf05c2054205d28 Co-Authored-By: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + .../com/android/server/display/ColorFade.java | 2 +- .../display/DisplayPowerController.java | 61 +- .../server/display/DisplayPowerState.java | 22 +- .../android/server/display/ElectronBeam.java | 947 ++++++++++++++++++ .../server/display/ScreenStateAnimator.java | 42 + 6 files changed, 1066 insertions(+), 14 deletions(-) create mode 100644 services/core/java/com/android/server/display/ElectronBeam.java create mode 100644 services/core/java/com/android/server/display/ScreenStateAnimator.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 930002fff018..2b1e70e565fa 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7421,6 +7421,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SMART_PIXELS_ON_POWER_SAVE = "smart_pixels_on_power_save"; + /** + * Defines the screen-off animation to display + * @hide + */ + public static final String SCREEN_OFF_ANIMATION = "screen_off_animation"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java index 49e18b75276e..cf14d5ddfef4 100644 --- a/services/core/java/com/android/server/display/ColorFade.java +++ b/services/core/java/com/android/server/display/ColorFade.java @@ -64,7 +64,7 @@ * that belongs to the {@link DisplayPowerController}. *

*/ -final class ColorFade { +final class ColorFade implements ScreenStateAnimator { private static final String TAG = "ColorFade"; // To enable these logs, run: diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index e1f13b9f7417..4662bc4c14fb 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -32,6 +32,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.ParceledListSlice; import android.content.res.Resources; @@ -495,6 +496,12 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private ObjectAnimator mColorFadeOffAnimator; private DualRampAnimator mScreenBrightnessRampAnimator; + // Screen-off animation + private int mScreenOffAnimation; + static final int SCREEN_OFF_FADE = 0; + static final int SCREEN_OFF_CRT = 1; + static final int SCREEN_OFF_SCALE = 2; + // True if this DisplayPowerController has been stopped and should no longer be running. private boolean mStopped; @@ -1018,9 +1025,48 @@ private void sendUpdatePowerStateLocked() { } } + private int getScreenAnimationModeForDisplayState(int displayState) { + switch (mScreenOffAnimation) { + case SCREEN_OFF_FADE: + return ScreenStateAnimator.MODE_FADE; + case SCREEN_OFF_CRT: + if (displayState == Display.STATE_OFF) { + return ScreenStateAnimator.MODE_COOL_DOWN; + } else { + return ScreenStateAnimator.MODE_FADE; + } + case SCREEN_OFF_SCALE: + if (displayState == Display.STATE_OFF) { + return ScreenStateAnimator.MODE_SCALE_DOWN; + } else { + return ScreenStateAnimator.MODE_FADE; + } + default: + return ScreenStateAnimator.MODE_FADE; + } + } + private void initialize(int displayState) { - mPowerState = mInjector.getDisplayPowerState(mBlanker, - mColorFadeEnabled ? new ColorFade(mDisplayId) : null, mDisplayId, displayState); + final ContentResolver cr = mContext.getContentResolver(); + final ContentObserver observer = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + mScreenOffAnimation = Settings.System.getIntForUser(cr, + Settings.System.SCREEN_OFF_ANIMATION, + SCREEN_OFF_FADE, UserHandle.USER_CURRENT); + if (mPowerState != null) { + mPowerState.setScreenStateAnimator(mScreenOffAnimation); + } + } + }; + cr.registerContentObserver(Settings.System.getUriFor( + Settings.System.SCREEN_OFF_ANIMATION), + false, observer, UserHandle.USER_ALL); + mScreenOffAnimation = Settings.System.getIntForUser(cr, + Settings.System.SCREEN_OFF_ANIMATION, + SCREEN_OFF_FADE, UserHandle.USER_CURRENT); + + mPowerState = mInjector.getDisplayPowerState(mBlanker, mScreenOffAnimation, mDisplayId, displayState); if (mColorFadeEnabled) { mColorFadeOffAnimator = ObjectAnimator.ofFloat( @@ -2376,7 +2422,7 @@ private void animateScreenStateChange( // ensure ColorFade is present. // TODO(b/428688446): ColorFade.MODE_FADE should be enough here. mPowerState.prepareColorFade(mContext, - mColorFadeFadesConfig ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP); + getScreenAnimationModeForDisplayState(Display.STATE_ON)); } if (mDisplayBlanksAfterDozeConfig @@ -2386,7 +2432,7 @@ private void animateScreenStateChange( // contents of the screen. // TODO(b/428688446): ColorFade.MODE_FADE should be enough here. mPowerState.prepareColorFade(mContext, - mColorFadeFadesConfig ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP); + getScreenAnimationModeForDisplayState(Display.STATE_ON)); if (mColorFadeOffAnimator != null) { mColorFadeOffAnimator.end(); } @@ -2491,8 +2537,7 @@ private void animateScreenStateChange( mPowerState.dismissColorFadeResources(); } else if (performScreenOffTransition && mPowerState.prepareColorFade(mContext, - mColorFadeFadesConfig - ? ColorFade.MODE_FADE : ColorFade.MODE_COOL_DOWN) + getScreenAnimationModeForDisplayState(Display.STATE_OFF)) && mPowerState.getScreenState() != Display.STATE_OFF) { // Perform the screen off animation. mColorFadeOffAnimator.start(); @@ -3387,9 +3432,9 @@ Clock getClock() { return SystemClock::uptimeMillis; } - DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade, + DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, int screenOffAnimation, int displayId, int displayState) { - return new DisplayPowerState(blanker, colorFade, displayId, displayState); + return new DisplayPowerState(blanker, screenOffAnimation, displayId, displayState); } DualRampAnimator getDualRampAnimator(DisplayPowerState dps, diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index 2fbb114c9a63..26d8492fb8a7 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -61,7 +61,6 @@ final class DisplayPowerState { private final Handler mHandler; private final Choreographer mChoreographer; private final DisplayBlanker mBlanker; - private final ColorFade mColorFade; private final PhotonicModulator mPhotonicModulator; private final int mDisplayId; @@ -82,19 +81,21 @@ final class DisplayPowerState { private volatile boolean mStopped; + private ScreenStateAnimator mColorFade; + DisplayPowerState( - DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState) { - this(blanker, colorFade, displayId, displayState, BackgroundThread.getExecutor()); + DisplayBlanker blanker, int screenAnimatorMode, int displayId, int displayState) { + this(blanker, screenAnimatorMode, displayId, displayState, BackgroundThread.getExecutor()); } @VisibleForTesting DisplayPowerState( - DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState, + DisplayBlanker blanker, int screenAnimatorMode, int displayId, int displayState, Executor asyncDestroyExecutor) { mHandler = new Handler(true /*async*/); mChoreographer = Choreographer.getInstance(); mBlanker = blanker; - mColorFade = colorFade; + setScreenStateAnimator(screenAnimatorMode); mPhotonicModulator = new PhotonicModulator(); mPhotonicModulator.start(); mDisplayId = displayId; @@ -116,6 +117,17 @@ final class DisplayPowerState { mColorFadeReady = true; } + public void setScreenStateAnimator(int mode) { + if (mColorFade != null) { + mColorFade.dismiss(); + } + if (mode == DisplayPowerController.SCREEN_OFF_FADE) { + mColorFade = new ColorFade(Display.DEFAULT_DISPLAY); + } else { + mColorFade = new ElectronBeam(Display.DEFAULT_DISPLAY); + } + } + public static final FloatProperty COLOR_FADE_LEVEL = new FloatProperty("electronBeamLevel") { @Override diff --git a/services/core/java/com/android/server/display/ElectronBeam.java b/services/core/java/com/android/server/display/ElectronBeam.java new file mode 100644 index 000000000000..6b28fa571ecf --- /dev/null +++ b/services/core/java/com/android/server/display/ElectronBeam.java @@ -0,0 +1,947 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2018 The Dirty Unicorns Project + * Copyright (C) 2022 The Potato Open Sauce Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import static com.android.internal.policy.TransitionAnimation.hasProtectedContent; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +import android.hardware.display.DisplayManagerInternal; +import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.opengl.GLES10; +import android.opengl.GLES11Ext; +import android.util.Slog; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.Surface; +import android.view.Surface.OutOfResourcesException; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.SurfaceSession; +import android.window.ScreenCaptureInternal; + +import com.android.server.LocalServices; +import com.android.server.policy.WindowManagerPolicy; + +import java.io.PrintWriter; +import java.lang.Math; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + + +/** + *

+ * Animates a screen transition from on to off or off to on by applying + * some GL transformations to a screenshot. + *

+ * This component must only be created or accessed by the {@link Looper} thread + * that belongs to the {@link DisplayPowerController}. + *

+ */ +final class ElectronBeam implements ScreenStateAnimator { + private static final String TAG = "ElectronBeam"; + + private static final boolean DEBUG = false; + + // The layer for the electron beam surface. + // This is currently hardcoded to be one layer above the boot animation. + private static final int ELECTRON_BEAM_LAYER = WindowManagerPolicy.COLOR_FADE_LAYER; + + // The relative proportion of the animation to spend performing + // the horizontal stretch effect. The remainder is spent performing + // the vertical stretch effect. + private static final float HSTRETCH_DURATION = 0.5f; + private static final float VSTRETCH_DURATION = 1.0f - HSTRETCH_DURATION; + + // The number of frames to draw when preparing the animation so that it will + // be ready to run smoothly. We use 3 frames because we are triple-buffered. + // See code for details. + private static final int DEJANK_FRAMES = 3; + + private static final int EGL_GL_COLORSPACE_KHR = 0x309D; + private static final int EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT = 0x3490; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + private final int mDisplayId; + + // Set to true when the animation context has been fully prepared. + private boolean mPrepared; + private boolean mCreatedResources; + private int mMode; + + private final DisplayManagerInternal mDisplayManagerInternal; + private int mDisplayLayerStack; // layer stack associated with primary display + private int mDisplayWidth; // real width, not rotated + private int mDisplayHeight; // real height, not rotated + private SurfaceSession mSurfaceSession; + private SurfaceControl mSurfaceControl; + private Surface mSurface; + private NaturalSurfaceLayout mSurfaceLayout; + private EGLDisplay mEglDisplay; + private EGLConfig mEglConfig; + private EGLContext mEglContext; + private EGLSurface mEglSurface; + private boolean mSurfaceVisible; + private float mSurfaceAlpha; + private boolean mLastWasWideColor; + private boolean mLastWasProtectedContent; + + // Texture names. We only use one texture, which contains the screenshot. + private final int[] mTexNames = new int[1]; + private boolean mTexNamesGenerated; + private final float mTexMatrix[] = new float[16]; + + // Vertex and corresponding texture coordinates. + // We have 4 2D vertices, so 8 elements. The vertices form a quad. + private final FloatBuffer mVertexBuffer = createNativeFloatBuffer(8); + private final FloatBuffer mTexCoordBuffer = createNativeFloatBuffer(8); + + private final Transaction mTransaction = new Transaction(); + + /** + * Animates an electron beam warming up. + */ + public static final int MODE_WARM_UP = 0; + + /** + * Animates an electron beam shutting off. + */ + public static final int MODE_COOL_DOWN = 1; + + /** + * Animates a simple dim layer to fade the contents of the screen in or out progressively. + */ + public static final int MODE_FADE = 2; + + /** + * Animates a scale down of the screen + */ + public static final int MODE_SCALE_DOWN = 3; + + + public ElectronBeam(int displayId) { + mDisplayId = displayId; + mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); + } + + /** + * Warms up the electron beam in preparation for turning on or off. + * This method prepares a GL context, and captures a screen shot. + * + * @param mode The desired mode for the upcoming animation. + * @return True if the electron beam is ready, false if it is uncontrollable. + */ + public boolean prepare(Context context, int mode) { + if (DEBUG) { + Slog.d(TAG, "prepare: mode=" + mode); + } + + mMode = mode; + + // Get the display size and layer stack. + // This is not expected to change while the electron beam surface is showing. + DisplayInfo displayInfo = mDisplayManagerInternal.getDisplayInfo(mDisplayId); + mDisplayLayerStack = displayInfo.layerStack; + mDisplayWidth = displayInfo.getNaturalWidth(); + mDisplayHeight = displayInfo.getNaturalHeight(); + + final boolean isWideColor = displayInfo.colorMode == Display.COLOR_MODE_DISPLAY_P3; + // Set mPrepared here so if initialization fails, resources can be cleaned up. + mPrepared = true; + + final ScreenCaptureInternal.ScreenshotHardwareBuffer hardwareBuffer = captureScreen(); + if (hardwareBuffer == null) { + dismiss(); + return false; + } + + final boolean isProtected = hasProtectedContent(hardwareBuffer.getHardwareBuffer()); + if (!createSurfaceControl(hardwareBuffer.containsSecureLayers())) { + dismiss(); + return false; + } + + // MODE_FADE use ColorLayer to implement. + if (mMode == MODE_FADE) { + return true; + } + + if (!(createEglContext(isProtected) && createEglSurface(isProtected, isWideColor) + && setScreenshotTextureAndSetViewport(hardwareBuffer, displayInfo.rotation))) { + dismiss(); + return false; + } + + // Done. + mCreatedResources = true; + mLastWasProtectedContent = isProtected; + mLastWasWideColor = isWideColor; + + // Dejanking optimization. + // Some GL drivers can introduce a lot of lag in the first few frames as they + // initialize their state and allocate graphics buffers for rendering. + // Work around this problem by rendering the first frame of the animation a few + // times. The rest of the animation should run smoothly thereafter. + // The frames we draw here aren't visible because we are essentially just + // painting the screenshot as-is. + if (mode == MODE_COOL_DOWN || mode == MODE_SCALE_DOWN) { + for (int i = 0; i < DEJANK_FRAMES; i++) { + draw(1.0f); + } + } + return true; + } + + /** + * Dismisses the electron beam animation resources. + * + * This function destroys the resources that are created for the electron beam + * animation but does not clean up the surface. + */ + public void dismissResources() { + if (DEBUG) { + Slog.d(TAG, "dismissResources"); + } + + if (mCreatedResources) { + attachEglContext(); + try { + destroyScreenshotTexture(); + destroyEglSurface(); + } finally { + detachEglContext(); + } + // This is being called with no active context so shouldn't be + // needed but is safer to not change for now. + mCreatedResources = false; + } + } + + /** + * Dismisses the electron beam animation surface and cleans up. + * + * To prevent stray photons from leaking out after the electron beam has been + * turned off, it is a good idea to defer dismissing the animation until the + * electron beam has been turned back on fully. + */ + public void dismiss() { + if (DEBUG) { + Slog.d(TAG, "dismiss"); + } + + if (mPrepared) { + dismissResources(); + destroySurface(); + mPrepared = false; + } + } + + /** + * Destroys electron beam animation and its resources + * + * This method should be called when the electron beam is no longer in use; i.e. when + * the {@link #mDisplayId display} has been removed. + */ + public void destroy() { + if (DEBUG) { + Slog.d(TAG, "destroy"); + } + if (mPrepared) { + if (mCreatedResources) { + attachEglContext(); + try { + destroyScreenshotTexture(); + destroyEglSurface(); + } finally { + detachEglContext(); + } + } + destroyEglContext(); + destroySurface(); + } + } + + /** + * Draws an animation frame showing the electron beam activated at the + * specified level. + * + * @param level The electron beam level. + * @return True if successful. + */ + public boolean draw(float level) { + if (DEBUG) { + Slog.d(TAG, "drawFrame: level=" + level); + } + + if (!mPrepared) { + return false; + } + + if (mMode == MODE_FADE) { + return showSurface(1.0f - level); + } + + if (!attachEglContext()) { + return false; + } + try { + // Clear frame to solid black. + GLES10.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); + GLES10.glClear(GLES10.GL_COLOR_BUFFER_BIT); + + // Draw the frame. + if (mMode == MODE_WARM_UP || mMode == MODE_COOL_DOWN) { + if (level < HSTRETCH_DURATION) { + drawHStretch(1.0f - (level / HSTRETCH_DURATION)); + } else { + drawVStretch(1.0f - ((level - HSTRETCH_DURATION) / VSTRETCH_DURATION)); + } + } else if (mMode == MODE_SCALE_DOWN) { + drawScaled(level); + } + + if (checkGlErrors("drawFrame")) { + return false; + } + + EGL14.eglSwapBuffers(mEglDisplay, mEglSurface); + } finally { + detachEglContext(); + } + + return showSurface(1.0f); + } + + private void drawScaled(float scale) { + final float curvedScale = scurve(scale, 8.0f); + + // set blending, enable alpha operations + GLES10.glEnable(GLES10.GL_BLEND); + GLES10.glBlendFunc(GLES10.GL_SRC_ALPHA, GLES10.GL_ONE_MINUS_SRC_ALPHA); + + // bind vertex buffer + GLES10.glVertexPointer(2, GLES10.GL_FLOAT, 0, mVertexBuffer); + GLES10.glEnableClientState(GLES10.GL_VERTEX_ARRAY); + + // set-up texturing + GLES10.glDisable(GLES10.GL_TEXTURE_2D); + GLES10.glEnable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + + // bind texture and set blending for drawing planes + GLES10.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTexNames[0]); + GLES10.glTexEnvx(GLES10.GL_TEXTURE_ENV, GLES10.GL_TEXTURE_ENV_MODE, + mMode == MODE_WARM_UP ? GLES10.GL_MODULATE : GLES10.GL_REPLACE); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_MAG_FILTER, GLES10.GL_LINEAR); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_MIN_FILTER, GLES10.GL_LINEAR); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_WRAP_S, GLES10.GL_CLAMP_TO_EDGE); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_WRAP_T, GLES10.GL_CLAMP_TO_EDGE); + GLES10.glEnable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + GLES10.glTexCoordPointer(2, GLES10.GL_FLOAT, 0, mTexCoordBuffer); + GLES10.glEnableClientState(GLES10.GL_TEXTURE_COORD_ARRAY); + + // Draw the frame + setQuad(mVertexBuffer, mDisplayWidth / 2 * (1.0f - curvedScale), + mDisplayHeight / 2 * (1.0f - curvedScale), + mDisplayWidth * curvedScale, mDisplayHeight * curvedScale); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // dim progressively, using previous vertexes + GLES10.glDisable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + GLES10.glDisableClientState(GLES10.GL_TEXTURE_COORD_ARRAY); + GLES10.glColorMask(true, true, true, true); + GLES10.glColor4f(0.0f, 0.0f, 0.0f, 1.0f - curvedScale); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // clean up after drawing planes + GLES10.glDisableClientState(GLES10.GL_VERTEX_ARRAY); + GLES10.glDisable(GLES10.GL_BLEND); + } + + /** + * Draws a frame where the content of the electron beam is collapsing inwards upon + * itself vertically with red / green / blue channels dispersing and eventually + * merging down to a single horizontal line. + * + * @param stretch The stretch factor. 0.0 is no collapse, 1.0 is full collapse. + */ + private void drawVStretch(float stretch) { + // compute interpolation scale factors for each color channel + final float ar = scurve(stretch, 7.5f); + final float ag = scurve(stretch, 8.0f); + final float ab = scurve(stretch, 8.5f); + if (DEBUG) { + Slog.d(TAG, "drawVStretch: stretch=" + stretch + + ", ar=" + ar + ", ag=" + ag + ", ab=" + ab); + } + + // set blending + GLES10.glBlendFunc(GLES10.GL_ONE, GLES10.GL_ONE); + GLES10.glEnable(GLES10.GL_BLEND); + + // bind vertex buffer + GLES10.glVertexPointer(2, GLES10.GL_FLOAT, 0, mVertexBuffer); + GLES10.glEnableClientState(GLES10.GL_VERTEX_ARRAY); + + // set-up texturing + GLES10.glDisable(GLES10.GL_TEXTURE_2D); + GLES10.glEnable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + + // bind texture and set blending for drawing planes + GLES10.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTexNames[0]); + GLES10.glTexEnvx(GLES10.GL_TEXTURE_ENV, GLES10.GL_TEXTURE_ENV_MODE, + mMode == MODE_WARM_UP ? GLES10.GL_MODULATE : GLES10.GL_REPLACE); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_MAG_FILTER, GLES10.GL_LINEAR); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_MIN_FILTER, GLES10.GL_LINEAR); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_WRAP_S, GLES10.GL_CLAMP_TO_EDGE); + GLES10.glTexParameterx(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES10.GL_TEXTURE_WRAP_T, GLES10.GL_CLAMP_TO_EDGE); + GLES10.glEnable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + GLES10.glTexCoordPointer(2, GLES10.GL_FLOAT, 0, mTexCoordBuffer); + GLES10.glEnableClientState(GLES10.GL_TEXTURE_COORD_ARRAY); + + // draw the red plane + setVStretchQuad(mVertexBuffer, mDisplayWidth, mDisplayHeight, ar); + GLES10.glColorMask(true, false, false, true); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // draw the green plane + setVStretchQuad(mVertexBuffer, mDisplayWidth, mDisplayHeight, ag); + GLES10.glColorMask(false, true, false, true); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // draw the blue plane + setVStretchQuad(mVertexBuffer, mDisplayWidth, mDisplayHeight, ab); + GLES10.glColorMask(false, false, true, true); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // clean up after drawing planes + GLES10.glDisable(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + GLES10.glDisableClientState(GLES10.GL_TEXTURE_COORD_ARRAY); + GLES10.glColorMask(true, true, true, true); + + // draw the white highlight (we use the last vertices) + if (mMode == MODE_COOL_DOWN) { + GLES10.glColor4f(ag, ag, ag, 1.0f); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + } + + // clean up + GLES10.glDisableClientState(GLES10.GL_VERTEX_ARRAY); + GLES10.glDisable(GLES10.GL_BLEND); + } + + /** + * Draws a frame where the electron beam has been stretched out into + * a thin white horizontal line that fades as it collapses inwards. + * + * @param stretch The stretch factor. 0.0 is maximum stretch / no fade, + * 1.0 is collapsed / maximum fade. + */ + private void drawHStretch(float stretch) { + // compute interpolation scale factor + final float ag = scurve(stretch, 8.0f); + if (DEBUG) { + Slog.d(TAG, "drawHStretch: stretch=" + stretch + ", ag=" + ag); + } + + if (stretch < 1.0f) { + // bind vertex buffer + GLES10.glVertexPointer(2, GLES10.GL_FLOAT, 0, mVertexBuffer); + GLES10.glEnableClientState(GLES10.GL_VERTEX_ARRAY); + + // draw narrow fading white line + setHStretchQuad(mVertexBuffer, mDisplayWidth, mDisplayHeight, ag); + GLES10.glColor4f(1.0f - ag*0.75f, 1.0f - ag*0.75f, 1.0f - ag*0.75f, 1.0f); + GLES10.glDrawArrays(GLES10.GL_TRIANGLE_FAN, 0, 4); + + // clean up + GLES10.glDisableClientState(GLES10.GL_VERTEX_ARRAY); + } + } + + private static void setVStretchQuad(FloatBuffer vtx, float dw, float dh, float a) { + final float w = dw + (dw * a); + final float h = dh - (dh * a); + final float x = (dw - w) * 0.5f; + final float y = (dh - h) * 0.5f; + setQuad(vtx, x, y, w, h); + } + + private static void setHStretchQuad(FloatBuffer vtx, float dw, float dh, float a) { + final float w = 2 * dw * (1.0f - a); + final float h = 1.0f; + final float x = (dw - w) * 0.5f; + final float y = (dh - h) * 0.5f; + setQuad(vtx, x, y, w, h); + } + + private static void setQuad(FloatBuffer vtx, float x, float y, float w, float h) { + if (DEBUG) { + Slog.d(TAG, "setQuad: x=" + x + ", y=" + y + ", w=" + w + ", h=" + h); + } + vtx.put(0, x); + vtx.put(1, y); + vtx.put(2, x); + vtx.put(3, y + h); + vtx.put(4, x + w); + vtx.put(5, y + h); + vtx.put(6, x + w); + vtx.put(7, y); + } + + private boolean setScreenshotTextureAndSetViewport( + ScreenCaptureInternal.ScreenshotHardwareBuffer screenshotBuffer, + @Surface.Rotation int rotation) { + if (!attachEglContext()) { + return false; + } + try { + if (!mTexNamesGenerated) { + GLES10.glGenTextures(1, mTexNames, 0); + if (checkGlErrors("glGenTextures")) { + return false; + } + mTexNamesGenerated = true; + } + + final SurfaceTexture st = new SurfaceTexture(mTexNames[0]); + final Surface s = new Surface(st); + try { + s.attachAndQueueBufferWithColorSpace(screenshotBuffer.getHardwareBuffer(), + screenshotBuffer.getColorSpace()); + + st.updateTexImage(); + st.getTransformMatrix(mTexMatrix); + } finally { + s.release(); + st.release(); + } + + // if screen is rotated, map texture starting different corner + int indexDelta = (rotation == Surface.ROTATION_90) ? 2 + : (rotation == Surface.ROTATION_180) ? 4 + : (rotation == Surface.ROTATION_270) ? 6 : 0; + + // Set up texture coordinates for a quad. + // We might need to change this if the texture ends up being + // a different size from the display for some reason. + mTexCoordBuffer.put(indexDelta, 0f); + mTexCoordBuffer.put(indexDelta + 1, 0f); + mTexCoordBuffer.put((indexDelta + 2) % 8, 0f); + mTexCoordBuffer.put((indexDelta + 3) % 8, 1f); + mTexCoordBuffer.put((indexDelta + 4) % 8, 1f); + mTexCoordBuffer.put((indexDelta + 5) % 8, 1f); + mTexCoordBuffer.put((indexDelta + 6) % 8, 1f); + mTexCoordBuffer.put((indexDelta + 7) % 8, 0f); + + // Set up our viewport. + GLES10.glViewport(0, 0, mDisplayWidth, mDisplayHeight); + GLES10.glMatrixMode(GLES10.GL_PROJECTION); + GLES10.glLoadIdentity(); + GLES10.glOrthof(0, mDisplayWidth, 0, mDisplayHeight, 0, 1); + GLES10.glMatrixMode(GLES10.GL_MODELVIEW); + GLES10.glLoadIdentity(); + GLES10.glMatrixMode(GLES10.GL_TEXTURE); + GLES10.glLoadIdentity(); + GLES10.glLoadMatrixf(mTexMatrix, 0); + } finally { + detachEglContext(); + } + return true; + } + + private void destroyScreenshotTexture() { + if (mTexNamesGenerated) { + mTexNamesGenerated = false; + if (attachEglContext()) { + try { + GLES10.glDeleteTextures(1, mTexNames, 0); + checkGlErrors("glDeleteTextures"); + } finally { + detachEglContext(); + } + } + } + } + + private ScreenCaptureInternal.ScreenshotHardwareBuffer captureScreen() { + ScreenCaptureInternal.ScreenshotHardwareBuffer screenshotBuffer = + mDisplayManagerInternal.systemScreenshot(mDisplayId); + if (screenshotBuffer == null) { + Slog.e(TAG, "Failed to take screenshot. Buffer is null"); + return null; + } + return screenshotBuffer; + } + + private boolean createSurfaceControl(boolean isSecure) { + if (mSurfaceControl != null) { + mTransaction.setSecure(mSurfaceControl, isSecure).apply(); + return true; + } + + if (mSurfaceSession == null) { + mSurfaceSession = new SurfaceSession(); + } + + if (mSurfaceControl == null) { + try { + int flags; + if (mMode == MODE_FADE) { + flags = SurfaceControl.FX_SURFACE_EFFECT | SurfaceControl.HIDDEN; + } else { + flags = SurfaceControl.OPAQUE | SurfaceControl.HIDDEN; + } + SurfaceControl.Builder builder = new SurfaceControl.Builder(mSurfaceSession); + builder.setFlags(flags) + .setFormat(PixelFormat.OPAQUE) + .setName("ElectronBeam") + .setBufferSize(mDisplayWidth, mDisplayHeight); + mSurfaceControl = builder.build(); + } catch (OutOfResourcesException ex) { + Slog.e(TAG, "Unable to create surface.", ex); + return false; + } + + mTransaction.setLayerStack(mSurfaceControl, mDisplayLayerStack); + mTransaction.setWindowCrop(mSurfaceControl, mDisplayWidth, mDisplayHeight); + mSurface = new Surface(); + mSurface.copyFrom(mSurfaceControl); + + mSurfaceLayout = new NaturalSurfaceLayout(mDisplayManagerInternal, + mDisplayId, mSurfaceControl); + mSurfaceLayout.onDisplayTransaction(mTransaction); + mTransaction.apply(); + } + return true; + } + + private boolean createEglContext(boolean isProtected) { + if (mEglDisplay == null) { + mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEglDisplay == EGL14.EGL_NO_DISPLAY) { + logEglError("eglGetDisplay"); + return false; + } + + int[] version = new int[2]; + if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) { + mEglDisplay = null; + logEglError("eglInitialize"); + return false; + } + } + + if (mEglConfig == null) { + int[] eglConfigAttribList = new int[] { + EGL14.EGL_RENDERABLE_TYPE, + EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_NONE + }; + int[] numEglConfigs = new int[1]; + EGLConfig[] eglConfigs = new EGLConfig[1]; + if (!EGL14.eglChooseConfig(mEglDisplay, eglConfigAttribList, 0, + eglConfigs, 0, eglConfigs.length, numEglConfigs, 0)) { + logEglError("eglChooseConfig"); + return false; + } + if (numEglConfigs[0] <= 0) { + Slog.e(TAG, "no valid config found"); + return false; + } + + mEglConfig = eglConfigs[0]; + } + + // The old context needs to be destroyed if the protected flag has changed. The context will + // be recreated based on the protected flag + if (mEglContext != null && isProtected != mLastWasProtectedContent) { + EGL14.eglDestroyContext(mEglDisplay, mEglContext); + mEglContext = null; + } + + if (mEglContext == null) { + int[] eglContextAttribList = new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 1, + EGL14.EGL_NONE, EGL14.EGL_NONE, + EGL14.EGL_NONE + }; + if (isProtected) { + eglContextAttribList[2] = EGL_PROTECTED_CONTENT_EXT; + eglContextAttribList[3] = EGL14.EGL_TRUE; + } + mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT, + eglContextAttribList, 0); + if (mEglContext == null) { + logEglError("eglCreateContext"); + return false; + } + } + return true; + } + + private boolean createEglSurface(boolean isProtected, boolean isWideColor) { + // The old surface needs to be destroyed if either the protected flag or wide color flag has + // changed. The surface will be recreated based on the new flags. + boolean didContentAttributesChange = + isProtected != mLastWasProtectedContent || isWideColor != mLastWasWideColor; + if (mEglSurface != null && didContentAttributesChange) { + EGL14.eglDestroySurface(mEglDisplay, mEglSurface); + mEglSurface = null; + } + + if (mEglSurface == null) { + int[] eglSurfaceAttribList = new int[] { + EGL14.EGL_NONE, + EGL14.EGL_NONE, + EGL14.EGL_NONE, + EGL14.EGL_NONE, + EGL14.EGL_NONE + }; + + int index = 0; + // If the current display is in wide color, then so is the screenshot. + if (isWideColor) { + eglSurfaceAttribList[index++] = EGL_GL_COLORSPACE_KHR; + eglSurfaceAttribList[index++] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; + } + if (isProtected) { + eglSurfaceAttribList[index++] = EGL_PROTECTED_CONTENT_EXT; + eglSurfaceAttribList[index] = EGL14.EGL_TRUE; + } + // turn our SurfaceControl into a Surface + mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, mSurface, + eglSurfaceAttribList, 0); + if (mEglSurface == null) { + logEglError("eglCreateWindowSurface"); + return false; + } + } + return true; + } + + private void destroyEglSurface() { + if (mEglSurface != null) { + if (!EGL14.eglDestroySurface(mEglDisplay, mEglSurface)) { + logEglError("eglDestroySurface"); + } + mEglSurface = null; + } + } + + private void destroySurface() { + if (mSurfaceControl != null) { + mSurfaceLayout.dispose(); + mSurfaceLayout = null; + mTransaction.remove(mSurfaceControl).apply(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + + mSurfaceControl = null; + mSurfaceVisible = false; + mSurfaceAlpha = 0f; + } + } + + private boolean showSurface(float alpha) { + if (!mSurfaceVisible || mSurfaceAlpha != alpha) { + mTransaction.setLayer(mSurfaceControl, ELECTRON_BEAM_LAYER) + .setAlpha(mSurfaceControl, alpha) + .show(mSurfaceControl) + .apply(); + mSurfaceVisible = true; + mSurfaceAlpha = alpha; + } + return true; + } + + private boolean attachEglContext() { + if (mEglSurface == null) { + return false; + } + if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + logEglError("eglMakeCurrent"); + return false; + } + return true; + } + + private void detachEglContext() { + if (mEglDisplay != null) { + EGL14.eglMakeCurrent(mEglDisplay, + EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + } + } + + /** + * Interpolates a value in the range 0 .. 1 along a sigmoid curve + * yielding a result in the range 0 .. 1 scaled such that: + * scurve(0) == 0, scurve(0.5) == 0.5, scurve(1) == 1. + */ + private static float scurve(float value, float s) { + // A basic sigmoid has the form y = 1.0f / FloatMap.exp(-x * s). + // Here we take the input datum and shift it by 0.5 so that the + // domain spans the range -0.5 .. 0.5 instead of 0 .. 1. + final float x = value - 0.5f; + + // Next apply the sigmoid function to the scaled value + // which produces a value in the range 0 .. 1 so we subtract + // 0.5 to get a value in the range -0.5 .. 0.5 instead. + final float y = sigmoid(x, s) - 0.5f; + + // To obtain the desired boundary conditions we need to scale + // the result so that it fills a range of -1 .. 1. + final float v = sigmoid(0.5f, s) - 0.5f; + + // And finally remap the value back to a range of 0 .. 1. + return y / v * 0.5f + 0.5f; + } + + private static float sigmoid(float x, float s) { + return 1.0f / (1.0f + (float)Math.exp(-x * s)); + } + + private void destroyEglContext() { + if (mEglDisplay != null && mEglContext != null) { + EGL14.eglDestroyContext(mEglDisplay, mEglContext); + } + } + + private static FloatBuffer createNativeFloatBuffer(int size) { + ByteBuffer bb = ByteBuffer.allocateDirect(size * 4); + bb.order(ByteOrder.nativeOrder()); + return bb.asFloatBuffer(); + } + + private static void logEglError(String func) { + Slog.e(TAG, func + " failed: error " + EGL14.eglGetError(), new Throwable()); + } + + private static boolean checkGlErrors(String func) { + return checkGlErrors(func, true); + } + + private static boolean checkGlErrors(String func, boolean log) { + boolean hadError = false; + int error; + while ((error = GLES10.glGetError()) != GLES10.GL_NO_ERROR) { + if (log) { + Slog.e(TAG, func + " failed: error " + error, new Throwable()); + } + hadError = true; + } + return hadError; + } + + public void dump(PrintWriter pw) { + pw.println(); + pw.println("Electron Beam State:"); + pw.println(" mPrepared=" + mPrepared); + pw.println(" mMode=" + mMode); + pw.println(" mDisplayLayerStack=" + mDisplayLayerStack); + pw.println(" mDisplayWidth=" + mDisplayWidth); + pw.println(" mDisplayHeight=" + mDisplayHeight); + pw.println(" mSurfaceVisible=" + mSurfaceVisible); + pw.println(" mSurfaceAlpha=" + mSurfaceAlpha); + } + + /** + * Keeps a surface aligned with the natural orientation of the device. + * Updates the position and transformation of the matrix whenever the display + * is rotated. This is a little tricky because the display transaction + * callback can be invoked on any thread, not necessarily the thread that + * owns the electron beam. + */ + private static final class NaturalSurfaceLayout implements DisplayTransactionListener { + private final DisplayManagerInternal mDisplayManagerInternal; + private final int mDisplayId; + private SurfaceControl mSurfaceControl; + + public NaturalSurfaceLayout(DisplayManagerInternal displayManagerInternal, + int displayId, SurfaceControl surfaceControl) { + mDisplayManagerInternal = displayManagerInternal; + mDisplayId = displayId; + mSurfaceControl = surfaceControl; + mDisplayManagerInternal.registerDisplayTransactionListener(this); + } + + public void dispose() { + synchronized (this) { + mSurfaceControl = null; + } + mDisplayManagerInternal.unregisterDisplayTransactionListener(this); + } + + @Override + public void onDisplayTransaction(Transaction t) { + synchronized (this) { + if (mSurfaceControl == null) { + return; + } + + DisplayInfo displayInfo = mDisplayManagerInternal.getDisplayInfo(mDisplayId); + if (displayInfo == null) { + // displayInfo can be null if the associated display has been removed. There + // is a delay between the display being removed and ElectronBeam being dismissed. + return; + } + + switch (displayInfo.rotation) { + case Surface.ROTATION_0: + t.setPosition(mSurfaceControl, 0, 0); + t.setMatrix(mSurfaceControl, 1, 0, 0, 1); + break; + case Surface.ROTATION_90: + t.setPosition(mSurfaceControl, 0, displayInfo.logicalHeight); + t.setMatrix(mSurfaceControl, 0, -1, 1, 0); + break; + case Surface.ROTATION_180: + t.setPosition(mSurfaceControl, displayInfo.logicalWidth, + displayInfo.logicalHeight); + t.setMatrix(mSurfaceControl, -1, 0, 0, -1); + break; + case Surface.ROTATION_270: + t.setPosition(mSurfaceControl, displayInfo.logicalWidth, 0); + t.setMatrix(mSurfaceControl, 0, 1, -1, 0); + break; + } + } + } + } +} diff --git a/services/core/java/com/android/server/display/ScreenStateAnimator.java b/services/core/java/com/android/server/display/ScreenStateAnimator.java new file mode 100644 index 000000000000..9722e1949dd6 --- /dev/null +++ b/services/core/java/com/android/server/display/ScreenStateAnimator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 The Dirty Unicorns Project + * Copyright (C) 2022 The Potato Open Sauce Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import java.io.PrintWriter; + +import android.content.Context; + +/** @hide */ +public interface ScreenStateAnimator { + public static final int MODE_WARM_UP = 0; + public static final int MODE_COOL_DOWN = 1; + public static final int MODE_FADE = 2; + public static final int MODE_SCALE_DOWN = 3; + + public boolean prepare(Context context, int mode); + + public default void dismissResources() {} + + public void dismiss(); + + public void destroy(); + + public boolean draw(float level); + + public void dump(PrintWriter pw); +} From 2dfb1d8df2ea95d1f8a62dc129591dbd36ed0f94 Mon Sep 17 00:00:00 2001 From: Matt Filetto Date: Tue, 17 May 2022 16:52:58 -0700 Subject: [PATCH 0510/1315] Fix crash with protected content with ElectronBeam/Scale screen-off animation * Kind of an oddly specific use case here but to reproduce: - Enable CRT Screen Off Animation - Start playing a video from Netflix - Turn off screen using power key, you should see the crash then Co-authored by: Pranav Vashi Signed-off-by: Matt Filetto Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/display/ColorFade.java | 4 ++-- .../core/java/com/android/server/display/ElectronBeam.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java index cf14d5ddfef4..3e825a5d7a6c 100644 --- a/services/core/java/com/android/server/display/ColorFade.java +++ b/services/core/java/com/android/server/display/ColorFade.java @@ -706,8 +706,8 @@ private boolean createEglContext(boolean isProtected) { EGL14.EGL_NONE }; if (isProtected) { - eglContextAttribList[2] = EGL_PROTECTED_CONTENT_EXT; - eglContextAttribList[3] = EGL14.EGL_TRUE; + eglContextAttribList[3] = EGL_PROTECTED_CONTENT_EXT; + eglContextAttribList[4] = EGL14.EGL_TRUE; } mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT, eglContextAttribList, 0); diff --git a/services/core/java/com/android/server/display/ElectronBeam.java b/services/core/java/com/android/server/display/ElectronBeam.java index 6b28fa571ecf..f28416703077 100644 --- a/services/core/java/com/android/server/display/ElectronBeam.java +++ b/services/core/java/com/android/server/display/ElectronBeam.java @@ -701,8 +701,8 @@ private boolean createEglContext(boolean isProtected) { EGL14.EGL_NONE }; if (isProtected) { - eglContextAttribList[2] = EGL_PROTECTED_CONTENT_EXT; - eglContextAttribList[3] = EGL14.EGL_TRUE; + eglContextAttribList[3] = EGL_PROTECTED_CONTENT_EXT; + eglContextAttribList[4] = EGL14.EGL_TRUE; } mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT, eglContextAttribList, 0); From ed4877026995ab6eb840c1f7b4c21cad2b41d31b Mon Sep 17 00:00:00 2001 From: HELLBOY017 Date: Wed, 22 Mar 2023 18:35:31 +0530 Subject: [PATCH 0511/1315] SystemUI: Fix heads up notification timeout on ambient display Thanks to prochy-exe for pointing it out. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/notification/headsup/HeadsUpManagerImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 3dcad66bc258..79e79798c62a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -127,6 +127,7 @@ public class HeadsUpManagerImpl private int mAutoDismissTime; private final DelayableExecutor mExecutor; private final int mDecayDefault; + private boolean mDozing; private final int mExtensionTime; @@ -1261,6 +1262,7 @@ public void onStateChanged(int newState) { @Override public void onDozingChanged(boolean isDozing) { + mDozing = isDozing; if (!isDozing) { // Let's make sure all huns we got while dozing time out within the normal timeout // duration. Otherwise they could get stuck for a very long time @@ -1449,7 +1451,8 @@ public void updateEntry( FinishTimeUpdater finishTimeCalculator = () -> { RemainingDuration remainingDuration = - mAvalancheController.getDuration(this, mAutoDismissTime); + mAvalancheController.getDuration(this, + mDozing ? mDecayDefault * 1000 : mAutoDismissTime); if (remainingDuration instanceof RemainingDuration.HideImmediately) { if (AvalancheReplaceHunWhenCritical.isEnabled()) { From 48f5400e25007900a55d1619c13f3b823f9a36ff Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 19 Oct 2025 18:49:38 +0530 Subject: [PATCH 0512/1315] SystemUI: Add toggle for media squiggle animation * Currently, it disables only if Animator scale is 0. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++++++ .../ui/controller/MediaCarouselController.kt | 6 +++++- .../controls/ui/controller/MediaControlPanel.java | 12 +++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2b1e70e565fa..f304d4ebbdad 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14185,6 +14185,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String PULSE_ON_NEW_TRACKS = "pulse_on_new_tracks"; + /** + * Whether to show media squiggle animation + * @hide + */ + public static final String MEDIA_SQUIGGLE_ANIMATION = "media_squiggle_animation"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index b9056e0c9e43..ab0d9b5af4ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -459,7 +459,11 @@ constructor( bgExecutor.execute { globalSettings.registerContentObserverSync( Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), - animationScaleObserver, + animationScaleObserver + ) + secureSettings.registerContentObserverSync( + Settings.Secure.getUriFor(Settings.Secure.MEDIA_SQUIGGLE_ANIMATION), + animationScaleObserver ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index c8c4415c014e..82062b03c497 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -128,6 +128,7 @@ import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.GlobalSettings; import dagger.Lazy; @@ -232,6 +233,7 @@ public class MediaControlPanel { private TurbulenceNoiseController mTurbulenceNoiseController; private LoadingEffect mLoadingEffect; private final GlobalSettings mGlobalSettings; + private final SecureSettings mSecureSettings; private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig; private boolean mWasPlaying = false; private boolean mButtonClicked = false; @@ -287,7 +289,8 @@ public MediaControlPanel( ActivityIntentHelper activityIntentHelper, CommunalSceneInteractor communalSceneInteractor, NotificationLockscreenUserManager lockscreenUserManager, - GlobalSettings globalSettings + GlobalSettings globalSettings, + SecureSettings secureSettings ) { mContext = context; mBackgroundExecutor = backgroundExecutor; @@ -314,7 +317,7 @@ public MediaControlPanel( }); mGlobalSettings = globalSettings; - updateAnimatorDurationScale(); + mSecureSettings = secureSettings; } /** @@ -407,7 +410,9 @@ private void setSeekbarContentDescription(CharSequence elapsedTime, CharSequence void updateAnimatorDurationScale() { if (mSeekBarObserver != null) { mSeekBarObserver.setAnimationEnabled( - mGlobalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f); + mGlobalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f && + mSecureSettings.getIntForUser(Settings.Secure.MEDIA_SQUIGGLE_ANIMATION, + 1, UserHandle.USER_CURRENT) != 0); } } @@ -432,6 +437,7 @@ public void attachPlayer(MediaViewHolder vh) { mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener); mSeekBarViewModel.setContentDescriptionListener(mContentDescriptionListener); mMediaViewController.attach(player); + updateAnimatorDurationScale(); vh.getPlayer().setOnLongClickListener(v -> { if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true; From cb3ff53aa7c77cac7085edbb1477fc449c6cc98d Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 24 Mar 2024 23:59:24 +0530 Subject: [PATCH 0513/1315] SystemUI: Allow toggling rotation button suggestion [1/2] * Move logic to RotationButtonController rather defining in NavigationBarView. Co-authored-by: Ido Ben-Hur Change-Id: I870ce1dd54a97d85c418e5c4f1d00023871a2ec5 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 7 +++++++ .../rotation/RotationButtonController.java | 21 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index f304d4ebbdad..aceacb44591a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7427,6 +7427,13 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SCREEN_OFF_ANIMATION = "screen_off_animation"; + /** + * Whether to show rotation suggestion + * @hide + */ + @Readable + public static final String ENABLE_ROTATION_BUTTON = "enable_rotation_button"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 29753ed6bdc7..7d826fe096b5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -37,14 +37,17 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.database.ContentObserver; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemProperties; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import android.view.HapticFeedbackConstants; @@ -129,6 +132,7 @@ public class RotationButtonController { private boolean mSkipOverrideUserLockPrefsOnce; private final int mLightIconColor; private final int mDarkIconColor; + private boolean mButtonEnabled = true; @DrawableRes private final int mIconCcwStart0ResId; @@ -238,6 +242,21 @@ public RotationButtonController(RotationPolicyWrapper rotationPolicyWrapper, Con mRotationPolicyWrapper = rotationPolicyWrapper; mBgExecutor = context.getMainExecutor(); + + mButtonEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.ENABLE_ROTATION_BUTTON, 1, UserHandle.USER_CURRENT) == 1; + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ENABLE_ROTATION_BUTTON), false, + new ContentObserver(mMainThreadHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri.getLastPathSegment().equals(Settings.System.ENABLE_ROTATION_BUTTON)) { + mButtonEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.ENABLE_ROTATION_BUTTON, 1, UserHandle.USER_CURRENT) == 1; + } + } + } + ); } public void setRotationButton(RotationButton rotationButton, @@ -490,7 +509,7 @@ public void onRotationProposal(int rotation, boolean isValid) { int windowRotation = mWindowRotationProvider.get(); - if (!mRotationButton.acceptRotationProposal()) { + if (!mButtonEnabled || !mRotationButton.acceptRotationProposal()) { return; } From e60f1cd7940b38170d0874a3ea010a4bfa80cdb2 Mon Sep 17 00:00:00 2001 From: Ashwin R C Date: Wed, 27 May 2020 10:26:16 +0000 Subject: [PATCH 0514/1315] SystemUI: Adapt screenshot sound to ringer modes [SahilSonar - POSP]: Forward port to android-12.0.0 idoybh: Adapt to A13 Change-Id: I381c351131241e45ddb6049706d6c302c2eee946 base: allow disable of screenshot shutter sound [1/2] [SahilSonar - POSP] - Forward port to android-12.0.0 idoybh: Adapt to A13 Change-Id: I47d52bba21170118af87d35376d81d7569587a2f SystemUI: Screenshots: Refactor shutter sound logic * Commit 2f09bacae02fe7a99d62355331c3a26b83752a6a introduced some duplicated code * Move it to an own method to reduce the footprint in the AOSP code parts and to reduce duplication [SahilSonar - POSP] - Forward port to androi-12.0.0 idoybh: Adapt to A13 Change-Id: I57eaaee4db401d16cc6ef65c68604cdb4053ca01 Signed-off-by: Anushek Prasal SystemUI: Fix shutter sound * When shutter sound for camera is forced on as required in some states, (config_camera_sound_forced, set via mcc/mnc), we also want to (or should) play it when a screenshot is taken from the preview instead of an actual picture * This change is loosely based on https://android-review.googlesource.com/c/platform/frameworks/base/+/1517742/ but uses publicly available APIs Testing: - Set config_camera_sound_forced to true and push a build to device - Turn down all stream volumes to muted - Take screenshot of any normal screen -> No sound played - Open camera, take screenshot -> Sound played - Turn up volume and repeat the screenshots -> Sound played in all cases [SahilSonar - POSP] - Forward port to android-12.0.0 idoybh: Adapt to A13 @neobuddy89: * Remove enforcing shutter sound via prop. * Adapt for A15 QPR1 new screenshot controller Change-Id: I381c351131241e45ddb6049706d6c302c2eee946 Signed-off-by: Omkar Chandorkar Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 5 +++ .../screenshot/ScreenshotController.kt | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index aceacb44591a..9ead3222351d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7434,6 +7434,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String ENABLE_ROTATION_BUTTON = "enable_rotation_button"; + /** + * @hide + */ + public static final String SCREENSHOT_SHUTTER_SOUND = "screenshot_shutter_sound"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index 5b2c9b28bb88..57368610a8e4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -28,10 +28,13 @@ import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Insets import android.graphics.Rect +import android.media.AudioManager import android.net.Uri import android.os.Process import android.os.UserHandle import android.os.UserManager +import android.os.VibrationEffect +import android.os.Vibrator import android.provider.Settings import android.util.DisplayMetrics import android.util.Log @@ -125,12 +128,18 @@ internal constructor( ActivityInfo.CONFIG_ASSETS_PATHS ) + private val audioManager: AudioManager + private val vibrator: Vibrator? + init { screenshotHandler.defaultTimeoutMillis = SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS window = screenshotWindowFactory.create(display) context = window.getContext() + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? + viewProxy = viewProxyFactory.getProxy(context, display.displayId) screenshotHandler.setOnTimeoutRunnable { @@ -479,13 +488,43 @@ internal constructor( viewProxy.stopInputListening() } + private fun playScreenshotSound() { + var playSound = false + when (audioManager.ringerMode) { + AudioManager.RINGER_MODE_SILENT -> { + // do nothing + } + AudioManager.RINGER_MODE_VIBRATE -> { + vibrator?.takeIf { it.hasVibrator() }?.vibrate( + VibrationEffect.createOneShot( + 50, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + } + AudioManager.RINGER_MODE_NORMAL -> { + // in this case we want to play sound even if not forced on + playSound = true + } + } + if (playSound && Settings.System.getIntForUser( + context.contentResolver, + Settings.System.SCREENSHOT_SHUTTER_SOUND, + 1, + UserHandle.USER_CURRENT + ) == 1 + ) { + screenshotSoundController.playScreenshotSoundAsync() + } + } + /** * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on * failure). */ private fun saveScreenshotAndToast(screenshot: ScreenshotData, finisher: Consumer) { // Play the shutter sound to notify that we've taken a screenshot - screenshotSoundController.playScreenshotSoundAsync() + playScreenshotSound() saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher) { result: ImageExporter.Result -> @@ -514,7 +553,7 @@ internal constructor( viewProxy.createScreenshotDropInAnimation(screenRect, showFlash).apply { doOnEnd { onAnimationComplete?.run() } // Play the shutter sound to notify that we've taken a screenshot - screenshotSoundController.playScreenshotSoundAsync() + playScreenshotSound() if (LogConfig.DEBUG_ANIM) { Log.d(TAG, "starting post-screenshot animation") } From 7d7373f00ad7b08b840d2793af0e7fa968e3a8e6 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 8 Dec 2025 11:22:09 +0530 Subject: [PATCH 0515/1315] VolumeHaptics: Tune the primitives Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../volume/haptics/ui/VolumeHapticsConfigsProvider.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt index a1207f421581..75f032ad5374 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt @@ -38,7 +38,9 @@ object VolumeHapticsConfigsProvider { // Create a set of continuous configs hapticFeedbackConfig = SliderHapticFeedbackConfig( - additionalVelocityMaxBump = 0.1f, + progressBasedDragMinScale = 0.1f, + progressBasedDragMaxScale = 0.85f, + additionalVelocityMaxBump = 0.25f, deltaProgressForDragThreshold = 0.02f, numberOfLowTicks = 4, maxVelocityToScale = 0.5f, /* slider progress(from 0 to 1) per sec */ @@ -55,9 +57,9 @@ object VolumeHapticsConfigsProvider { SliderHapticFeedbackConfig( lowerBookendScale = 0.2f, progressBasedDragMinScale = 0.2f, - progressBasedDragMaxScale = 0.5f, + progressBasedDragMaxScale = 0.85f, deltaProgressForDragThreshold = 0f, - additionalVelocityMaxBump = 0.2f, + additionalVelocityMaxBump = 0.25f, maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */ sliderStepSize = stepSize, filter = filter, From 74ae7e45bb08578c28250904ae53844e463c5c94 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 13 Apr 2025 21:37:21 +0530 Subject: [PATCH 0516/1315] SystemUI: VolumeDialog: Add toggle for haptic feedback [1/2] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 +++++ .../ui/VolumeDialogSliderViewBinder.kt | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 9ead3222351d..0ce8a719be88 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13390,6 +13390,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String VOLUME_DIALOG_DISMISS_TIMEOUT = "volume_dialog_dismiss_timeout"; + /** + * Volume dialog haptic feedback + * @hide + */ + public static final String VOLUME_DIALOG_HAPTIC_FEEDBACK = "volume_dialog_haptic_feedback"; + /** * What behavior should be invoked when the volume hush gesture is triggered * One of VOLUME_HUSH_OFF, VOLUME_HUSH_VIBRATE, VOLUME_HUSH_MUTE. diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index 82fabca7a848..bb6b59c08e97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.volume.dialog.sliders.ui +import android.os.UserHandle +import android.provider.Settings import android.view.View import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.interaction.DragInteraction @@ -32,6 +34,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -81,6 +84,19 @@ constructor( } } +@Composable +private fun rememberVolumeHapticsEnabled(): Boolean { + val context = LocalContext.current + return remember { + Settings.Secure.getIntForUser( + context.contentResolver, + Settings.Secure.VOLUME_DIALOG_HAPTIC_FEEDBACK, + 1, + UserHandle.USER_CURRENT, + ) != 0 + } +} + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun VolumeDialogSlider( @@ -100,6 +116,7 @@ private fun VolumeDialogSlider( val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null) val sliderStateModel = collectedSliderStateModel ?: return val interactionSource = remember { MutableInteractionSource() } + val isHapticsEnabled = rememberVolumeHapticsEnabled() LaunchedEffect(interactionSource) { interactionSource.interactions.collect { @@ -128,7 +145,7 @@ private fun VolumeDialogSlider( isVertical = isVolumeDialogVertical, colors = colors, interactionSource = interactionSource, - haptics = + haptics = if (isHapticsEnabled) { Haptics.Enabled( hapticsViewModelFactory = hapticsViewModelFactory, hapticConfigs = @@ -139,7 +156,10 @@ private fun VolumeDialogSlider( } else { Orientation.Horizontal }, - ), + ) + } else { + Haptics.Disabled + }, stepDistance = 1f, track = { sliderState -> SliderTrack( From 2d4d2d8454de9ebb640703cf39289d11d03283c6 Mon Sep 17 00:00:00 2001 From: John Galt Date: Sat, 6 Dec 2025 11:12:06 -0500 Subject: [PATCH 0517/1315] Revert "Adjust the threshold for disabling blur on thermal status" This reverts commit a22611171fd334ea153dded3f310bffaa38e56ed. Just no. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/BlurController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/wm/BlurController.java b/services/core/java/com/android/server/wm/BlurController.java index 3ef31acd8365..41d9dbf894f4 100644 --- a/services/core/java/com/android/server/wm/BlurController.java +++ b/services/core/java/com/android/server/wm/BlurController.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.os.PowerManager.THERMAL_STATUS_SEVERE; +import static android.os.PowerManager.THERMAL_STATUS_CRITICAL; import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED; import android.content.BroadcastReceiver; @@ -46,7 +46,7 @@ final class BlurController { private final Object mLock = new Object(); private volatile boolean mBlurEnabled; private boolean mInPowerSaveMode; - private boolean mDisabledByThermal; + private boolean mCriticalThermalStatus; private boolean mBlurDisabledSetting; private boolean mTunnelModeEnabled = false; @@ -92,10 +92,10 @@ public void onChange(boolean selfChange) { mBlurDisabledSetting = getBlurDisabledSetting(); powerManager.addThermalStatusListener((status) -> { - mDisabledByThermal = status >= THERMAL_STATUS_SEVERE; + mCriticalThermalStatus = status >= THERMAL_STATUS_CRITICAL; updateBlurEnabled(); }); - mDisabledByThermal = powerManager.getCurrentThermalStatus() >= THERMAL_STATUS_SEVERE; + mCriticalThermalStatus = powerManager.getCurrentThermalStatus() >= THERMAL_STATUS_CRITICAL; TunnelModeEnabledListener.register(mTunnelModeListener); @@ -120,7 +120,7 @@ boolean getBlurEnabled() { private void updateBlurEnabled() { synchronized (mLock) { final boolean newEnabled = CROSS_WINDOW_BLUR_SUPPORTED && !mBlurDisabledSetting - && !mInPowerSaveMode && !mTunnelModeEnabled && !mDisabledByThermal; + && !mInPowerSaveMode && !mTunnelModeEnabled && !mCriticalThermalStatus; if (mBlurEnabled == newEnabled) { return; } From e3ccd5233b02d98603e36a2eb77b229501c1916c Mon Sep 17 00:00:00 2001 From: John Galt Date: Sat, 6 Dec 2025 11:12:19 -0500 Subject: [PATCH 0518/1315] Revert "Disable blurs during critical thermal state" This reverts commit 150831e55f7497e7769596f641de302fae3f2a41. Just no Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/BlurController.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/services/core/java/com/android/server/wm/BlurController.java b/services/core/java/com/android/server/wm/BlurController.java index 41d9dbf894f4..03639449c3df 100644 --- a/services/core/java/com/android/server/wm/BlurController.java +++ b/services/core/java/com/android/server/wm/BlurController.java @@ -16,7 +16,6 @@ package com.android.server.wm; -import static android.os.PowerManager.THERMAL_STATUS_CRITICAL; import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED; import android.content.BroadcastReceiver; @@ -46,7 +45,6 @@ final class BlurController { private final Object mLock = new Object(); private volatile boolean mBlurEnabled; private boolean mInPowerSaveMode; - private boolean mCriticalThermalStatus; private boolean mBlurDisabledSetting; private boolean mTunnelModeEnabled = false; @@ -91,12 +89,6 @@ public void onChange(boolean selfChange) { }); mBlurDisabledSetting = getBlurDisabledSetting(); - powerManager.addThermalStatusListener((status) -> { - mCriticalThermalStatus = status >= THERMAL_STATUS_CRITICAL; - updateBlurEnabled(); - }); - mCriticalThermalStatus = powerManager.getCurrentThermalStatus() >= THERMAL_STATUS_CRITICAL; - TunnelModeEnabledListener.register(mTunnelModeListener); updateBlurEnabled(); @@ -120,7 +112,7 @@ boolean getBlurEnabled() { private void updateBlurEnabled() { synchronized (mLock) { final boolean newEnabled = CROSS_WINDOW_BLUR_SUPPORTED && !mBlurDisabledSetting - && !mInPowerSaveMode && !mTunnelModeEnabled && !mCriticalThermalStatus; + && !mInPowerSaveMode && !mTunnelModeEnabled; if (mBlurEnabled == newEnabled) { return; } From d1e21e2fbb8089c83f1c1395ca91da2d90edc9d2 Mon Sep 17 00:00:00 2001 From: Alex Cruz Date: Wed, 4 Oct 2017 02:30:55 -0400 Subject: [PATCH 0519/1315] Increase Zenmode max hour limit from 12 to 24 - Doesn't make sense to limit zenmode to just 12 hours when there's 24 hours on the clock. I get that someone at Google was looking at it like 'we only work 12 hours a day' and that's probably where that came from but real people sometimes need their phone quiet for more than 12 hours in a day. Change-Id: Ibf4b5ec7412b3b9656622065e28280f42cc57f94 Signed-off-by: ZeNiXxX Signed-off-by: spezi77 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/service/notification/ZenModeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 417b478e9f41..3d909e02a29f 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -526,7 +526,7 @@ private static boolean sameCondition(ZenRule rule) { } private static int[] generateMinuteBuckets() { - final int maxHrs = 12; + final int maxHrs = 24; final int[] buckets = new int[maxHrs + 3]; buckets[0] = 15; buckets[1] = 30; From c28027e4882aad0fdc49ddbd37a6987b1fab585d Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 11 Dec 2024 14:20:57 +0200 Subject: [PATCH 0520/1315] disable safe media volume management Original change: https://github.com/GrapheneOS-Archive/platform_frameworks_base-old/pull/27 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/values/symbols.xml | 2 -- .../com/android/server/audio/SoundDoseHelper.java | 13 +++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2ff9d0586b68..77fdce7098f6 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -370,9 +370,7 @@ - - diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index d413ca716f08..8d7b3c8314c8 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -1016,10 +1016,7 @@ private void updateSafeMediaVolume_l(String caller) { || mEnableCsd.get(); boolean safeMediaVolumeForce = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_FORCE, false); - // we are using the MCC overlaid legacy flag used for the safe volume enablement - // to determine whether the MCC enforces any safe hearing standard. - boolean mccEnforcedSafeMediaVolume = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_safe_media_volume_enabled); + boolean mccEnforcedSafeMediaVolume = false; boolean safeVolumeEnabled = (mccEnforcedSafeMediaVolume || safeMediaVolumeForce) && !safeMediaVolumeBypass; @@ -1055,12 +1052,8 @@ private void updateSafeMediaVolume_l(String caller) { private void updateCsdEnabled(String caller) { mForceCsdProperty.set(SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE, false)); - // we are using the MCC overlaid legacy flag used for the safe volume enablement - // to determine whether the MCC enforces any safe hearing standard. - boolean mccEnforcedSafeMedia = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_safe_media_volume_enabled); - boolean csdEnable = mContext.getResources().getBoolean( - R.bool.config_safe_sound_dosage_enabled); + boolean mccEnforcedSafeMedia = false; + boolean csdEnable = false; boolean newEnabledCsd = (mccEnforcedSafeMedia && csdEnable) || mForceCsdProperty.get(); synchronized (mCsdAsAFeatureLock) { From 2247770ac7996fc4601fde0ff7061535e289fa2b Mon Sep 17 00:00:00 2001 From: SagarMakhar Date: Wed, 17 Aug 2022 13:21:15 +0000 Subject: [PATCH 0521/1315] BiometricScheduler: Cancel operation if not idle - some hals fail to report success/failure (for ex. realme fp hals) [DarkJoker360 - Switch to overlays] [timjosten - Adapt to 12.1.0_r8 merge changes] [ghostrider-reborn - Simplify code] Change-Id: I442ce063280af36a04c25fcbc3dd45a90f196988 Signed-off-by: SagarMakhar Signed-off-by: DarkJoker360 Signed-off-by: Sarthak Roy Signed-off-by: Adithya R Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/values/matrixx_config.xml | 3 +++ core/res/res/values/matrixx_symbols.xml | 3 +++ .../biometrics/sensors/BiometricScheduler.java | 17 ++++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index 9a2ccb14b9d5..b34461663e93 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -98,4 +98,7 @@ false + + + false diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 9e92ddb11e4f..c8266f590a64 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -84,4 +84,7 @@ + + + diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 1d4e8b8c185e..842272fcf20d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -69,6 +69,9 @@ public class BiometricScheduler { private static final String TAG = "BiometricScheduler"; + + private boolean mCancel; + // Number of recent operations to keep in our logs for dumpsys protected static final int LOG_NUM_RECENT_OPERATIONS = 50; @@ -276,12 +279,15 @@ public BiometricScheduler(@NonNull Handler handler, * @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures * (such as fingerprint swipe). */ - public BiometricScheduler(@SensorType int sensorType, + public BiometricScheduler(Context context, @SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { this(new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher, IBiometricService.Stub.asInterface( ServiceManager.getService(Context.BIOMETRIC_SERVICE)), LOG_NUM_RECENT_OPERATIONS); + + mCancel = context.getResources().getBoolean( + com.android.internal.R.bool.config_fpCancelIfNotIdle); } /** @@ -352,8 +358,13 @@ protected void checkCurrentUserAndStartNextOperation() { protected void startNextOperationIfIdle() { if (mCurrentOperation != null) { - Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation); - return; + if (mCancel && !mCurrentOperation.isFinished()) { + Slog.v(TAG, "Not idle, cancelling current operation: " + mCurrentOperation); + mCurrentOperation.cancel(mHandler, mInternalCallback); + } else { + Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation); + return; + } } if (mPendingOperations.isEmpty()) { Slog.d(TAG, "No operations, returning to idle"); From b6fd99fd3e42d58f2924e8f39a18ce7a8dae4455 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 9 Aug 2024 15:12:13 +0800 Subject: [PATCH 0522/1315] Shell: Use night/light theme for buttons/caption color instead of luminance * using luminance for caption decor background color results to a lot of visual issues, making the buttons hard to see. * this change follows OEM bevahiour like hyperOS/nothingOS etc. Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../windowdecor/CaptionWindowDecoration.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 127ca3c98984..83ee5694ce3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -390,17 +390,17 @@ private void setCaptionColor(int captionColor) { if (mResult.mRootView == null) { return; } + int nightModeFlags = mContext.getResources().getConfiguration().uiMode & + android.content.res.Configuration.UI_MODE_NIGHT_MASK; + boolean isNightMode = nightModeFlags == android.content.res.Configuration.UI_MODE_NIGHT_YES; + + int mCaptionColor = isNightMode ? Color.BLACK : Color.WHITE; + int buttonTintColorRes = isNightMode ? R.color.decor_button_light_color : R.color.decor_button_dark_color; + ColorStateList buttonTintColor = mContext.getResources().getColorStateList(buttonTintColorRes, null /* theme */); final View caption = mResult.mRootView.findViewById(R.id.caption); final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); - captionDrawable.setColor(captionColor); - - final int buttonTintColorRes = - Color.valueOf(captionColor).luminance() < 0.5 - ? R.color.decor_button_light_color - : R.color.decor_button_dark_color; - final ColorStateList buttonTintColor = - caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); + captionDrawable.setColor(mCaptionColor); final View back = caption.findViewById(R.id.back_button); back.setBackgroundTintList(buttonTintColor); From 022c7cab8d4ccf0ea4782ae00da4904a404e0df8 Mon Sep 17 00:00:00 2001 From: tejasvp25 Date: Mon, 3 Jan 2022 07:14:11 +0000 Subject: [PATCH 0523/1315] NavigationModeController: Silence log spam Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../navigationbar/NavigationModeController.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java index 99daf368c846..cf66e1596ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java @@ -140,7 +140,6 @@ public void updateCurrentInteractionMode(boolean notify) { Secure.NAVIGATION_MODE, String.valueOf(mode))); if (DEBUG) { Log.d(TAG, "updateCurrentInteractionMode: mode=" + mode); - dumpAssetPaths(mCurrentUserContext); } if (notify) { @@ -205,15 +204,5 @@ public void dump(PrintWriter pw, String[] args) { defaultOverlays = "failed_to_fetch"; } pw.println(" defaultOverlays=" + defaultOverlays); - dumpAssetPaths(mCurrentUserContext); - } - - private void dumpAssetPaths(Context context) { - Log.d(TAG, " contextUser=" + mCurrentUserContext.getUserId()); - Log.d(TAG, " assetPaths="); - ApkAssets[] assets = context.getResources().getAssets().getApkAssets(); - for (ApkAssets a : assets) { - Log.d(TAG, " " + a.getDebugName()); - } } } From 34bc980745d391e2cefb73becd92a3894ccc8dc8 Mon Sep 17 00:00:00 2001 From: Alvin Francis Date: Fri, 13 Dec 2024 00:19:22 -0400 Subject: [PATCH 0524/1315] SystemUI: Fix NullPointerException in updateFocusOverlayRadii for missing drawable layer Log 12-13 00:05:50.403 2156 2156 E AndroidRuntime: FATAL EXCEPTION: main 12-13 00:05:50.403 2156 2156 E AndroidRuntime: Process: com.android.systemui, PID: 2156 12-13 00:05:50.403 2156 2156 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.graphics.drawable.GradientDrawable.setCornerRadii(float[])' on a null object reference 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.NotificationBackgroundView.updateFocusOverlayRadii(NotificationBackgroundView.java:317) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.NotificationBackgroundView.updateBackgroundRadii(NotificationBackgroundView.java:302) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.NotificationBackgroundView.setCustomBackground(NotificationBackgroundView.java:169) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.NotificationBackgroundView.setCustomBackground(NotificationBackgroundView.java:175) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.ActivatableNotificationView.initBackground(ActivatableNotificationView.java:180) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.onDensityOrFontScaleChanged(ExpandableNotificationRow.java:1367) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.collection.NotificationEntry.onDensityOrFontScaleChanged(NotificationEntry.java:743) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.collection.coordinator.ViewConfigCoordinator.updateNotificationsOnDensityOrFontScaleChanged(ViewConfigCoordinator.kt:147) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.collection.coordinator.ViewConfigCoordinator.onDensityOrFontScaleChanged(ViewConfigCoordinator.kt:95) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.notification.collection.coordinator.ViewConfigCoordinator.onThemeChanged(ViewConfigCoordinator.kt:118) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.statusbar.phone.ConfigurationControllerImpl.onConfigurationChanged(ConfigurationControllerImpl.kt:141) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.systemui.SystemUIApplication.onConfigurationChanged(SystemUIApplication.java:458) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ConfigurationController.performConfigurationChanged(ConfigurationController.java:265) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChangedInner(ConfigurationController.java:239) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:157) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:132) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ActivityThread.handleConfigurationChanged(ActivityThread.java:6582) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.servertransaction.ConfigurationChangeItem.execute(ConfigurationChangeItem.java:46) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:174) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:109) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:81) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2636) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:107) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8705) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580) 12-13 00:05:50.403 2156 2156 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886) Signed-off-by: Alvin Francis Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../notification/row/NotificationBackgroundView.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index a1987581fab3..03d31a87438e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -31,6 +31,7 @@ import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -450,6 +451,10 @@ private void updateFocusOverlayRadii(LayerDrawable background) { GradientDrawable overlay = (GradientDrawable) background.findDrawableByLayerId( R.id.notification_focus_overlay); + if (overlay == null) { + Log.w("NotificationBackgroundView", "Focus overlay not found in LayerDrawable"); + return; + } for (int i = 0; i < mCornerRadii.length; i++) { // in theory subtracting mFocusOverlayStroke/2 should be enough but notification // background is still peeking a bit from below - probably due to antialiasing or From 220f32fdda281014ec083435d5cabf031347343b Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 28 Mar 2025 13:07:09 +0200 Subject: [PATCH 0525/1315] fix NPE system_server crash in F2fsUtils.getFilesRecursive() Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/content/F2fsUtils.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/java/com/android/internal/content/F2fsUtils.java b/core/java/com/android/internal/content/F2fsUtils.java index 27f1b308ed9c..afa8d7773b0b 100644 --- a/core/java/com/android/internal/content/F2fsUtils.java +++ b/core/java/com/android/internal/content/F2fsUtils.java @@ -266,7 +266,10 @@ private static List getFilesRecursive(@NonNull File path) { final ArrayList files = new ArrayList<>(); for (File f : allFiles) { if (f.isDirectory()) { - files.addAll(getFilesRecursive(f)); + List inner = getFilesRecursive(f); + if (inner != null) { + files.addAll(inner); + } } else if (f.isFile()) { files.add(f); } From 3d42c630f71a7d00c1ed21670ddd035648c7a994 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 5 May 2025 14:03:59 +0800 Subject: [PATCH 0526/1315] NetworkManagementService: fix crash when mUidCleartextPolicy is empty Change-Id: Ib4d3554968d6b4f3a07083bf8494db9e7668d3e9 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/net/NetworkManagementService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index 74f0d9cf3e39..0448972fffe7 100644 --- a/services/core/java/com/android/server/net/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -485,7 +485,7 @@ private void prepareNativeDaemon() { } } - size = mUidCleartextPolicy.size(); + size = mUidCleartextPolicy != null ? mUidCleartextPolicy.size() : 0; if (size > 0) { if (DBG) Slog.d(TAG, "Pushing " + size + " active UID cleartext policies"); final SparseIntArray local = mUidCleartextPolicy; From a6dd2ae2ad3ff323cf5d5dc1820cc21ee994684c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 10 Jun 2025 06:42:02 +0800 Subject: [PATCH 0527/1315] DeviceIdleController: fix google gms idle whitelist security exception 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: com.google.android.gms requires exemption in /system/etc/sysconfig/google.xml for core device features to function. 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: java.lang.SecurityException: No permission to change device idle whitelist: Neither user 10253 nor current process has android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST. 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.Parcel.createExceptionOrNull(Parcel.java:3261) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.Parcel.createException(Parcel.java:3245) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.Parcel.readException(Parcel.java:3228) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.Parcel.readException(Parcel.java:3170) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.IDeviceIdleController$Stub$Proxy.addPowerSaveTempWhitelistApp(IDeviceIdleController.java:793) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.PowerExemptionManager.addToTemporaryAllowList(PowerExemptionManager.java:630) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.PowerWhitelistManager.whitelistAppTemporarily(PowerWhitelistManager.java:481) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.app.usage.UsageStatsManager.whitelistAppTemporarily(UsageStatsManager.java:1448) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at axgs.a(:com.google.android.gms@252037035@25.20.37 (260400-766884605):147) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at axgr.run(:com.google.android.gms@252037035@25.20.37 (260400-766884605):178) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at ayjt.c(:com.google.android.gms@252037035@25.20.37 (260400-766884605):50) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at ayjt.run(:com.google.android.gms@252037035@25.20.37 (260400-766884605):70) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at ayph.run(:com.google.android.gms@252037035@25.20.37 (260400-766884605):8) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at java.lang.Thread.run(Thread.java:1119) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: Caused by: android.os.RemoteException: Remote stack trace: 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.app.ContextImpl.enforce(ContextImpl.java:2471) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.app.ContextImpl.enforceCallingOrSelfPermission(ContextImpl.java:2499) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at com.android.server.DeviceIdleController.addPowerSaveTempAllowlistAppChecked(DeviceIdleController.java:3234) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at com.android.server.DeviceIdleController$BinderService.addPowerSaveTempWhitelistApp(DeviceIdleController.java:2280) 06-09 01:31:05.679 3008 3050 E GmsReceiverSupport: at android.os.IDeviceIdleController$Stub.onTransact(IDeviceIdleController.java:410) Change-Id: I93689cc35d6ac5d576365515494aea1ce871f2ab Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/DeviceIdleController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 0b286a7bf8de..cda18f1ffb97 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -3241,9 +3241,11 @@ public int[] getAppIdTempWhitelistInternal() { void addPowerSaveTempAllowlistAppChecked(String packageName, long duration, int userId, @ReasonCode int reasonCode, @Nullable String reason) throws RemoteException { - getContext().enforceCallingOrSelfPermission( - Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, - "No permission to change device idle whitelist"); + if (!packageName.equals("com.google.android.gms")) { + getContext().enforceCallingOrSelfPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + } final int callingUid = Binder.getCallingUid(); userId = ActivityManager.getService().handleIncomingUser( Binder.getCallingPid(), From d1be2584a9e25b9b7c643856516b8b237f119b69 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 1 Feb 2025 16:33:26 +0200 Subject: [PATCH 0528/1315] don't delay setting observer callbacks for background system packages 462062c91cc2bad6d289ded27d3d5d7b53e224fb added an unconditional 10 second delay to ContentProvider change callbacks for all background apps as part of MediaProvider optimizations. This led to unexpected delays when changing SettingsProvider settings (android.provider.Settings) that are observed by background system packages. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/content/ContentService.java | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java index 811d372e3899..22e08c29e31e 100644 --- a/services/core/java/com/android/server/content/ContentService.java +++ b/services/core/java/com/android/server/content/ContentService.java @@ -51,6 +51,7 @@ import android.content.SyncRequest; import android.content.SyncStatusInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.ProviderInfo; import android.database.IContentObserver; import android.net.Uri; @@ -65,6 +66,7 @@ import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -86,6 +88,8 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.permission.LegacyPermissionManagerInternal; +import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.PackageState; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -584,6 +588,14 @@ public void dispatch() { final Key key = collected.keyAt(i); final List value = collected.valueAt(i); + boolean hasSettingsUri = false; + for (Uri uri : value) { + if (Settings.AUTHORITY.equals(uri.getAuthority())) { + hasSettingsUri = true; + break; + } + } + final Runnable task = () -> { try { key.observer.onChangeEtc(key.selfChange, @@ -593,12 +605,27 @@ public void dispatch() { }; // Immediately dispatch notifications to foreground apps that - // are important to the user; all other background observers are - // delayed to avoid stampeding + // are important to the user and to all system apps if there's at least one changed + // settings uri; all other background observers are delayed to avoid stampeding final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0; - final int procState = LocalServices.getService(ActivityManagerInternal.class) + boolean isImmediate = noDelay; + if (!isImmediate) { + final int procState = LocalServices.getService(ActivityManagerInternal.class) .getUidProcessState(key.uid); - if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || noDelay) { + isImmediate = procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; + } + if (hasSettingsUri && !isImmediate) { + var pm = LocalServices.getService(PackageManagerInternal.class); + AndroidPackage pkg = pm.getPackage(key.uid); + if (pkg != null) { + PackageState state = pm.getPackageStateInternal(pkg.getPackageName()); + if (state != null && state.isSystem()) { + isImmediate = true; + } + } + } + + if (isImmediate) { task.run(); } else { BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY); From 3ecd1c1365739d6fd6817a6f6fc0dbb8b91860ce Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 17 Dec 2025 11:06:41 +0200 Subject: [PATCH 0529/1315] add a workaround for WallpaperManagerService.hasPermission() crash Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wallpaper/WallpaperData.java | 4 +++- .../android/server/wallpaper/WallpaperManagerService.java | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index b117ab226157..ac5f327d0433 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -26,6 +26,7 @@ import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; import android.app.WallpaperManager.ScreenOrientation; @@ -239,7 +240,8 @@ private File getFile(SparseArray map, String fileName) { return result; } - @NonNull ComponentName getComponent() { + @Nullable + ComponentName getComponent() { return mDescription.getComponent(); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 1d15f9291b75..8e1d342c6dc4 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2412,10 +2412,14 @@ private boolean hasPermission(String permission) { } private boolean hasPermission(WallpaperData data, String permission) { + ComponentName component = data.getComponent(); + if (component == null) { + return false; + } try { return PackageManager.PERMISSION_GRANTED == mIPackageManager.checkPermission( permission, - data.getComponent().getPackageName(), + component.getPackageName(), data.userId); } catch (RemoteException e) { Slog.e(TAG, "Failed to check wallpaper service permission", e); From 902ae70734dae60131c6bcb8c670f1b6927d5732 Mon Sep 17 00:00:00 2001 From: Ge Tianxiong Date: Thu, 22 May 2025 12:32:45 +0800 Subject: [PATCH 0530/1315] Fix the system server restart issue caused by the fingerprint framework In the `HidlToAidlSessionAdapter.java` code, if the fingerprint service provided by the HAL malfunctions and causes `mSession.get()` to return null, it will lead to a `NullPointerException`, which subsequently causes the system service to restart. To address this, we need to modify the code to add safeguards that ensure other system services continue to function correctly, even when the HAL layer's fingerprint service is unavailable. Google: 3636658 Change-Id: Iebbfbfe5b20fcfd99eab954d6dc5f9939ac4df8c Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../hidl/HidlToAidlSessionAdapter.java | 98 +++++++++++++++---- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java index 671bd875d194..62a08545d094 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java @@ -65,46 +65,91 @@ public IBinder asBinder() { @Override public void generateChallenge() throws RemoteException { - long challenge = mSession.get().preEnroll(); - mHidlToAidlCallbackConverter.onChallengeGenerated(challenge); + try { + long challenge = mSession.get().preEnroll(); + mHidlToAidlCallbackConverter.onChallengeGenerated(challenge); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "generateChallenge: exception!", e); + } } @Override public void revokeChallenge(long challenge) throws RemoteException { - mSession.get().postEnroll(); - mHidlToAidlCallbackConverter.onChallengeRevoked(0L); + try { + mSession.get().postEnroll(); + mHidlToAidlCallbackConverter.onChallengeRevoked(0L); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "revokeChallenge: exception!", e); + } } @Override public ICancellationSignal enroll(HardwareAuthToken hat) throws RemoteException { - mSession.get().enroll(HardwareAuthTokenUtils.toByteArray(hat), mUserId, - ENROLL_TIMEOUT_SEC); - return new Cancellation(); + try { + mSession.get().enroll(HardwareAuthTokenUtils.toByteArray(hat), mUserId, + ENROLL_TIMEOUT_SEC); + return new Cancellation(); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "enroll: exception!", e); + } + return null; } @Override public ICancellationSignal authenticate(long operationId) throws RemoteException { - mSession.get().authenticate(operationId, mUserId); - return new Cancellation(); + try { + mSession.get().authenticate(operationId, mUserId); + return new Cancellation(); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "authenticate: exception!", e); + } + return null; } @Override public ICancellationSignal detectInteraction() throws RemoteException { - mSession.get().authenticate(0, mUserId); - return new Cancellation(); + try { + mSession.get().authenticate(0, mUserId); + return new Cancellation(); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "detectInteraction: exception!", e); + } + return null; } @Override public void enumerateEnrollments() throws RemoteException { - mSession.get().enumerate(); + try { + mSession.get().enumerate(); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "enumerateEnrollments: exception!", e); + } } @Override public void removeEnrollments(int[] enrollmentIds) throws RemoteException { - if (enrollmentIds.length > 1) { - mSession.get().remove(mUserId, 0); - } else { - mSession.get().remove(mUserId, enrollmentIds[0]); + try { + if (enrollmentIds.length > 1) { + mSession.get().remove(mUserId, 0); + } else { + mSession.get().remove(mUserId, enrollmentIds[0]); + } + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "removeEnrollments: exception!", e); } } @@ -214,11 +259,24 @@ protected IBiometricsFingerprint getIBiometricsFingerprint() { } public long getAuthenticatorIdForUpdateClient() throws RemoteException { - return mSession.get().getAuthenticatorId(); + try { + return mSession.get().getAuthenticatorId(); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "getAuthenticatorIdForUpdateClient: exception!", e); + } + return 0L; } public void setActiveGroup(int userId, String absolutePath) throws RemoteException { - mSession.get().setActiveGroup(userId, absolutePath); + try { + mSession.get().setActiveGroup(userId, absolutePath); + } catch (RemoteException re) { + throw re; + } catch (Exception e) { + Slog.e(TAG, "setActiveGroup: exception!", e); + } } private void setCallback(AidlResponseHandler aidlResponseHandler) { @@ -235,6 +293,8 @@ private void setCallback(AidlResponseHandler aidlResponseHandler) { } } catch (RemoteException e) { Slog.d(TAG, "Failed to set callback"); + } catch (Exception e) { + Slog.e(TAG, "setCallback: exception!", e); } } @@ -247,6 +307,8 @@ public void cancel() throws RemoteException { mSession.get().cancel(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when requesting cancel", e); + } catch (Exception e) { + Slog.e(TAG, "cancel: exception!", e); } } From a4f126357ad8de8cbd2db2ed7451d496b676c12c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:29:14 +0800 Subject: [PATCH 0531/1315] QSAnimator: fix random crash 06-18 18:54:10.747 2012 2012 E QSAnimator: Trying to create animators for empty page 2. Tiles: [internet, bt, flashlight, dnd, alarm, airplane, controls, wallet, rotation, battery, cast, screenrecord, mictoggle, cameratoggle, custom(com.android.permissioncontroller/.permission.service.v33.SafetyCenterQsTileService), nfc, dark] 06-18 18:54:10.747 2012 2012 D AndroidRuntime: Shutting down VM 06-18 18:54:10.748 2012 2012 E AndroidRuntime: FATAL EXCEPTION: main 06-18 18:54:10.748 2012 2012 E AndroidRuntime: Process: com.android.systemui, PID: 2012 06-18 18:54:10.748 2012 2012 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.systemui.qs.TouchAnimator$Builder.addFloat(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:153) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.systemui.qs.QSAnimator.addNonFirstPageAnimators(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:274) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.systemui.qs.QSAnimator.onPageChanged(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:20) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at androidx.viewpager.widget.ViewPager.onPageScrolled(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:175) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at androidx.viewpager.widget.ViewPager.pageScrolled(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:60) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at androidx.viewpager.widget.ViewPager.scrollToItem(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:307) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at androidx.viewpager.widget.ViewPager.setCurrentItemInternal(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:150) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.systemui.qs.PagedTileLayout.setCurrentItem(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:20) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.systemui.qs.QSPanel$$ExternalSyntheticLambda2.run(go/retraceme ebada943079844490f5faef378f5e1e2a2d320424389cf32578076b01f7391dd:6) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:991) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:102) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8934) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591) 06-18 18:54:10.748 2012 2012 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911) Change-Id: I2ca99915f8dcb68046862d14fa5c819570671297 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/systemui/qs/QSAnimator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index 0cb695cc3553..cc8f68e6fc44 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -221,7 +221,7 @@ public void onViewDetachedFromWindow(@NonNull View v) { private void addNonFirstPageAnimators(int page) { Pair pair = createSecondaryPageAnimators(page); - if (pair != null) { + if (pair != null && pair.second != null) { // pair is null in one of two cases: // * mPagedTileLayout is null, meaning we are still setting up. // * the page has no tiles @@ -493,6 +493,9 @@ private Pair createSecondaryPageAnimator for (int i = 0; i < specs.size(); i++) { QSTileView tileView = mQsPanelController.getTileView(specs.get(i)); + if (tileView == null) { + return null; + } getRelativePosition(mTmpLoc2, tileView, view); int diff = mTmpLoc2[1] - (mQQSTop + qqsLayout.getPhantomTopPosition(i)); builder.addFloat(tileView, "translationY", -diff, 0); From 4a9ce90f5e8622463a4c22c29716b09675379d41 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 17 Jan 2025 16:31:54 +0800 Subject: [PATCH 0532/1315] SettingsProvider: Fix NPE when upgrading * Seems AOSP miss. Log: 02-06 09:15:36.049 2082 2082 E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'com.android.providers.settings.SettingsState$Setting com.android.providers.settings.SettingsState.getSettingLocked(java.lang.String)' on a null object reference 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at com.android.providers.settings.SettingsProvider$SettingsRegistry$UpgradeController.onUpgradeLocked(SettingsProvider.java:4345) 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at com.android.providers.settings.SettingsProvider$SettingsRegistry$UpgradeController.upgradeIfNeededLocked(SettingsProvider.java:3957) 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at com.android.providers.settings.SettingsProvider$SettingsRegistry.migrateAllLegacySettingsIfNeededLocked(SettingsProvider.java:3592) 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at com.android.providers.settings.SettingsProvider$SettingsRegistry.-$$Nest$mmigrateAllLegacySettingsIfNeededLocked(SettingsProvider.java:0) 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at com.android.providers.settings.SettingsProvider.onCreate(SettingsProvider.java:414) 02-06 09:15:36.049 2082 2082 E AndroidRuntime: at android.content.ContentProvider.attachInfo(ContentProvider.java:2649) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../providers/settings/SettingsProvider.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index b3e7a4aaeb93..f17021949a34 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -4770,17 +4770,19 @@ private int onUpgradeLocked(int userId, int deviceId, int oldVersion, int newVer } final SettingsState ssaidSettings = getSsaidSettingsLocked(userId); - for (PackageInfo info : packages) { - // Check if the UID already has an entry in the table. - final String uid = Integer.toString(info.applicationInfo.uid); - final Setting ssaid = ssaidSettings.getSettingLocked(uid); - - if (ssaid.isNull() || ssaid.getValue() == null) { - // Android Id doesn't exist for this package so create it. - ssaidSettings.insertSettingOverrideableByRestoreLocked( - uid, legacySsaid, null, true, info.packageName); - if (DEBUG) { - Slog.d(LOG_TAG, "Keep the legacy ssaid for uid=" + uid); + if (ssaidSettings != null) { + for (PackageInfo info : packages) { + // Check if the UID already has an entry in the table. + final String uid = Integer.toString(info.applicationInfo.uid); + final Setting ssaid = ssaidSettings.getSettingLocked(uid); + + if (ssaid.isNull() || ssaid.getValue() == null) { + // Android Id doesn't exist for this package so create it. + ssaidSettings.insertSettingOverrideableByRestoreLocked( + uid, legacySsaid, null, true, info.packageName); + if (DEBUG) { + Slog.d(LOG_TAG, "Keep the legacy ssaid for uid=" + uid); + } } } } From 296e23e9d93729aaf8e279b0b0457fe934fb243e Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 30 May 2024 22:36:33 +0800 Subject: [PATCH 0533/1315] TelephonyManager: Gracefully handle null telephony service [2] --------- beginning of crash 05-30 17:19:07.775 7184 7418 E AndroidRuntime: FATAL EXCEPTION: pool-2-thread-1 05-30 17:19:07.775 7184 7418 E AndroidRuntime: Process: com.google.android.connectivitythermalpowermanager, PID: 7184 05-30 17:19:07.775 7184 7418 E AndroidRuntime: java.lang.IllegalStateException: telephony service is null. 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at android.telephony.TelephonyManager.getAllowedNetworkTypesForReason(TelephonyManager.java:9681) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at com.google.android.connectivitythermalpowermanager.power.cellular.decision.PowerManagerDecision.internalApplyCriteriaAllowedNetworkTypes(PowerManagerDecision.java:928) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at com.google.android.connectivitythermalpowermanager.power.cellular.decision.PowerManagerDecision.applyCriteriaAllowedNetworkTypesWithRetry(PowerManagerDecision.java:888) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at com.google.android.connectivitythermalpowermanager.power.cellular.decision.PowerManagerDecision.lambda$applyCriteriaAllowedNetworkTypes$4(PowerManagerDecision.java:905) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at com.google.android.connectivitythermalpowermanager.power.cellular.decision.PowerManagerDecision.$r8$lambda$YOnForDFleFGD2Y5mI1VezN2WgU(PowerManagerDecision.java:0) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at com.google.android.connectivitythermalpowermanager.power.cellular.decision.PowerManagerDecision$$ExternalSyntheticLambda7.run(R8$$SyntheticClass:0) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) 05-30 17:19:07.775 7184 7418 E AndroidRuntime: at java.lang.Thread.run(Thread.java:1012) reference: https://github.com/RisingOS-staging/android_packages_apps_Settings/blob/1e59ca72a0d3a67e86be2d96ec6486189beca50d/src/com/android/settings/network/telephony/cdma/CdmaSystemSelectPreferenceController.java#L69 https://github.com/RisingOS-staging/android_packages_apps_Settings/blob/1e59ca72a0d3a67e86be2d96ec6486189beca50d/src/com/android/settings/network/telephony/PreferredNetworkModePreferenceController.java#L106 Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- telephony/java/android/telephony/TelephonyManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index b12a42b9a187..9f7a67c49a8d 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -10285,7 +10285,8 @@ public void setAllowedNetworkTypesForReason(@AllowedNetworkTypesReason int reaso if (telephony != null) { return telephony.getAllowedNetworkTypesForReason(getSubId(), reason); } else { - throw new IllegalStateException("telephony service is null."); + Rlog.d(TAG, "telephony service is null."); + return -1; // NETWORK_MODE_UNKNOWN } } catch (RemoteException ex) { Rlog.e(TAG, "getAllowedNetworkTypesForReason RemoteException", ex); From e02da11858f2970fb184af2434b6087a2779370e Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 23 Jul 2024 07:15:40 +0800 Subject: [PATCH 0534/1315] Telephony: Gracefully handle data enablement checks 07-23 07:16:21.271 E/TelephonyManager(5971): Error calling #isDataEnabled, returning default (false). 07-23 07:16:21.271 E/TelephonyManager(5971): java.lang.IllegalStateException: telephony service is null. 07-23 07:16:21.271 E/TelephonyManager(5971): at android.telephony.TelephonyManager.isDataEnabledForReason(TelephonyManager.java:14334) 07-23 07:16:21.271 E/TelephonyManager(5971): at android.telephony.TelephonyManager.isDataEnabledForReason(TelephonyManager.java:14325) 07-23 07:16:21.271 E/TelephonyManager(5971): at android.telephony.TelephonyManager.isDataEnabled(TelephonyManager.java:11660) 07-23 07:16:21.271 E/TelephonyManager(5971): at fmvz.a(PG:23) 07-23 07:16:21.271 E/TelephonyManager(5971): at fnlc.call(PG:2) 07-23 07:16:21.271 E/TelephonyManager(5971): at java.util.concurrent.FutureTask.run(FutureTask.java:264) 07-23 07:16:21.271 E/TelephonyManager(5971): at fibt.run(PG:1) 07-23 07:16:21.271 E/TelephonyManager(5971): at iino.run(PG:23) 07-23 07:16:21.271 E/TelephonyManager(5971): at fiaw.run(PG:2) 07-23 07:16:21.271 E/TelephonyManager(5971): at java.lang.Thread.run(Thread.java:1012) 07-23 07:16:21.271 E/TelephonyManager(5971): at fiby.run(PG:6) Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- telephony/java/android/telephony/TelephonyManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9f7a67c49a8d..8aaba4d45034 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -14498,7 +14498,8 @@ private boolean isDataEnabledForReason(int subId, @DataEnabledReason int reason) if (service != null) { return service.isDataEnabledForReason(subId, reason); } else { - throw new IllegalStateException("telephony service is null."); + Log.d(TAG, "Telephony service is null."); + return false; } } catch (RemoteException ex) { Log.e(TAG, "Telephony#isDataEnabledForReason RemoteException", ex); From cfb9e47d27f060d642ed4f5eef70ba174562aee0 Mon Sep 17 00:00:00 2001 From: pix106 Date: Sat, 19 Oct 2024 15:46:37 +0200 Subject: [PATCH 0535/1315] TelephonyManager: Gracefully handle null telephony service - isNullCipherNotificationsEnabled Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- telephony/java/android/telephony/TelephonyManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 8aaba4d45034..a0baf495889f 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -19261,7 +19261,8 @@ public boolean isNullCipherNotificationsEnabled() { if (telephony != null) { return telephony.isNullCipherNotificationsEnabled(); } else { - throw new IllegalStateException("telephony service is null."); + Log.d(TAG, "Telephony service is null."); + return false; } } catch (RemoteException ex) { Rlog.e(TAG, "isNullCipherNotificationsEnabled RemoteException", ex); From 56371ddfb0c485cf7790e6e4ff4deb400cef9192 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 23 Feb 2025 23:35:49 +0530 Subject: [PATCH 0536/1315] SystemUI: Hide QR code scanner tile if not launch activity available Fixes: https://github.com/crdroidandroid/issue_tracker/issues/650 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/qs/tiles/QRCodeScannerTile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java index 467233d5f71f..45434fcb813c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java @@ -138,7 +138,7 @@ public int getMetricsCategory() { @Override public boolean isAvailable() { - return mQRCodeScannerController.isCameraAvailable(); + return mQRCodeScannerController.isAbleToLaunchScannerActivity(); } @Nullable From 2cfd3561200dc007b8efabda3586b7735eb30d4d Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:36:40 +0800 Subject: [PATCH 0537/1315] fixing ShadeDialogContextRepo crash 8:03.430 2440 2440 E ShadeDialogContextRepo: Couldn't get dialog context for displayId=0. Returning default one 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: java.lang.IllegalStateException: This should be instantiated only when either StatusBarConnectedDisplays or ShadeWindowGoesAround or cursorHotCorner are enabled. 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl.(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:35) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get3(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:715) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:96) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at dagger.internal.DoubleCheck.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:14) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContextOrDefault(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:46) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContext(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:33) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl.getContext$1(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:3) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1$$ExternalSyntheticLambda0.invoke(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:14) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.animation.ActivityTransitionAnimator.startIntentWithAnimation(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:307) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1.run(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:80) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.handleCallback(Handler.java:1041) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.dispatchMessage(Handler.java:103) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.dispatchMessage(Looper.java:315) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loopOnce(Looper.java:251) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loop(Looper.java:349) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at android.app.ActivityThread.main(ActivityThread.java:9067) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at java.lang.reflect.Method.invoke(Native Method) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 01-06 12:58:03.430 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: Couldn't get dialog context for displayId=0. Returning default one 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: java.lang.IllegalStateException: This should be instantiated only when either StatusBarConnectedDisplays or ShadeWindowGoesAround or cursorHotCorner are enabled. 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl.(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:35) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get3(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:715) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:96) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at dagger.internal.DoubleCheck.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:14) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContextOrDefault(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:46) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContext(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:33) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl.getContext$1(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:3) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1$$ExternalSyntheticLambda0.invoke(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:84) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.animation.ActivityTransitionAnimator.startIntentWithAnimation(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:307) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1.run(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:80) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.handleCallback(Handler.java:1041) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.dispatchMessage(Handler.java:103) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.dispatchMessage(Looper.java:315) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loopOnce(Looper.java:251) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loop(Looper.java:349) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at android.app.ActivityThread.main(ActivityThread.java:9067) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at java.lang.reflect.Method.invoke(Native Method) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 01-06 12:58:03.434 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: Couldn't get dialog context for displayId=0. Returning default one 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: java.lang.IllegalStateException: This should be instantiated only when either StatusBarConnectedDisplays or ShadeWindowGoesAround or cursorHotCorner are enabled. 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl.(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:35) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get3(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:715) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:96) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at dagger.internal.DoubleCheck.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:14) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContextOrDefault(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:46) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContext(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:33) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl.getContext$1(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:3) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1$$ExternalSyntheticLambda0.invoke(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:92) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.animation.ActivityTransitionAnimator.startIntentWithAnimation(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:307) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1.run(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:80) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.handleCallback(Handler.java:1041) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.dispatchMessage(Handler.java:103) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.dispatchMessage(Looper.java:315) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loopOnce(Looper.java:251) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loop(Looper.java:349) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at android.app.ActivityThread.main(ActivityThread.java:9067) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at java.lang.reflect.Method.invoke(Native Method) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 01-06 12:58:03.438 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: Couldn't get dialog context for displayId=0. Returning default one 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: java.lang.IllegalStateException: This should be instantiated only when either StatusBarConnectedDisplays or ShadeWindowGoesAround or cursorHotCorner are enabled. 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl.(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:35) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get3(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:715) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.dagger.DaggerReferenceGlobalRootComponent$ReferenceSysUIComponentImpl$SwitchingProvider.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:96) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at dagger.internal.DoubleCheck.get(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:14) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContextOrDefault(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:46) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl.getContext(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:33) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl.getContext$1(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:3) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1$$ExternalSyntheticLambda0.invoke(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:100) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.animation.ActivityTransitionAnimator.startIntentWithAnimation(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:307) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.systemui.statusbar.phone.LegacyActivityStarterInternalImpl$startActivityDismissingKeyguard$runnable$1.run(go/retraceme 72c579ceb01122ff1cb014d1b6f305a3c734b2a386d5105a246f105cfb7379b4:80) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.handleCallback(Handler.java:1041) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.os.Handler.dispatchMessage(Handler.java:103) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.dispatchMessage(Looper.java:315) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loopOnce(Looper.java:251) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.os.Looper.loop(Looper.java:349) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at android.app.ActivityThread.main(ActivityThread.java:9067) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at java.lang.reflect.Method.invoke(Native Method) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 01-06 12:58:03.442 2440 2440 E ShadeDialogContextRepo: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) Change-Id: I0819895e20ee4abc326920f84f741bb01cdbc070 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../shade/domain/interactor/ShadeDialogContextInteractor.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt index 18854b9cdbb5..c7192d693c5e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt @@ -84,6 +84,10 @@ constructor( } private fun getContextOrDefault(displayId: Int): Context { + if (displayId == Display.DEFAULT_DISPLAY) { + return defaultContext + } + return try { traceSection({ "Getting dialog context for displayId=$displayId" }) { val displayWindowProperties = From 76ac013996735453bf3eea64aca16f8ee6c2c8fa Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:02:31 +0800 Subject: [PATCH 0538/1315] fixing app directory access Change-Id: I480874825319e35d340fc719f8c729bc1a33ed97 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ContextImpl.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 2e3c93a27d9f..72804256ef40 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -3921,6 +3921,11 @@ private File[] ensureExternalDirsExistOrFilter(File[] dirs, boolean tryCreateInP final File[] result = new File[dirs.length]; for (int i = 0; i < dirs.length; i++) { File dir = dirs[i]; + if (dir == null) { + // If input was null, we can't do anything + result[i] = null; + continue; + } if (!dir.exists()) { try { if (!tryCreateInProcess || !dir.mkdirs()) { @@ -3933,10 +3938,11 @@ private File[] ensureExternalDirsExistOrFilter(File[] dirs, boolean tryCreateInP } } catch (Exception e) { Log.w(TAG, "Failed to ensure " + dir, e); - dir = null; + // Do not null out the dir, so we can still return the path + // effectively preventing the NPE downstream } } - if (dir != null && !dir.canWrite()) { + if (!dir.canWrite()) { // Older versions of the MediaProvider mainline module had a rare early boot race // condition where app-private dirs could be created with the wrong permissions; // fix this up here. This check should be very fast, because dir.exists() above From 311c1ac0f48b8f0dcb56d9672f526c42f541eb34 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 12 Apr 2022 22:40:17 +0530 Subject: [PATCH 0539/1315] SystemUI: Update default tiles as per usability * Added NFC tile to default list for supported devicesp Change-Id: I0265865dd6a594889e2f25a3adb499d3a9dc0808 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/config.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 851925ed6fdb..1901126b02ce 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -102,12 +102,12 @@ - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,custom(com.android.permissioncontroller/.permission.service.v33.SafetyCenterQsTileService) + internet,bt,flashlight,dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,custom(com.android.permissioncontroller/.permission.service.v33.SafetyCenterQsTileService) - internet,bt,dnd,cast,flashlight,airplane,rotation,wallet,alarm,controls,screenrecord,battery + internet,bt,flashlight,dnd,alarm,airplane,nfc,rotation,battery,screenrecord,cast,wallet,controls @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn From 820ff653cda34cf0ad880bc92e93796a3e76e349 Mon Sep 17 00:00:00 2001 From: Jason Edson Date: Sun, 17 Jun 2018 17:44:52 -0700 Subject: [PATCH 0540/1315] SystemUI: QS: Add On-The-Go Tile @neobuddy89: Updated for A12, A13, A14, A16 Squashed: From: ShevT Date: Wed, 23 Feb 2022 22:37:52 +0300 Subject: OnTheGoTile: Fixed refresh state when turning off tile Test: 1) Click on the tile. The On-The-Go mode is activated. The tile becomes active. 2) Click on the tile again. On-The-Go mode turns off. The tile remains active until we close quick settings and reopen. Let's fix this. Change-Id: Ic345f83723a48e3a7d610926fb9b47fab7ef878f Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_qs_onthego.xml | 14 ++ packages/SystemUI/res/values/config.xml | 2 +- .../android/systemui/lineage/LineageModule.kt | 22 +++ .../systemui/qs/tiles/OnTheGoTile.java | 134 ++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_onthego.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_onthego.xml b/packages/SystemUI/res/drawable/ic_qs_onthego.xml new file mode 100644 index 000000000000..d05649849b51 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_onthego.xml @@ -0,0 +1,14 @@ + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 1901126b02ce..a0e65add4951 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 5a0cb1013d88..9f753ab8dcae 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -24,6 +24,7 @@ import com.android.systemui.qs.tiles.AmbientDisplayTile import com.android.systemui.qs.tiles.AODTile import com.android.systemui.qs.tiles.CaffeineTile import com.android.systemui.qs.tiles.HeadsUpTile +import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile @@ -66,6 +67,12 @@ interface LineageModule { @StringKey(HeadsUpTile.TILE_SPEC) fun bindHeadsUpTile(headsUpTile: HeadsUpTile): QSTileImpl<*> + /** Inject OnTheGoTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(OnTheGoTile.TILE_SPEC) + fun bindOnTheGoTile(onTheGoTile: OnTheGoTile): QSTileImpl<*> + /** Inject PowerShareTile into tileMap in QSModule */ @Binds @IntoMap @@ -174,6 +181,21 @@ interface LineageModule { category = TileCategory.ACCESSIBILITY, ) + @Provides + @IntoMap + @StringKey(OnTheGoTile.TILE_SPEC) + fun provideOnTheGoTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(OnTheGoTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_onthego, + labelRes = R.string.global_action_onthego + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.ACCESSIBILITY, + ) + @Provides @IntoMap @StringKey(POWERSHARE_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java new file mode 100644 index 000000000000..0e23bba4c8e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 Benzo Rom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.ComponentName; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.systemui.animation.Expandable; +import com.android.systemui.crdroid.onthego.OnTheGoService; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.matrixx.OnTheGoUtils; + +import javax.inject.Inject; + +/** Quick settings tile: Enable/Disable OnTheGo Mode **/ +public class OnTheGoTile extends QSTileImpl { + + public static final String TILE_SPEC = "onthego"; + + @Nullable + private Icon mIcon = null; + private boolean mIsEnabled; + + @Inject + public OnTheGoTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mIsEnabled = isOnTheGoEnabled(); + } + + @Override + public BooleanState newTileState() { + BooleanState state = new BooleanState(); + state.handlesLongClick = false; + return state; + } + + @Override + public void handleSetListening(boolean listening) { + // nothing + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + ComponentName cn = new ComponentName("com.android.systemui", + "com.android.systemui.crdroid.onthego.OnTheGoService"); + Intent startIntent = new Intent(); + startIntent.setComponent(cn); + if (isOnTheGoEnabled()) { + startIntent.setAction("stop"); + mIsEnabled = false; + } else { + startIntent.setAction("start"); + mIsEnabled = true; + } + mContext.startService(startIntent); + refreshState(); + } + + protected boolean isOnTheGoEnabled() { + String service = OnTheGoService.class.getName(); + return OnTheGoUtils.isServiceRunning(mContext, service); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.global_action_onthego); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.value = mIsEnabled; + state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.label = mContext.getString(R.string.global_action_onthego); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_onthego); + } + state.icon = mIcon; + state.contentDescription = state.label; + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } +} From 3567787925de6621c161841341701f699db3acb8 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 15 Jul 2025 01:21:50 +0530 Subject: [PATCH 0541/1315] SystemUI: OnTheGo: Update tile more dynamically Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java index 0e23bba4c8e0..0f802734191e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OnTheGoTile.java @@ -117,7 +117,7 @@ public CharSequence getTileLabel() { @Override protected void handleUpdateState(BooleanState state, Object arg) { - state.value = mIsEnabled; + state.value = mIsEnabled && isOnTheGoEnabled(); state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; state.label = mContext.getString(R.string.global_action_onthego); if (mIcon == null) { From 88c9396425554023ff90a778bf8aedfe13666874 Mon Sep 17 00:00:00 2001 From: Yoshinori Hirano Date: Sat, 16 Oct 2021 10:02:58 +0000 Subject: [PATCH 0542/1315] SystemUI: Add Sound tile to Quick Settings [ghostrider-reborn: misc improvements and cleanup] Squashed commit of the following: commit 69f86d9a38b5a942e3f9c914895ee45c0079e8e9 Author: maxwen Date: Fri Apr 13 03:32:51 2018 +0200 base: SystemUI: stop crashing qs tiles during boot this triggers my ocd continues a579c2430f1a0bf088023b4283dcec37776c070d Change-Id: I31719b595cb50a330f26e25074243e04c06f63c1 Signed-off-by: xyyx commit f419619c033fa99a809f53aa82dbdd7d6732177c Author: xyyx Date: Thu Sep 28 11:55:36 2017 +0800 SoundTile: Change ZEN_MODE_NO_INTERRUPTIONS to ZEN_MODE_ALARMS Change-Id: I5edaaa4d551630049b29e09d3cecfb5ffd503e99 commit 916645271f57c47d60bb0a04c4e51776b51a18f1 Author: Yoshinori Hirano Date: Sun Sep 18 22:33:08 2016 +0200 Add Sound tile to Quick Settings abc ezio84: adapt to O, show volume panel with long press, use full dnd (no interruptions) as silent mode beanstown106: Sound Tile improvements *added longpress action *added more descriptive labels for each state instead of always saying sound ezio84: Adapt for N Change-Id: I46f4f8cc62683144a9bd714ca67e2a0f46940d96 Change-Id: I178bfa69ff181f0f65f94ffae1444aaea5f21a80 Signed-off-by: Adithya R Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_qs_ringer_audible.xml | 28 +++ .../res/drawable/ic_qs_ringer_silent.xml | 31 ++++ .../res/drawable/ic_qs_ringer_vibrate.xml | 25 +++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 6 + .../android/systemui/lineage/LineageModule.kt | 22 +++ .../android/systemui/qs/tiles/SoundTile.java | 159 ++++++++++++++++++ 7 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_ringer_audible.xml create mode 100644 packages/SystemUI/res/drawable/ic_qs_ringer_silent.xml create mode 100644 packages/SystemUI/res/drawable/ic_qs_ringer_vibrate.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_ringer_audible.xml b/packages/SystemUI/res/drawable/ic_qs_ringer_audible.xml new file mode 100644 index 000000000000..29741577c039 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_ringer_audible.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_ringer_silent.xml b/packages/SystemUI/res/drawable/ic_qs_ringer_silent.xml new file mode 100644 index 000000000000..6db508cd31e8 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_ringer_silent.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_ringer_vibrate.xml b/packages/SystemUI/res/drawable/ic_qs_ringer_vibrate.xml new file mode 100644 index 000000000000..c87b595a20f2 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_ringer_vibrate.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index a0e65add4951..dca3b0548e99 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 1df4664620ad..3db103d7cad3 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -109,4 +109,10 @@ Lens + + + Sound + Ring + Vibrate + Silent diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 9f753ab8dcae..5a392053ae28 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile +import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile import com.android.systemui.qs.tiles.VpnTile @@ -91,6 +92,12 @@ interface LineageModule { @StringKey(ReadingModeTile.TILE_SPEC) fun bindReadingModeTile(readingModeTile: ReadingModeTile): QSTileImpl<*> + /** Inject SoundTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(SoundTile.TILE_SPEC) + fun bindSoundTile(soundTile: SoundTile): QSTileImpl<*> + /** Inject SyncTile into tileMap in QSModule */ @Binds @IntoMap @@ -241,6 +248,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(SoundTile.TILE_SPEC) + fun provideSoundConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(SoundTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_ringer_audible, + labelRes = R.string.quick_settings_sound_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(SYNC_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java new file mode 100644 index 000000000000..2fc142626ee8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.res.R; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; + +import javax.inject.Inject; + +public class SoundTile extends QSTileImpl { + + public static final String TILE_SPEC = "sound"; + + private boolean mListening = false; + + private final AudioManager mAudioManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + refreshState(); + } + }; + + @Inject + public SoundTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + if (mAudioManager == null) { + return; + } + if (mListening == listening) return; + mListening = listening; + if (listening) { + final IntentFilter filter = new IntentFilter( + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + mContext.registerReceiver(mReceiver, filter); + } else { + mContext.unregisterReceiver(mReceiver); + } + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + int oldState = mAudioManager.getRingerModeInternal(); + int newState = oldState; + switch (oldState) { + case AudioManager.RINGER_MODE_NORMAL: + newState = AudioManager.RINGER_MODE_VIBRATE; + break; + case AudioManager.RINGER_MODE_VIBRATE: + newState = AudioManager.RINGER_MODE_SILENT; + break; + case AudioManager.RINGER_MODE_SILENT: + newState = AudioManager.RINGER_MODE_NORMAL; + break; + } + mAudioManager.setRingerModeInternal(newState); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(Settings.ACTION_SOUND_SETTINGS); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_sound_label); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.label = getTileLabel(); + if (mAudioManager == null) { + return; + } + switch (mAudioManager.getRingerModeInternal()) { + case AudioManager.RINGER_MODE_NORMAL: + state.icon = maybeLoadResourceIcon(R.drawable.ic_qs_ringer_audible); + state.secondaryLabel = mContext.getString(R.string.quick_settings_sound_ring); + state.state = Tile.STATE_ACTIVE; + break; + case AudioManager.RINGER_MODE_VIBRATE: + state.icon = maybeLoadResourceIcon(R.drawable.ic_qs_ringer_vibrate); + state.secondaryLabel = mContext.getString(R.string.quick_settings_sound_vibrate); + state.state = Tile.STATE_INACTIVE; + break; + case AudioManager.RINGER_MODE_SILENT: + state.icon = maybeLoadResourceIcon(R.drawable.ic_qs_ringer_silent); + state.secondaryLabel = mContext.getString(R.string.quick_settings_sound_silent); + state.state = Tile.STATE_INACTIVE; + break; + } + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } +} From 4f4f47bb9dbd1eb136af3ef778bb55dc19a81d6d Mon Sep 17 00:00:00 2001 From: Jayant-Deshmukh Date: Tue, 10 Dec 2024 13:30:27 +0530 Subject: [PATCH 0543/1315] SoundTile: Add DOUBLE_CLICK effect for vibrate mode [neobuddy89: Do not re-initialize vibtor if already done] Change-Id: I263dde1b4fbe1c6ec6137a247d9804b3fa5eaaae Signed-off-by: Jayant-Deshmukh Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/qs/tiles/SoundTile.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java index 2fc142626ee8..4120af26f75a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SoundTile.java @@ -23,6 +23,8 @@ import android.media.AudioManager; import android.os.Handler; import android.os.Looper; +import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.Settings; import android.service.quicksettings.Tile; @@ -61,6 +63,10 @@ public void onReceive(Context context, Intent intent) { } }; + private Vibrator mVibrator; + private static final VibrationEffect VIBRATE_MODE_HAPTIC = + VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + @Inject public SoundTile( QSHost host, @@ -106,6 +112,10 @@ protected void handleClick(@Nullable Expandable expandable) { switch (oldState) { case AudioManager.RINGER_MODE_NORMAL: newState = AudioManager.RINGER_MODE_VIBRATE; + if (mVibrator == null) mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + if (mVibrator != null) { + if (mVibrator.hasVibrator()) { mVibrator.vibrate(VIBRATE_MODE_HAPTIC); } + } break; case AudioManager.RINGER_MODE_VIBRATE: newState = AudioManager.RINGER_MODE_SILENT; From 4b0d68f626fd82bc851a3650d6bbd9f2d153fda8 Mon Sep 17 00:00:00 2001 From: maxwen Date: Fri, 27 Sep 2019 23:13:28 +0200 Subject: [PATCH 0544/1315] base: add CPU info overlay includes all modifications and fixes from P @neobuddy89: Remove redundant DreamManager code Change-Id: I0e6dee53ebcd864a522cfd3d78a5abb253991a9c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + packages/SystemUI/AndroidManifest.xml | 9 + packages/SystemUI/res/values/cr_config.xml | 9 + .../com/android/systemui/BootReceiver.java | 84 ++++ .../com/android/systemui/CPUInfoService.java | 439 ++++++++++++++++++ 5 files changed, 547 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/BootReceiver.java create mode 100644 packages/SystemUI/src/com/android/systemui/CPUInfoService.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0ce8a719be88..8d42678c17ad 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14209,6 +14209,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String MEDIA_SQUIGGLE_ANIMATION = "media_squiggle_animation"; + /** + * Control whether the process CPU info meter should be shown. + * @hide + */ + public static final String SHOW_CPU_OVERLAY = "show_cpu_overlay"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 104128f3df94..6aaf9c347d63 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -596,6 +596,15 @@ android:enabled="true" android:exported="true" /> + + + + + + + + false + + + /sys/class/thermal/thermal_zone0/temp + + + 1 + + + diff --git a/packages/SystemUI/src/com/android/systemui/BootReceiver.java b/packages/SystemUI/src/com/android/systemui/BootReceiver.java new file mode 100644 index 000000000000..768e8eaf7c20 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/BootReceiver.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; +import android.util.Log; + +/** + * Performs a number of miscellaneous, non-system-critical actions + * after the system has finished booting. + */ +public class BootReceiver extends BroadcastReceiver { + private static final String TAG = "SystemUIBootReceiver"; + private Handler mHandler = new Handler(); + private SettingsObserver mSettingsObserver; + private Context mContext; + + private class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + void observe() { + mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SHOW_CPU_OVERLAY), + false, this); + update(); + } + + @Override + public void onChange(boolean selfChange) { + update(); + } + + public void update() { + Intent cpuinfo = new Intent(mContext, com.android.systemui.CPUInfoService.class); + if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.SHOW_CPU_OVERLAY, 0) != 0) { + mContext.startService(cpuinfo); + } else { + mContext.stopService(cpuinfo); + } + } + } + + @Override + public void onReceive(final Context context, Intent intent) { + try { + mContext = context; + if (mSettingsObserver == null) { + mSettingsObserver = new SettingsObserver(mHandler); + mSettingsObserver.observe(); + } + + // Start the cpu info overlay, if activated + if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.SHOW_CPU_OVERLAY, 0) != 0) { + Intent cpuinfo = new Intent(mContext, com.android.systemui.CPUInfoService.class); + mContext.startService(cpuinfo); + } + + } catch (Exception e) { + Log.e(TAG, "Can't start load average service", e); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/CPUInfoService.java b/packages/SystemUI/src/com/android/systemui/CPUInfoService.java new file mode 100644 index 000000000000..05dfc1f1eadf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CPUInfoService.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.util.Log; + +import com.android.systemui.res.R; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.StringBuffer; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CPUInfoService extends Service { + private View mView; + private Thread mCurCPUThread; + private final String TAG = "CPUInfoService"; + private int mNumCpus = 2; + private String[] mCpu = null; + private String[] mCurrFreq = null; + private String[] mCurrGov = null; + + private int CPU_TEMP_DIVIDER = 1; + private String CPU_TEMP_SENSOR = ""; + private String DISPLAY_CPUS = ""; + private boolean mCpuTempAvail; + + private static final String NUM_OF_CPUS_PATH = "/sys/devices/system/cpu/present"; + private static final String CURRENT_CPU = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"; + private static final String CPU_ROOT = "/sys/devices/system/cpu/cpu"; + private static final String CPU_CUR_TAIL = "/cpufreq/scaling_cur_freq"; + private static final String CPU_GOV_TAIL = "/cpufreq/scaling_governor"; + + private class CPUView extends View { + private Paint mOnlinePaint; + private Paint mOfflinePaint; + private float mAscent; + private int mFH; + private int mMaxWidth; + + private int mNeededWidth; + private int mNeededHeight; + private String mCpuTemp; + + private boolean mDataAvail; + + private Handler mCurCPUHandler = new Handler() { + public void handleMessage(Message msg) { + if(msg.obj==null){ + return; + } + if(msg.what==1){ + String msgData = (String) msg.obj; + try { + String[] parts=msgData.split(";"); + mCpuTemp=parts[0]; + + String[] cpuParts=parts[1].split("\\|"); + for(int i=0; i 1) { + return String.format("%s", + Integer.parseInt(cpuTemp) / CPU_TEMP_DIVIDER); + } else { + return cpuTemp; + } + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (!mDataAvail) { + return; + } + + final int W = mNeededWidth; + final int RIGHT = getWidth()-1; + + int x = RIGHT - mPaddingRight; + int top = mPaddingTop + 2; + int bottom = mPaddingTop + mFH - 2; + + int y = mPaddingTop - (int)mAscent; + + if(!mCpuTemp.equals("0")) { + canvas.drawText("Temp: " + getCpuTemp(mCpuTemp) + "°C", + RIGHT-mPaddingRight-mMaxWidth, y-1, mOnlinePaint); + y += mFH; + } + + for(int i=0; i 0) { + numOfCpu = cpuList.length; + mCpu = new String[numOfCpu]; + + for (int i = 0; i < numOfCpu; i++) { + try { + int cpu = Integer.parseInt(cpuList[i]); + mCpu[i] = cpuList[i]; + } catch (NumberFormatException ex) { + // derped overlay + return getCpus(null); + } + } + } else { + // derped overlay + return getCpus(null); + } + } else { + // empty overlay, take all cores + String numOfCpus = readOneLine(NUM_OF_CPUS_PATH); + cpuList = numOfCpus.split("-"); + if (cpuList.length > 1) { + try { + int cpuStart = Integer.parseInt(cpuList[0]); + int cpuEnd = Integer.parseInt(cpuList[1]); + + numOfCpu = cpuEnd - cpuStart + 1; + + if (numOfCpu < 0) + numOfCpu = 1; + } catch (NumberFormatException ex) { + numOfCpu = 1; + } + } + + mCpu = new String[numOfCpu]; + for (int i = 0; i < numOfCpu; i++) + { + mCpu[i] = String.valueOf(i); + } + } + return numOfCpu; + } + + private BroadcastReceiver mScreenStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { + Log.d(TAG, "ACTION_SCREEN_ON "); + startThread(); + mView.setVisibility(View.VISIBLE); + } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { + Log.d(TAG, "ACTION_SCREEN_OFF"); + mView.setVisibility(View.GONE); + stopThread(); + } + } + }; + + private void startThread() { + Log.d(TAG, "started CurCPUThread"); + mCurCPUThread = new CurCPUThread(mView.getHandler(), mNumCpus); + mCurCPUThread.start(); + } + + private void stopThread() { + if (mCurCPUThread != null && mCurCPUThread.isAlive()) { + Log.d(TAG, "stopping CurCPUThread"); + mCurCPUThread.interrupt(); + try { + mCurCPUThread.join(); + } catch (InterruptedException e) { + } + } + mCurCPUThread = null; + } +} From d64394c74302fd950a2a460a306f80f27eadbe5b Mon Sep 17 00:00:00 2001 From: mydongistiny Date: Sat, 23 Dec 2017 22:46:44 -0800 Subject: [PATCH 0545/1315] QS: Add CPUInfo toggle tile @neobuddy89: Adapted tile for A13, A14, A16 Change-Id: I84fcdff1c29c0bcd3f85d60b4ccba25288ebbddd Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_qs_cpu_info.xml | 10 ++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 3 + .../android/systemui/lineage/LineageModule.kt | 22 +++ .../systemui/qs/tiles/CPUInfoTile.java | 145 ++++++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_cpu_info.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/CPUInfoTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_cpu_info.xml b/packages/SystemUI/res/drawable/ic_qs_cpu_info.xml new file mode 100644 index 000000000000..78738b142e9a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_cpu_info.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index dca3b0548e99..739893000dab 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 3db103d7cad3..20513bb2fdde 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -115,4 +115,7 @@ Ring Vibrate Silent + + + CPU Info diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 5a392053ae28..72148fe5b3f8 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -22,6 +22,7 @@ import com.android.systemui.qs.shared.model.TileCategory import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tiles.AmbientDisplayTile import com.android.systemui.qs.tiles.AODTile +import com.android.systemui.qs.tiles.CPUInfoTile import com.android.systemui.qs.tiles.CaffeineTile import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.OnTheGoTile @@ -56,6 +57,12 @@ interface LineageModule { @StringKey(AODTile.TILE_SPEC) fun bindAODTile(aodTile: AODTile): QSTileImpl<*> + /** Inject CPUInfoTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(CPUInfoTile.TILE_SPEC) + fun CPUInfoTile(cpuInfoTile: CPUInfoTile): QSTileImpl<*> + /** Inject CaffeineTile into tileMap in QSModule */ @Binds @IntoMap @@ -158,6 +165,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(CPUInfoTile.TILE_SPEC) + fun provideCPUInfoConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(CPUInfoTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_cpu_info, + labelRes = R.string.quick_settings_cpuinfo_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(CAFFEINE_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CPUInfoTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CPUInfoTile.java new file mode 100644 index 000000000000..ef6d551695e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CPUInfoTile.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2017-2018 Benzo Rom + * (C) 2017-2025 crDroidAndroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; +import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.settings.SettingObserver; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import javax.inject.Inject; + +/** Quick settings tile: CPUInfo overlay **/ +public class CPUInfoTile extends QSTileImpl { + + public static final String TILE_SPEC = "cpuinfo"; + + private final SettingObserver mSetting; + @Nullable + private Icon mIcon = null; + + @Inject + public CPUInfoTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + SecureSettings secureSettings) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mSetting = new SettingObserver(secureSettings, mHandler, Secure.SHOW_CPU_OVERLAY, getHost().getUserId()) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + public BooleanState newTileState() { + BooleanState state = new BooleanState(); + state.handlesLongClick = false; + return state; + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + mSetting.setValue(mState.value ? 0 : 1); + refreshState(); + toggleState(); + } + + protected void toggleState() { + Intent service = (new Intent()) + .setClassName("com.android.systemui", + "com.android.systemui.CPUInfoService"); + if (mSetting.getValue() == 0) { + mContext.stopService(service); + } else { + mContext.startService(service); + } + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + if (mSetting == null) return; + final int value = arg instanceof Integer ? (Integer)arg : mSetting.getValue(); + final boolean cpuInfoEnabled = value != 0; + state.value = cpuInfoEnabled; + state.label = mContext.getString(R.string.quick_settings_cpuinfo_label); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_cpu_info); + } + state.icon = mIcon; + state.contentDescription = mContext.getString( + R.string.quick_settings_cpuinfo_label); + if (cpuInfoEnabled) { + state.state = Tile.STATE_ACTIVE; + } else { + state.state = Tile.STATE_INACTIVE; + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_cpuinfo_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public void handleSetListening(boolean listening) { + // Do nothing + } +} From b18aee9776c9d0b78a5e1b1ce67547b8c83490e3 Mon Sep 17 00:00:00 2001 From: Marko Man Date: Tue, 10 Mar 2020 18:34:12 +0000 Subject: [PATCH 0546/1315] SystemUI: FPS Info Overlay & Tile @neobuddy89: Adapted tile for A13, A14, A16 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + packages/SystemUI/AndroidManifest.xml | 3 + .../SystemUI/res/drawable/ic_qs_fps_info.xml | 9 + packages/SystemUI/res/values/config.xml | 2 +- packages/SystemUI/res/values/cr_config.xml | 3 + .../SystemUI/res/values/matrixx_strings.xml | 3 + .../com/android/systemui/BootReceiver.java | 15 + .../com/android/systemui/FPSInfoService.java | 290 ++++++++++++++++++ .../android/systemui/lineage/LineageModule.kt | 22 ++ .../systemui/qs/tiles/FPSInfoTile.java | 157 ++++++++++ 10 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_fps_info.xml create mode 100644 packages/SystemUI/src/com/android/systemui/FPSInfoService.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8d42678c17ad..8ffb642b460e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14215,6 +14215,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String SHOW_CPU_OVERLAY = "show_cpu_overlay"; + /** + * Control whether the process FPS info meter should be shown. + * @hide + */ + public static final String SHOW_FPS_OVERLAY = "show_fps_overlay"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6aaf9c347d63..995acb6caeb3 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -599,6 +599,9 @@ + + diff --git a/packages/SystemUI/res/drawable/ic_qs_fps_info.xml b/packages/SystemUI/res/drawable/ic_qs_fps_info.xml new file mode 100644 index 000000000000..3fb9ccba74f0 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_fps_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 739893000dab..c20316c7c796 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index 6936db35fe87..50c4ac0dcaa8 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -26,4 +26,7 @@ + + + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 20513bb2fdde..9ee19152ffee 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -118,4 +118,7 @@ CPU Info + + + FPS Info diff --git a/packages/SystemUI/src/com/android/systemui/BootReceiver.java b/packages/SystemUI/src/com/android/systemui/BootReceiver.java index 768e8eaf7c20..ae4d97ea482e 100644 --- a/packages/SystemUI/src/com/android/systemui/BootReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/BootReceiver.java @@ -44,6 +44,9 @@ void observe() { mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( Settings.Secure.SHOW_CPU_OVERLAY), false, this); + mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SHOW_FPS_OVERLAY), + false, this); update(); } @@ -54,11 +57,17 @@ public void onChange(boolean selfChange) { public void update() { Intent cpuinfo = new Intent(mContext, com.android.systemui.CPUInfoService.class); + Intent fpsinfo = new Intent(mContext, com.android.systemui.FPSInfoService.class); if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.SHOW_CPU_OVERLAY, 0) != 0) { mContext.startService(cpuinfo); } else { mContext.stopService(cpuinfo); } + if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.SHOW_FPS_OVERLAY, 0) != 0) { + mContext.startService(fpsinfo); + } else { + mContext.stopService(fpsinfo); + } } } @@ -77,6 +86,12 @@ public void onReceive(final Context context, Intent intent) { mContext.startService(cpuinfo); } + // Start the fps info overlay, if activated + if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.SHOW_FPS_OVERLAY, 0) != 0) { + Intent fpsinfo = new Intent(mContext, com.android.systemui.FPSInfoService.class); + mContext.startService(fpsinfo); + } + } catch (Exception e) { Log.e(TAG, "Can't start load average service", e); } diff --git a/packages/SystemUI/src/com/android/systemui/FPSInfoService.java b/packages/SystemUI/src/com/android/systemui/FPSInfoService.java new file mode 100644 index 000000000000..95bca0e072b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/FPSInfoService.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2019-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.StringBuffer; +import java.lang.Math; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FPSInfoService extends Service { + private View mView; + private Thread mCurFPSThread; + private final String TAG = "FPSInfoService"; + private String mFps = null; + private String MEASURED_FPS = ""; + + private class FPSView extends View { + private Paint mOnlinePaint; + private float mAscent; + private int mFH; + private int mMaxWidth; + + private int mNeededWidth; + private int mNeededHeight; + + private boolean mDataAvail; + + private Handler mCurFPSHandler = new Handler() { + public void handleMessage(Message msg) { + if(msg.obj==null){ + return; + } + if(msg.what==1){ + String msgData = (String) msg.obj; + msgData = msgData.substring(0, Math.min(msgData.length(), 9)); + mFps = msgData; + mDataAvail = true; + updateDisplay(); + } + } + }; + + FPSView(Context c) { + super(c); + float density = c.getResources().getDisplayMetrics().density; + int paddingPx = Math.round(5 * density); + setPadding(paddingPx, paddingPx, paddingPx, paddingPx); + setBackgroundColor(Color.argb(0x60, 0, 0, 0)); + + final int textSize = Math.round(12 * density); + + mOnlinePaint = new Paint(); + mOnlinePaint.setAntiAlias(true); + mOnlinePaint.setTextSize(textSize); + mOnlinePaint.setColor(Color.WHITE); + mOnlinePaint.setShadowLayer(5.0f, 0.0f, 0.0f, Color.BLACK); + + mAscent = mOnlinePaint.ascent(); + float descent = mOnlinePaint.descent(); + mFH = (int)(descent - mAscent + .5f); + + final String maxWidthStr="fps: 60.1"; + mMaxWidth = (int)mOnlinePaint.measureText(maxWidthStr); + + updateDisplay(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mCurFPSHandler.removeMessages(1); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(resolveSize(mNeededWidth, widthMeasureSpec), + resolveSize(mNeededHeight, heightMeasureSpec)); + } + + private String getFPSInfoString() { + return mFps; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (!mDataAvail) { + return; + } + + final int W = mNeededWidth; + final int LEFT = getWidth()-1; + + int x = LEFT - mPaddingLeft; + int top = mPaddingTop + 2; + int bottom = mPaddingTop + mFH - 2; + + int y = mPaddingTop - (int)mAscent; + + String s=getFPSInfoString(); + canvas.drawText(s, LEFT-mPaddingLeft-mMaxWidth, + y-1, mOnlinePaint); + y += mFH; + } + + void updateDisplay() { + if (!mDataAvail) { + return; + } + + int neededWidth = mPaddingLeft + mPaddingRight + mMaxWidth; + int neededHeight = mPaddingTop + mPaddingBottom + 40; + if (neededWidth != mNeededWidth || neededHeight != mNeededHeight) { + mNeededWidth = neededWidth; + mNeededHeight = neededHeight; + requestLayout(); + } else { + invalidate(); + } + } + + public Handler getHandler(){ + return mCurFPSHandler; + } + } + + protected class CurFPSThread extends Thread { + private boolean mInterrupt = false; + private Handler mHandler; + + public CurFPSThread(Handler handler){ + mHandler=handler; + } + + public void interrupt() { + mInterrupt = true; + } + + @Override + public void run() { + try { + while (!mInterrupt) { + sleep(500); + StringBuffer sb=new StringBuffer(); + String fpsVal = FPSInfoService.readOneLine(MEASURED_FPS); + mHandler.sendMessage(mHandler.obtainMessage(1, fpsVal)); + } + } catch (InterruptedException e) { + return; + } + } + }; + + @Override + public void onCreate() { + super.onCreate(); + + MEASURED_FPS = getResources().getString(R.string.config_fpsInfoSysNode); + + mView = new FPSView(this); + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.LEFT | Gravity.TOP; + params.setTitle("FPS Info"); + + startThread(); + + IntentFilter screenStateFilter = new IntentFilter(Intent.ACTION_SCREEN_ON); + screenStateFilter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenStateReceiver, screenStateFilter, Context.RECEIVER_NOT_EXPORTED); + + WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); + wm.addView(mView, params); + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopThread(); + ((WindowManager)getSystemService(WINDOW_SERVICE)).removeView(mView); + mView = null; + unregisterReceiver(mScreenStateReceiver); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private static String readOneLine(String fname) { + BufferedReader br; + String line = null; + try { + br = new BufferedReader(new FileReader(fname), 512); + try { + line = br.readLine(); + } finally { + br.close(); + } + } catch (Exception e) { + return null; + } + return line; + } + + private BroadcastReceiver mScreenStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { + Log.d(TAG, "ACTION_SCREEN_ON"); + startThread(); + mView.setVisibility(View.VISIBLE); + } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { + Log.d(TAG, "ACTION_SCREEN_OFF"); + mView.setVisibility(View.GONE); + stopThread(); + } + } + }; + + private void startThread() { + Log.d(TAG, "started CurFPSThread"); + mCurFPSThread = new CurFPSThread(mView.getHandler()); + mCurFPSThread.start(); + } + + private void stopThread() { + if (mCurFPSThread != null && mCurFPSThread.isAlive()) { + Log.d(TAG, "stopping CurFPSThread"); + mCurFPSThread.interrupt(); + try { + mCurFPSThread.join(); + } catch (InterruptedException e) { + } + } + mCurFPSThread = null; + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 72148fe5b3f8..bb68f163af13 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -24,6 +24,7 @@ import com.android.systemui.qs.tiles.AmbientDisplayTile import com.android.systemui.qs.tiles.AODTile import com.android.systemui.qs.tiles.CPUInfoTile import com.android.systemui.qs.tiles.CaffeineTile +import com.android.systemui.qs.tiles.FPSInfoTile import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile @@ -69,6 +70,12 @@ interface LineageModule { @StringKey(CaffeineTile.TILE_SPEC) fun bindCaffeineTile(caffeineTile: CaffeineTile): QSTileImpl<*> + /** Inject FPSInfoTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(FPSInfoTile.TILE_SPEC) + fun FPSInfoTile(fpsInfoTile: FPSInfoTile): QSTileImpl<*> + /** Inject HeadsUpTile into tileMap in QSModule */ @Binds @IntoMap @@ -195,6 +202,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(FPSInfoTile.TILE_SPEC) + fun provideFPSInfoConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(FPSInfoTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_fps_info, + labelRes = R.string.quick_settings_fpsinfo_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(HEADS_UP_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java new file mode 100644 index 000000000000..9377ae931800 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2019 The OmniROM Project + * (C) 2017-2025 crDroidAndroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; +import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.settings.SettingObserver; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import java.io.File; + +import javax.inject.Inject; + +/** Quick settings tile: FPSInfo overlay **/ +public class FPSInfoTile extends QSTileImpl { + + public static final String TILE_SPEC = "fpsinfo"; + + private final SettingObserver mSetting; + private final boolean isAvailable; + @Nullable + private Icon mIcon = null; + + @Inject + public FPSInfoTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + SecureSettings secureSettings) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + final String fpsInfoSysNode = mContext.getResources().getString( + R.string.config_fpsInfoSysNode); + isAvailable = fpsInfoSysNode != null && (new File(fpsInfoSysNode).isFile()); + + mSetting = new SettingObserver(secureSettings, mHandler, Secure.SHOW_FPS_OVERLAY, getHost().getUserId()) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + public BooleanState newTileState() { + BooleanState state = new BooleanState(); + state.handlesLongClick = false; + return state; + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + mSetting.setValue(mState.value ? 0 : 1); + refreshState(); + toggleState(); + } + + protected void toggleState() { + Intent service = (new Intent()) + .setClassName("com.android.systemui", + "com.android.systemui.FPSInfoService"); + if (mSetting.getValue() == 0) { + mContext.stopService(service); + } else { + mContext.startService(service); + } + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + if (mSetting == null) return; + final int value = arg instanceof Integer ? (Integer)arg : mSetting.getValue(); + final boolean fpsInfoEnabled = value != 0; + state.value = fpsInfoEnabled; + state.label = mContext.getString(R.string.quick_settings_fpsinfo_label); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_fps_info); + } + state.icon = mIcon; + state.contentDescription = mContext.getString( + R.string.quick_settings_fpsinfo_label); + if (fpsInfoEnabled) { + state.state = Tile.STATE_ACTIVE; + } else { + state.state = Tile.STATE_INACTIVE; + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_fpsinfo_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public void handleSetListening(boolean listening) { + // Do nothing + } + + @Override + public boolean isAvailable() { + return isAvailable; + } +} From bfe9b7055269e9ef018c0637466253e715b86266 Mon Sep 17 00:00:00 2001 From: jhonboy121 Date: Wed, 5 Jan 2022 13:30:41 +0530 Subject: [PATCH 0547/1315] SystemUI: Rewrite FPSInfoService in kt from scratch * We now make use of coroutines for periodically reading fps * Added support for controlling fps read interval via overlay * Make sure tile is not available if fps info node is not accessible * Also keep the overlay below statusbar inset Signed-off-by: jhonboy121 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/cr_config.xml | 3 + .../SystemUI/res/values/matrixx_dimens.xml | 3 + .../SystemUI/res/values/matrixx_strings.xml | 3 + .../com/android/systemui/FPSInfoService.java | 290 ------------------ .../com/android/systemui/FPSInfoService.kt | 201 ++++++++++++ .../systemui/dagger/DefaultServiceBinder.java | 7 + 6 files changed, 217 insertions(+), 290 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/FPSInfoService.java create mode 100644 packages/SystemUI/src/com/android/systemui/FPSInfoService.kt diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index 50c4ac0dcaa8..a86733fd06fc 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -29,4 +29,7 @@ + + + 1000 diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index 021751b2a7de..53d3ce90a532 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -97,4 +97,7 @@ 4dp + + + 4dp diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 9ee19152ffee..b172be788df0 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -121,4 +121,7 @@ FPS Info + + + FPS: %1$d diff --git a/packages/SystemUI/src/com/android/systemui/FPSInfoService.java b/packages/SystemUI/src/com/android/systemui/FPSInfoService.java deleted file mode 100644 index 95bca0e072b7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/FPSInfoService.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2019-2025 crDroid Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui; - -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.lang.StringBuffer; -import java.lang.Math; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class FPSInfoService extends Service { - private View mView; - private Thread mCurFPSThread; - private final String TAG = "FPSInfoService"; - private String mFps = null; - private String MEASURED_FPS = ""; - - private class FPSView extends View { - private Paint mOnlinePaint; - private float mAscent; - private int mFH; - private int mMaxWidth; - - private int mNeededWidth; - private int mNeededHeight; - - private boolean mDataAvail; - - private Handler mCurFPSHandler = new Handler() { - public void handleMessage(Message msg) { - if(msg.obj==null){ - return; - } - if(msg.what==1){ - String msgData = (String) msg.obj; - msgData = msgData.substring(0, Math.min(msgData.length(), 9)); - mFps = msgData; - mDataAvail = true; - updateDisplay(); - } - } - }; - - FPSView(Context c) { - super(c); - float density = c.getResources().getDisplayMetrics().density; - int paddingPx = Math.round(5 * density); - setPadding(paddingPx, paddingPx, paddingPx, paddingPx); - setBackgroundColor(Color.argb(0x60, 0, 0, 0)); - - final int textSize = Math.round(12 * density); - - mOnlinePaint = new Paint(); - mOnlinePaint.setAntiAlias(true); - mOnlinePaint.setTextSize(textSize); - mOnlinePaint.setColor(Color.WHITE); - mOnlinePaint.setShadowLayer(5.0f, 0.0f, 0.0f, Color.BLACK); - - mAscent = mOnlinePaint.ascent(); - float descent = mOnlinePaint.descent(); - mFH = (int)(descent - mAscent + .5f); - - final String maxWidthStr="fps: 60.1"; - mMaxWidth = (int)mOnlinePaint.measureText(maxWidthStr); - - updateDisplay(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mCurFPSHandler.removeMessages(1); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(resolveSize(mNeededWidth, widthMeasureSpec), - resolveSize(mNeededHeight, heightMeasureSpec)); - } - - private String getFPSInfoString() { - return mFps; - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (!mDataAvail) { - return; - } - - final int W = mNeededWidth; - final int LEFT = getWidth()-1; - - int x = LEFT - mPaddingLeft; - int top = mPaddingTop + 2; - int bottom = mPaddingTop + mFH - 2; - - int y = mPaddingTop - (int)mAscent; - - String s=getFPSInfoString(); - canvas.drawText(s, LEFT-mPaddingLeft-mMaxWidth, - y-1, mOnlinePaint); - y += mFH; - } - - void updateDisplay() { - if (!mDataAvail) { - return; - } - - int neededWidth = mPaddingLeft + mPaddingRight + mMaxWidth; - int neededHeight = mPaddingTop + mPaddingBottom + 40; - if (neededWidth != mNeededWidth || neededHeight != mNeededHeight) { - mNeededWidth = neededWidth; - mNeededHeight = neededHeight; - requestLayout(); - } else { - invalidate(); - } - } - - public Handler getHandler(){ - return mCurFPSHandler; - } - } - - protected class CurFPSThread extends Thread { - private boolean mInterrupt = false; - private Handler mHandler; - - public CurFPSThread(Handler handler){ - mHandler=handler; - } - - public void interrupt() { - mInterrupt = true; - } - - @Override - public void run() { - try { - while (!mInterrupt) { - sleep(500); - StringBuffer sb=new StringBuffer(); - String fpsVal = FPSInfoService.readOneLine(MEASURED_FPS); - mHandler.sendMessage(mHandler.obtainMessage(1, fpsVal)); - } - } catch (InterruptedException e) { - return; - } - } - }; - - @Override - public void onCreate() { - super.onCreate(); - - MEASURED_FPS = getResources().getString(R.string.config_fpsInfoSysNode); - - mView = new FPSView(this); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - PixelFormat.TRANSLUCENT); - params.gravity = Gravity.LEFT | Gravity.TOP; - params.setTitle("FPS Info"); - - startThread(); - - IntentFilter screenStateFilter = new IntentFilter(Intent.ACTION_SCREEN_ON); - screenStateFilter.addAction(Intent.ACTION_SCREEN_OFF); - registerReceiver(mScreenStateReceiver, screenStateFilter, Context.RECEIVER_NOT_EXPORTED); - - WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); - wm.addView(mView, params); - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopThread(); - ((WindowManager)getSystemService(WINDOW_SERVICE)).removeView(mView); - mView = null; - unregisterReceiver(mScreenStateReceiver); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private static String readOneLine(String fname) { - BufferedReader br; - String line = null; - try { - br = new BufferedReader(new FileReader(fname), 512); - try { - line = br.readLine(); - } finally { - br.close(); - } - } catch (Exception e) { - return null; - } - return line; - } - - private BroadcastReceiver mScreenStateReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { - Log.d(TAG, "ACTION_SCREEN_ON"); - startThread(); - mView.setVisibility(View.VISIBLE); - } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { - Log.d(TAG, "ACTION_SCREEN_OFF"); - mView.setVisibility(View.GONE); - stopThread(); - } - } - }; - - private void startThread() { - Log.d(TAG, "started CurFPSThread"); - mCurFPSThread = new CurFPSThread(mView.getHandler()); - mCurFPSThread.start(); - } - - private void stopThread() { - if (mCurFPSThread != null && mCurFPSThread.isAlive()) { - Log.d(TAG, "stopping CurFPSThread"); - mCurFPSThread.interrupt(); - try { - mCurFPSThread.join(); - } catch (InterruptedException e) { - } - } - mCurFPSThread = null; - } -} - diff --git a/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt new file mode 100644 index 000000000000..96316244c1a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2019-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.app.Service +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.PixelFormat +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.view.Gravity +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.TextView + +import androidx.core.graphics.ColorUtils + +import com.android.systemui.res.R +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.WakefulnessLifecycle + +import java.io.FileNotFoundException +import java.io.IOException +import java.io.RandomAccessFile + +import javax.inject.Inject + +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class FPSInfoService @Inject constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, + @Main private val handler: Handler +) : Service() { + + private lateinit var coroutineScope: CoroutineScope + + private lateinit var windowManager: WindowManager + private lateinit var fpsInfoView: TextView + private lateinit var configuration: Configuration + private val layoutParams = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT, + ).apply { + gravity = Gravity.TOP or Gravity.START + } + + private lateinit var fpsInfoNode: RandomAccessFile + + private var fpsReadJob: Job? = null + + private var observerRegistered = false + private val wakefulnessObserver = object: WakefulnessLifecycle.Observer { + override fun onStartedGoingToSleep() { + stopReading() + } + + override fun onStartedWakingUp() { + startReading() + } + } + + private var fpsReadInterval = FPS_MEASURE_INTERVAL_DEFAULT + + override fun onCreate() { + super.onCreate() + coroutineScope = CoroutineScope(Dispatchers.IO) + + windowManager = getSystemService(WindowManager::class.java)!! + configuration = resources.configuration + layoutParams.y = getTopInset() + + fpsInfoView = TextView(this).apply { + text = getString(R.string.fps_text_placeholder, 0) + setBackgroundColor(ColorUtils.setAlphaComponent(Color.BLACK, BACKGROUND_ALPHA)) + setTextColor(Color.WHITE) + val padding = resources.getDimensionPixelSize(R.dimen.fps_info_text_padding) + setPadding(padding, padding, padding, padding) + } + + val nodePath = getString(R.string.config_fpsInfoSysNode) + try { + fpsInfoNode = RandomAccessFile(nodePath, "r") + } catch (e: FileNotFoundException) { + Log.e(TAG, "Sysfs node $nodePath does not exist, stopping service") + stopSelf() + } + } + + override fun onStartCommand(intent: Intent?, startId: Int, flags: Int): Int { + if (!observerRegistered) { + wakefulnessLifecycle.addObserver(wakefulnessObserver) + observerRegistered = true + } + fpsReadInterval = resources.getInteger(R.integer.config_fpsReadInterval).toLong() + startReading() + return START_STICKY + } + + override fun onConfigurationChanged(newConfig: Configuration) { + if (configuration.orientation != newConfig.orientation) { + layoutParams.y = getTopInset() + if (fpsInfoView.parent != null) + windowManager.updateViewLayout(fpsInfoView, layoutParams) + } + configuration = newConfig + } + + private fun getTopInset(): Int = windowManager.currentWindowMetrics + .windowInsets.getInsets(WindowInsets.Type.statusBars()).top + + private fun startReading() { + if (fpsReadJob != null) return + if (fpsInfoView.parent == null) windowManager.addView(fpsInfoView, layoutParams) + fpsReadJob = coroutineScope.launch { + do { + val fps = measureFps() + handler.post { + fpsInfoView.text = getString(R.string.fps_text_placeholder, fps) + } + delay(fpsReadInterval) + } while (isActive) + } + } + + private fun stopReading() { + if (fpsReadJob == null) return + fpsReadJob?.cancel() + fpsReadJob = null + if (fpsInfoView.parent != null) windowManager.removeViewImmediate(fpsInfoView) + } + + private fun measureFps(): Int { + fpsInfoNode.seek(0L) + val measuredFps: String + try { + measuredFps = fpsInfoNode.readLine() + } catch (e: IOException) { + Log.e(TAG, "IOException while reading from FPS node, ${e.message}") + return -1 + } + try { + val fps: Float = measuredFps.trim().let { + if (it.contains(": ")) it.split("\\s+".toRegex())[1] else it + }.toFloat() + return fps.roundToInt() + } catch (e: NumberFormatException) { + Log.e(TAG, "NumberFormatException occurred while parsing FPS info, ${e.message}") + } + return -1 + } + + override fun onDestroy() { + stopReading() + coroutineScope.cancel() + if (observerRegistered) { + wakefulnessLifecycle.removeObserver(wakefulnessObserver) + observerRegistered = false + } + if (fpsInfoView.parent != null) + windowManager.removeViewImmediate(fpsInfoView) + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private companion object { + private const val TAG = "FPSInfoService" + private const val FPS_MEASURE_INTERVAL_DEFAULT = 1000L + + private const val BACKGROUND_ALPHA = 120 + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index 8f01775bc969..01d1d8905a46 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java @@ -18,6 +18,7 @@ import android.app.Service; +import com.android.systemui.FPSInfoService; import com.android.systemui.SystemUIService; import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerService; import com.android.systemui.doze.DozeService; @@ -100,4 +101,10 @@ public abstract Service bindNotificationListenerWithPlugins( @ClassKey(GlanceableHubWidgetManagerService.class) public abstract Service bindGlanceableHubWidgetManagerService( GlanceableHubWidgetManagerService service); + + /** Inject into FPSInfoService */ + @Binds + @IntoMap + @ClassKey(FPSInfoService.class) + public abstract Service bindFPSInfoService(FPSInfoService service); } From 959391015350e7f47809d5c2442cc9f998c14387 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 9 Apr 2024 22:26:36 +0530 Subject: [PATCH 0548/1315] SystemUI: Add default path for FPS info service Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/cr_config.xml | 2 +- .../src/com/android/systemui/FPSInfoService.kt | 16 +++++++++++----- .../android/systemui/qs/tiles/FPSInfoTile.java | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index a86733fd06fc..7867e1b88ceb 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -28,7 +28,7 @@ - + /sys/class/drm/sde-crtc-0/measured_fps 1000 diff --git a/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt index 96316244c1a9..c6362a7b1fc5 100644 --- a/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt +++ b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt @@ -35,7 +35,7 @@ import com.android.systemui.res.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.WakefulnessLifecycle -import java.io.FileNotFoundException +import java.io.File import java.io.IOException import java.io.RandomAccessFile @@ -107,10 +107,16 @@ class FPSInfoService @Inject constructor( } val nodePath = getString(R.string.config_fpsInfoSysNode) - try { - fpsInfoNode = RandomAccessFile(nodePath, "r") - } catch (e: FileNotFoundException) { - Log.e(TAG, "Sysfs node $nodePath does not exist, stopping service") + val file = File(nodePath) + if (file.exists() && file.canRead()) { + try { + fpsInfoNode = RandomAccessFile(nodePath, "r") + } catch (e: IOException) { + Log.e(TAG, "Sysfs node $nodePath does not exist, stopping service") + stopSelf() + } + } else { + Log.e(TAG, "Sysfs node $nodePath does not exist or is not readable") stopSelf() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java index 9377ae931800..74aec7ef8b8f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FPSInfoTile.java @@ -75,7 +75,9 @@ public FPSInfoTile( final String fpsInfoSysNode = mContext.getResources().getString( R.string.config_fpsInfoSysNode); - isAvailable = fpsInfoSysNode != null && (new File(fpsInfoSysNode).isFile()); + + File file = new File(fpsInfoSysNode); + isAvailable = fpsInfoSysNode != null && file.exists() && file.canRead(); mSetting = new SettingObserver(secureSettings, mHandler, Secure.SHOW_FPS_OVERLAY, getHost().getUserId()) { @Override From bdc70e6fb2a8bb412944eec31806f31f18f27290 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 16 Jul 2025 22:07:07 +0530 Subject: [PATCH 0549/1315] CPUInfoService: Rewrite from scratch Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/systemui/CPUInfoService.java | 439 ------------------ .../com/android/systemui/CPUInfoService.kt | 247 ++++++++++ .../systemui/dagger/DefaultServiceBinder.java | 7 + 3 files changed, 254 insertions(+), 439 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/CPUInfoService.java create mode 100644 packages/SystemUI/src/com/android/systemui/CPUInfoService.kt diff --git a/packages/SystemUI/src/com/android/systemui/CPUInfoService.java b/packages/SystemUI/src/com/android/systemui/CPUInfoService.java deleted file mode 100644 index 05dfc1f1eadf..000000000000 --- a/packages/SystemUI/src/com/android/systemui/CPUInfoService.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui; - -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager; -import android.util.Log; - -import com.android.systemui.res.R; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.lang.StringBuffer; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CPUInfoService extends Service { - private View mView; - private Thread mCurCPUThread; - private final String TAG = "CPUInfoService"; - private int mNumCpus = 2; - private String[] mCpu = null; - private String[] mCurrFreq = null; - private String[] mCurrGov = null; - - private int CPU_TEMP_DIVIDER = 1; - private String CPU_TEMP_SENSOR = ""; - private String DISPLAY_CPUS = ""; - private boolean mCpuTempAvail; - - private static final String NUM_OF_CPUS_PATH = "/sys/devices/system/cpu/present"; - private static final String CURRENT_CPU = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"; - private static final String CPU_ROOT = "/sys/devices/system/cpu/cpu"; - private static final String CPU_CUR_TAIL = "/cpufreq/scaling_cur_freq"; - private static final String CPU_GOV_TAIL = "/cpufreq/scaling_governor"; - - private class CPUView extends View { - private Paint mOnlinePaint; - private Paint mOfflinePaint; - private float mAscent; - private int mFH; - private int mMaxWidth; - - private int mNeededWidth; - private int mNeededHeight; - private String mCpuTemp; - - private boolean mDataAvail; - - private Handler mCurCPUHandler = new Handler() { - public void handleMessage(Message msg) { - if(msg.obj==null){ - return; - } - if(msg.what==1){ - String msgData = (String) msg.obj; - try { - String[] parts=msgData.split(";"); - mCpuTemp=parts[0]; - - String[] cpuParts=parts[1].split("\\|"); - for(int i=0; i 1) { - return String.format("%s", - Integer.parseInt(cpuTemp) / CPU_TEMP_DIVIDER); - } else { - return cpuTemp; - } - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (!mDataAvail) { - return; - } - - final int W = mNeededWidth; - final int RIGHT = getWidth()-1; - - int x = RIGHT - mPaddingRight; - int top = mPaddingTop + 2; - int bottom = mPaddingTop + mFH - 2; - - int y = mPaddingTop - (int)mAscent; - - if(!mCpuTemp.equals("0")) { - canvas.drawText("Temp: " + getCpuTemp(mCpuTemp) + "°C", - RIGHT-mPaddingRight-mMaxWidth, y-1, mOnlinePaint); - y += mFH; - } - - for(int i=0; i 0) { - numOfCpu = cpuList.length; - mCpu = new String[numOfCpu]; - - for (int i = 0; i < numOfCpu; i++) { - try { - int cpu = Integer.parseInt(cpuList[i]); - mCpu[i] = cpuList[i]; - } catch (NumberFormatException ex) { - // derped overlay - return getCpus(null); - } - } - } else { - // derped overlay - return getCpus(null); - } - } else { - // empty overlay, take all cores - String numOfCpus = readOneLine(NUM_OF_CPUS_PATH); - cpuList = numOfCpus.split("-"); - if (cpuList.length > 1) { - try { - int cpuStart = Integer.parseInt(cpuList[0]); - int cpuEnd = Integer.parseInt(cpuList[1]); - - numOfCpu = cpuEnd - cpuStart + 1; - - if (numOfCpu < 0) - numOfCpu = 1; - } catch (NumberFormatException ex) { - numOfCpu = 1; - } - } - - mCpu = new String[numOfCpu]; - for (int i = 0; i < numOfCpu; i++) - { - mCpu[i] = String.valueOf(i); - } - } - return numOfCpu; - } - - private BroadcastReceiver mScreenStateReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { - Log.d(TAG, "ACTION_SCREEN_ON "); - startThread(); - mView.setVisibility(View.VISIBLE); - } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { - Log.d(TAG, "ACTION_SCREEN_OFF"); - mView.setVisibility(View.GONE); - stopThread(); - } - } - }; - - private void startThread() { - Log.d(TAG, "started CurCPUThread"); - mCurCPUThread = new CurCPUThread(mView.getHandler(), mNumCpus); - mCurCPUThread.start(); - } - - private void stopThread() { - if (mCurCPUThread != null && mCurCPUThread.isAlive()) { - Log.d(TAG, "stopping CurCPUThread"); - mCurCPUThread.interrupt(); - try { - mCurCPUThread.join(); - } catch (InterruptedException e) { - } - } - mCurCPUThread = null; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/CPUInfoService.kt b/packages/SystemUI/src/com/android/systemui/CPUInfoService.kt new file mode 100644 index 000000000000..81ee058c6b4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CPUInfoService.kt @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2019-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.app.Service +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.PixelFormat +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.TextView +import androidx.core.graphics.ColorUtils +import com.android.systemui.res.R +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.WakefulnessLifecycle +import java.io.File +import java.io.RandomAccessFile +import java.io.IOException +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class CPUInfoService @Inject constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, + @Main private val handler: Handler +) : Service() { + + private lateinit var windowManager: WindowManager + private lateinit var cpuInfoView: TextView + private lateinit var configuration: Configuration + private lateinit var coroutineScope: CoroutineScope + + private var readJob: kotlinx.coroutines.Job? = null + private var observerRegistered = false + private var cpuList: List = emptyList() + private var cpuTempDivider = 1 + private var cpuTempSensor = "" + private var cpuDisplayString = "" + + private val fileMap = mutableMapOf() + + private val layoutParams = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.TOP or Gravity.END + } + + private val wakefulnessObserver = object : WakefulnessLifecycle.Observer { + override fun onStartedGoingToSleep() { + stopReading() + } + + override fun onStartedWakingUp() { + startReading() + } + } + + override fun onCreate() { + super.onCreate() + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + windowManager = getSystemService(WindowManager::class.java)!! + configuration = resources.configuration + layoutParams.y = getTopInset() + + cpuTempDivider = resources.getInteger(R.integer.config_cpuTempDivider) + cpuTempSensor = getString(R.string.config_cpuTempSensor) + cpuDisplayString = getString(R.string.config_displayCpus) + + cpuList = parseCpuList(cpuDisplayString) + + try { + fileMap[cpuTempSensor] = RandomAccessFile(cpuTempSensor, "r") + cpuList.forEach { cpu -> + val freqPath = "/sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_cur_freq" + val govPath = "/sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor" + fileMap[freqPath] = RandomAccessFile(freqPath, "r") + fileMap[govPath] = RandomAccessFile(govPath, "r") + } + } catch (e: IOException) { + Log.e(TAG, "Error preloading CPU sysfs nodes: ${e.message}") + stopSelf() + } + + cpuInfoView = TextView(this).apply { + setBackgroundColor(ColorUtils.setAlphaComponent(Color.BLACK, BACKGROUND_ALPHA)) + setTextColor(Color.WHITE) + val padding = resources.getDimensionPixelSize(R.dimen.fps_info_text_padding) + setPadding(padding, padding, padding, padding) + } + } + + override fun onStartCommand(intent: Intent?, startId: Int, flags: Int): Int { + if (!observerRegistered) { + wakefulnessLifecycle.addObserver(wakefulnessObserver) + observerRegistered = true + } + startReading() + return START_STICKY + } + + override fun onConfigurationChanged(newConfig: Configuration) { + if (configuration.orientation != newConfig.orientation) { + layoutParams.y = getTopInset() + if (cpuInfoView.parent != null) + windowManager.updateViewLayout(cpuInfoView, layoutParams) + } + configuration = newConfig + } + + private fun getTopInset(): Int = windowManager.currentWindowMetrics + .windowInsets.getInsets(WindowInsets.Type.statusBars()).top + + private fun parseCpuList(displayCpus: String): List { + return if (displayCpus.isNotBlank()) { + displayCpus.split(",").filter { it.toIntOrNull() != null } + } else { + val path = "/sys/devices/system/cpu/present" + val range = try { + File(path).bufferedReader().use { it.readLine() } + } catch (e: IOException) { + Log.e(TAG, "Failed to read CPU range: ${e.message}") + null + } ?: return listOf("0") + + return when { + range.contains("-") -> { + val bounds = range.split("-").mapNotNull { it.toIntOrNull() } + if (bounds.size == 2) (bounds[0]..bounds[1]).map { it.toString() } else listOf("0") + } + range.contains(",") -> { + range.split(",").filter { it.toIntOrNull() != null } + } + else -> listOf("0") + } + } + } + + private fun startReading() { + readJob?.cancel() + if (cpuInfoView.parent == null) windowManager.addView(cpuInfoView, layoutParams) + readJob = coroutineScope.launch { + while (isActive) { + val builder = StringBuilder() + val temp = readLineFromRandomAccessFile(cpuTempSensor)?.toIntOrNull()?.div(cpuTempDivider) ?: 0 + builder.append("Temp: ${temp}°C\n") + + cpuList.forEach { cpu -> + val freqPath = "/sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_cur_freq" + val govPath = "/sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor" + val freq = readLineFromRandomAccessFile(freqPath)?.toIntOrNull()?.div(1000) ?: 0 + val gov = readLineFromRandomAccessFile(govPath) ?: "N/A" + if (freq > 0) { + builder.append("cpu$cpu: $gov ${freq}MHz\n") + } else { + builder.append("cpu$cpu: offline\n") + } + } + + val output = builder.toString().trim() + handler.post { + cpuInfoView.text = output + cpuInfoView.visibility = View.VISIBLE + } + delay(MEASURE_INTERVAL_DEFAULT) + } + } + } + + private fun stopReading() { + readJob?.cancel() + readJob = null + removeViewIfNeeded() + } + + private fun removeViewIfNeeded() { + if (cpuInfoView.parent != null) { + windowManager.removeViewImmediate(cpuInfoView) + } + } + + private fun readLineFromRandomAccessFile(path: String): String? { + val file = fileMap[path] ?: return null + return try { + file.seek(0L) + file.readLine() + } catch (e: IOException) { + Log.e(TAG, "Failed to read from $path: ${e.message}") + null + } + } + + override fun onDestroy() { + stopReading() + coroutineScope.cancel() + fileMap.values.forEach { + try { it.close() } catch (_: IOException) {} + } + fileMap.clear() + if (observerRegistered) { + wakefulnessLifecycle.removeObserver(wakefulnessObserver) + observerRegistered = false + } + removeViewIfNeeded() + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private companion object { + private const val TAG = "CPUInfoService" + private const val MEASURE_INTERVAL_DEFAULT = 1000L + private const val BACKGROUND_ALPHA = 120 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index 01d1d8905a46..807ec4a008b0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java @@ -18,6 +18,7 @@ import android.app.Service; +import com.android.systemui.CPUInfoService; import com.android.systemui.FPSInfoService; import com.android.systemui.SystemUIService; import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerService; @@ -107,4 +108,10 @@ public abstract Service bindGlanceableHubWidgetManagerService( @IntoMap @ClassKey(FPSInfoService.class) public abstract Service bindFPSInfoService(FPSInfoService service); + + /** Inject into FPSInfoService */ + @Binds + @IntoMap + @ClassKey(CPUInfoService.class) + public abstract Service bindCPUInfoService(CPUInfoService service); } From 571cb169cbedfdaf3369f0fda731e53b08b0cb9e Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 16 Jul 2025 22:06:11 +0530 Subject: [PATCH 0550/1315] FPSInfoService: Improvements and clean up * Error handling, mem fixes, enhancements. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_qs_compass.xml | 31 ++ packages/SystemUI/res/values/config.xml | 2 +- packages/SystemUI/res/values/cr_config.xml | 3 - .../SystemUI/res/values/matrixx_arrays.xml | 23 +- .../SystemUI/res/values/matrixx_attrs.xml | 11 + .../SystemUI/res/values/matrixx_strings.xml | 13 + .../com/android/systemui/FPSInfoService.kt | 90 +++--- .../android/systemui/lineage/LineageModule.kt | 22 ++ .../systemui/qs/tiles/CompassTile.java | 264 ++++++++++++++++++ 9 files changed, 397 insertions(+), 62 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_compass.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_compass.xml b/packages/SystemUI/res/drawable/ic_qs_compass.xml new file mode 100644 index 000000000000..faaccb9ef563 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_compass.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index c20316c7c796..ec4cbb2fd425 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index 7867e1b88ceb..82c6082617e5 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -29,7 +29,4 @@ /sys/class/drm/sde-crtc-0/measured_fps - - - 1000 diff --git a/packages/SystemUI/res/values/matrixx_arrays.xml b/packages/SystemUI/res/values/matrixx_arrays.xml index 93f984cac18d..3333c852af07 100644 --- a/packages/SystemUI/res/values/matrixx_arrays.xml +++ b/packages/SystemUI/res/values/matrixx_arrays.xml @@ -4,16 +4,15 @@ SPDX-License-Identifier: Apache-2.0 --> - - - - - - - - - - - + + + @string/quick_settings_compass_N + @string/quick_settings_compass_NE + @string/quick_settings_compass_E + @string/quick_settings_compass_SE + @string/quick_settings_compass_S + @string/quick_settings_compass_SW + @string/quick_settings_compass_W + @string/quick_settings_compass_NW + diff --git a/packages/SystemUI/res/values/matrixx_attrs.xml b/packages/SystemUI/res/values/matrixx_attrs.xml index a2a2f90915aa..93f984cac18d 100644 --- a/packages/SystemUI/res/values/matrixx_attrs.xml +++ b/packages/SystemUI/res/values/matrixx_attrs.xml @@ -5,4 +5,15 @@ --> + + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index b172be788df0..0e5070ebf9db 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -124,4 +124,17 @@ FPS: %1$d + + + Compass + %1$.0f\u00b0 %2$s + Initializing\u2026 + N + NE + E + SE + S + SW + W + NW diff --git a/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt index c6362a7b1fc5..6ce924eb6be5 100644 --- a/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt +++ b/packages/SystemUI/src/com/android/systemui/FPSInfoService.kt @@ -25,24 +25,21 @@ import android.os.Handler import android.os.IBinder import android.util.Log import android.view.Gravity +import android.view.View import android.view.WindowInsets import android.view.WindowManager import android.widget.TextView - import androidx.core.graphics.ColorUtils - import com.android.systemui.res.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.WakefulnessLifecycle - import java.io.File import java.io.IOException import java.io.RandomAccessFile - import javax.inject.Inject - import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -56,10 +53,15 @@ class FPSInfoService @Inject constructor( ) : Service() { private lateinit var coroutineScope: CoroutineScope - private lateinit var windowManager: WindowManager private lateinit var fpsInfoView: TextView private lateinit var configuration: Configuration + + private var fpsReadJob: Job? = null + private var observerRegistered = false + private val fileMap = mutableMapOf() + private var fpsNodePath: String = "" + private val layoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, @@ -73,12 +75,7 @@ class FPSInfoService @Inject constructor( gravity = Gravity.TOP or Gravity.START } - private lateinit var fpsInfoNode: RandomAccessFile - - private var fpsReadJob: Job? = null - - private var observerRegistered = false - private val wakefulnessObserver = object: WakefulnessLifecycle.Observer { + private val wakefulnessObserver = object : WakefulnessLifecycle.Observer { override fun onStartedGoingToSleep() { stopReading() } @@ -88,11 +85,9 @@ class FPSInfoService @Inject constructor( } } - private var fpsReadInterval = FPS_MEASURE_INTERVAL_DEFAULT - override fun onCreate() { super.onCreate() - coroutineScope = CoroutineScope(Dispatchers.IO) + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) windowManager = getSystemService(WindowManager::class.java)!! configuration = resources.configuration @@ -106,18 +101,18 @@ class FPSInfoService @Inject constructor( setPadding(padding, padding, padding, padding) } - val nodePath = getString(R.string.config_fpsInfoSysNode) - val file = File(nodePath) - if (file.exists() && file.canRead()) { + fpsNodePath = getString(R.string.config_fpsInfoSysNode) + val file = File(fpsNodePath) + if (!file.exists() || !file.canRead()) { + Log.e(TAG, "Sysfs node $fpsNodePath does not exist or is not readable") + stopSelf() + } else { try { - fpsInfoNode = RandomAccessFile(nodePath, "r") + fileMap[fpsNodePath] = RandomAccessFile(fpsNodePath, "r") } catch (e: IOException) { - Log.e(TAG, "Sysfs node $nodePath does not exist, stopping service") + Log.e(TAG, "Failed to open FPS node: ${e.message}") stopSelf() } - } else { - Log.e(TAG, "Sysfs node $nodePath does not exist or is not readable") - stopSelf() } } @@ -126,7 +121,6 @@ class FPSInfoService @Inject constructor( wakefulnessLifecycle.addObserver(wakefulnessObserver) observerRegistered = true } - fpsReadInterval = resources.getInteger(R.integer.config_fpsReadInterval).toLong() startReading() return START_STICKY } @@ -144,55 +138,60 @@ class FPSInfoService @Inject constructor( .windowInsets.getInsets(WindowInsets.Type.statusBars()).top private fun startReading() { - if (fpsReadJob != null) return + fpsReadJob?.cancel() if (fpsInfoView.parent == null) windowManager.addView(fpsInfoView, layoutParams) fpsReadJob = coroutineScope.launch { - do { + while (isActive) { val fps = measureFps() handler.post { fpsInfoView.text = getString(R.string.fps_text_placeholder, fps) + fpsInfoView.visibility = View.VISIBLE } - delay(fpsReadInterval) - } while (isActive) + delay(FPS_MEASURE_INTERVAL_DEFAULT) + } } } private fun stopReading() { - if (fpsReadJob == null) return fpsReadJob?.cancel() fpsReadJob = null - if (fpsInfoView.parent != null) windowManager.removeViewImmediate(fpsInfoView) + removeFpsViewIfNeeded() + } + + private fun removeFpsViewIfNeeded() { + if (fpsInfoView.parent != null) { + windowManager.removeViewImmediate(fpsInfoView) + } } private fun measureFps(): Int { - fpsInfoNode.seek(0L) + val file = fileMap[fpsNodePath] ?: return -1 val measuredFps: String try { - measuredFps = fpsInfoNode.readLine() + file.seek(0L) + measuredFps = file.readLine() } catch (e: IOException) { - Log.e(TAG, "IOException while reading from FPS node, ${e.message}") + Log.e(TAG, "IOException while accessing FPS node, ${e.message}") return -1 } - try { - val fps: Float = measuredFps.trim().let { - if (it.contains(": ")) it.split("\\s+".toRegex())[1] else it - }.toFloat() - return fps.roundToInt() - } catch (e: NumberFormatException) { - Log.e(TAG, "NumberFormatException occurred while parsing FPS info, ${e.message}") - } - return -1 + val fpsValue = measuredFps.trim().let { + if (it.contains(": ")) it.split("\\s+".toRegex())[1] else it + }.toFloatOrNull() + return fpsValue?.roundToInt() ?: -1 } override fun onDestroy() { stopReading() coroutineScope.cancel() + fileMap.values.forEach { + try { it.close() } catch (_: IOException) {} + } + fileMap.clear() if (observerRegistered) { wakefulnessLifecycle.removeObserver(wakefulnessObserver) observerRegistered = false } - if (fpsInfoView.parent != null) - windowManager.removeViewImmediate(fpsInfoView) + removeFpsViewIfNeeded() super.onDestroy() } @@ -201,7 +200,6 @@ class FPSInfoService @Inject constructor( private companion object { private const val TAG = "FPSInfoService" private const val FPS_MEASURE_INTERVAL_DEFAULT = 1000L - private const val BACKGROUND_ALPHA = 120 } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index bb68f163af13..f9db4d8661ef 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -24,6 +24,7 @@ import com.android.systemui.qs.tiles.AmbientDisplayTile import com.android.systemui.qs.tiles.AODTile import com.android.systemui.qs.tiles.CPUInfoTile import com.android.systemui.qs.tiles.CaffeineTile +import com.android.systemui.qs.tiles.CompassTile import com.android.systemui.qs.tiles.FPSInfoTile import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.OnTheGoTile @@ -70,6 +71,12 @@ interface LineageModule { @StringKey(CaffeineTile.TILE_SPEC) fun bindCaffeineTile(caffeineTile: CaffeineTile): QSTileImpl<*> + /** Inject CompassTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(CompassTile.TILE_SPEC) + fun bindCompassTile(compassTile: CompassTile): QSTileImpl<*> + /** Inject FPSInfoTile into tileMap in QSModule */ @Binds @IntoMap @@ -202,6 +209,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(CompassTile.TILE_SPEC) + fun provideCompassTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(CompassTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_compass, + labelRes = R.string.quick_settings_compass_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(FPSInfoTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java new file mode 100644 index 000000000000..e600eca44b9d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2019-2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Looper; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; + +import javax.inject.Inject; + +public class CompassTile extends QSTileImpl implements SensorEventListener { + + public static final String TILE_SPEC = "compass"; + + private final static float ALPHA = 0.97f; + + private boolean mActive = false; + + private SensorManager mSensorManager; + private Sensor mAccelerationSensor; + private Sensor mGeomagneticFieldSensor; + + private float[] mAcceleration; + private float[] mGeomagnetic; + + private boolean mListeningSensors; + + @Inject + public CompassTile(QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + mAccelerationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mGeomagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + + @Override + public BooleanState newTileState() { + BooleanState state = new BooleanState(); + state.handlesLongClick = false; + return state; + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + setListeningSensors(false); + mSensorManager = null; + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + mActive = !mActive; + refreshState(); + setListeningSensors(mActive); + } + + @Override + public void handleLongClick(@Nullable Expandable expandable) { + handleClick(expandable); + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + private void setListeningSensors(boolean listening) { + if (listening == mListeningSensors) return; + mListeningSensors = listening; + if (mListeningSensors) { + mSensorManager.registerListener( + this, mAccelerationSensor, SensorManager.SENSOR_DELAY_GAME); + mSensorManager.registerListener( + this, mGeomagneticFieldSensor, SensorManager.SENSOR_DELAY_GAME); + } else { + mSensorManager.unregisterListener(this); + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_compass_label); + } + + private Drawable rotateDrawable(Drawable drawable, float degrees) { + // Convert drawable to bitmap + Bitmap bitmap = drawableToBitmap(drawable); + + // Create matrix for rotation + Matrix matrix = new Matrix(); + matrix.postRotate(degrees); + + // Create rotated bitmap + Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + + // Convert rotated bitmap back to drawable + return new BitmapDrawable(mContext.getResources(), rotatedBitmap); + } + + private Bitmap drawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + // If the drawable is not a BitmapDrawable, create a new bitmap and draw the drawable on a canvas + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final Float degrees = arg == null ? 0 : (Float) arg; + + state.value = mActive; + + if (state.value) { + state.state = Tile.STATE_ACTIVE; + if (arg != null) { + state.label = formatValueWithCardinalDirection(degrees); + } else { + state.label = mContext.getString(R.string.quick_settings_compass_init); + } + } else { + state.label = mContext.getString(R.string.quick_settings_compass_label); + state.state = Tile.STATE_INACTIVE; + } + state.icon = new DrawableIcon(rotateDrawable( + mContext.getResources().getDrawable(R.drawable.ic_qs_compass), degrees)); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public boolean isAvailable() { + return mSensorManager != null && mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null + && mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null; + } + + @Override + public void handleSetListening(boolean listening) { + if (!listening) { + setListeningSensors(false); + mActive = false; + } + } + + private String formatValueWithCardinalDirection(float degree) { + int cardinalDirectionIndex = (int) (Math.floor(((degree - 22.5) % 360) / 45) + 1) % 8; + String[] cardinalDirections = mContext.getResources().getStringArray( + R.array.cardinal_directions); + + return mContext.getString(R.string.quick_settings_compass_value, degree, + cardinalDirections[cardinalDirectionIndex]); + } + + @Override + public void onSensorChanged(SensorEvent event) { + float[] values; + if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + if (mAcceleration == null) { + mAcceleration = event.values.clone(); + } + + values = mAcceleration; + } else { + // Magnetic field sensor + if (mGeomagnetic == null) { + mGeomagnetic = event.values.clone(); + } + + values = mGeomagnetic; + } + + for (int i = 0; i < 3; i++) { + values[i] = ALPHA * values[i] + (1 - ALPHA) * event.values[i]; + } + + if (!mActive || !mListeningSensors || mAcceleration == null || mGeomagnetic == null) { + // Nothing to do at this moment + return; + } + + float R[] = new float[9]; + float I[] = new float[9]; + if (!SensorManager.getRotationMatrix(R, I, mAcceleration, mGeomagnetic)) { + // Rotation matrix couldn't be calculated + return; + } + + // Get the current orientation + float[] orientation = new float[3]; + SensorManager.getOrientation(R, orientation); + + // Convert azimuth to degrees + Float newDegree = Float.valueOf((float) Math.toDegrees(orientation[0])); + newDegree = (newDegree + 360) % 360; + + refreshState(newDegree); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // noop + } +} From cb5de6927158d2365e6e6d086875b1a66004513c Mon Sep 17 00:00:00 2001 From: Blake North Date: Fri, 16 May 2025 00:34:31 -0400 Subject: [PATCH 0551/1315] CompassTile: Make Compass point towards North (rather than where the user is facing) Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/qs/tiles/CompassTile.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java index e600eca44b9d..edf7703ab565 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CompassTile.java @@ -254,6 +254,9 @@ public void onSensorChanged(SensorEvent event) { Float newDegree = Float.valueOf((float) Math.toDegrees(orientation[0])); newDegree = (newDegree + 360) % 360; + // Convert the angle to one that points north relative to the device + newDegree = -newDegree + 360; + refreshState(newDegree); } From ee0502fc5784caefbf2dcd834099d4327f69da6a Mon Sep 17 00:00:00 2001 From: Christian Oder Date: Tue, 26 Nov 2019 23:35:01 +0100 Subject: [PATCH 0552/1315] SystemUI: Introduce DataSwitchTile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Based on OnePlus' OxygenOS tile, reworked to work with AOSP toggling without requirements on proprietary telephony-ext features @neobuddy89: Updated for Android 14 and squashed below - From: DennySPB Date: Thu, 5 Dec 2019 12:40:17 +0300 Subject: DataSwitchTile: collapse notification panel onClick Change-Id: I37a064c910ea05493b41bdd0123ca3a6aca1f25d Signed-off-by: DennySPB Signed-off-by: Pranav Vashi From: micky387 Date: Wed, 11 Mar 2020 13:36:51 +0100 Subject: DataSwitchTile: dont show toast on click and add drawable for No SIM Change-Id: Ib9d26630879420216e67de1a1e6fc82c5c2ace5e Signed-off-by: Pranav Vashi From: DennySPb Date: Tue, 17 Nov 2020 11:39:43 +0300 Subject: SystemUI: Show carrier name of opposite slot in DataSwitch tile label make it more user friendly Change-Id: I53094db21fe21b1a4bd9ee76fd28661622eb5e26 Signed-off-by: Pranav Vashi From: Ido Ben-Hur Date: Wed, 14 Jul 2021 01:43:13 +0900 Subject: DataSwitchTile: Improve the code * Get rid of deprecated AsyncTask * Removed unused imports * Finalize global vars * Some other formatting and minor stuff Change-Id: Id3e39c98dac6200301d15bf752f866f47875633d Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Mon, 7 Feb 2022 15:06:47 +0530 Subject: DataSwitchTile: Fix issue when subId is non-binary Fixes: https://github.com/crdroidandroid/android_frameworks_base/issues/770 Other changes: 1. Collapse QS panel after switching sim. 2. Change drawable to reflect opposite sim number. 3. Move move opposite sim label to secondary level. Signed-off-by: Pranav Vashi From: ShevT Date: Sat, 12 Feb 2022 00:02:04 +0300 Subject: DataSwitchTile: Resolve initial tile state After loading the system, we do not see on the tile the telecom operator to which we will switch. Instead, we see "On". And so on until we switch the network. If you pull out one SIM, then the name of the operator to which we could switch to will "freeze" on the tile. Let's fix this. @neobuddy89: Improvise logic and rewrite. Change-Id: I7a4996b48e014825c5452c1259454e9b7c70ce82 Signed-off-by: Pranav Vashi From: Joe Maples Date: Sat, 29 Feb 2020 16:38:14 -0500 Subject: DataSwitchTile: Use Mobile Data panel Change-Id: I9f818eea1ab906bb1721e0aab4cbb138187e507b Signed-off-by: SagarMakhar Signed-off-by: Pranav Vashi From: Hernán Castañón Álvarez Date: Thu, 4 Jun 2020 19:08:06 +0000 Subject: [PATCH 0533/1200] DataSwitchTile: Update SIMs QS icons * Designed and made by Andrew Fluck. Change-Id: I1b83c360029382a8a1d0598758cddf1634f7d489 Co-authored-by: Andrew Fluck Signed-off-by: Hernán Castañón Álvarez Signed-off-by: Pranav Vashi From: Pranav Vashi Date: Sat, 25 Mar 2023 14:16:57 +0530 Subject: DataSwitchTile: Show active sim as tile current state Signed-off-by: Pranav Vashi Change-Id: Ie2e280c07f24f9da6b4ee218b72501a2713ce429 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- data/etc/com.android.systemui.xml | 1 + packages/SystemUI/AndroidManifest.xml | 3 + .../res/drawable/ic_qs_data_switch_0.xml | 4 + .../res/drawable/ic_qs_data_switch_1.xml | 19 ++ .../res/drawable/ic_qs_data_switch_2.xml | 19 ++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 3 + .../android/systemui/lineage/LineageModule.kt | 22 ++ .../systemui/qs/tiles/DataSwitchTile.java | 290 ++++++++++++++++++ 9 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_data_switch_0.xml create mode 100644 packages/SystemUI/res/drawable/ic_qs_data_switch_1.xml create mode 100644 packages/SystemUI/res/drawable/ic_qs_data_switch_2.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 8b0b9e639160..7f89fd5fad51 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -99,5 +99,6 @@ + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 995acb6caeb3..0dfc91f08f82 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -412,6 +412,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_data_switch_0.xml b/packages/SystemUI/res/drawable/ic_qs_data_switch_0.xml new file mode 100644 index 000000000000..81b58abe42b3 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_data_switch_0.xml @@ -0,0 +1,4 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_data_switch_1.xml b/packages/SystemUI/res/drawable/ic_qs_data_switch_1.xml new file mode 100644 index 000000000000..dea973a4811d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_data_switch_1.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_data_switch_2.xml b/packages/SystemUI/res/drawable/ic_qs_data_switch_2.xml new file mode 100644 index 000000000000..037e3f108597 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_data_switch_2.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index ec4cbb2fd425..1d11ac01a3de 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 0e5070ebf9db..04a3e8d60532 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -137,4 +137,7 @@ SW W NW + + + Switch data card diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index f9db4d8661ef..40066ace54f2 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.qs.tiles.AODTile import com.android.systemui.qs.tiles.CPUInfoTile import com.android.systemui.qs.tiles.CaffeineTile import com.android.systemui.qs.tiles.CompassTile +import com.android.systemui.qs.tiles.DataSwitchTile import com.android.systemui.qs.tiles.FPSInfoTile import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.OnTheGoTile @@ -77,6 +78,12 @@ interface LineageModule { @StringKey(CompassTile.TILE_SPEC) fun bindCompassTile(compassTile: CompassTile): QSTileImpl<*> + /** Inject DataSwitchTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(DataSwitchTile.TILE_SPEC) + fun bindDataSwitchTile(dataSwitchTile: DataSwitchTile): QSTileImpl<*> + /** Inject FPSInfoTile into tileMap in QSModule */ @Binds @IntoMap @@ -224,6 +231,21 @@ interface LineageModule { category = TileCategory.UTILITIES, ) + @Provides + @IntoMap + @StringKey(DataSwitchTile.TILE_SPEC) + fun provideDataSwitchTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(DataSwitchTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_data_switch_0, + labelRes = R.string.qs_data_switch_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY, + ) + @Provides @IntoMap @StringKey(FPSInfoTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java new file mode 100644 index 000000000000..6b52a6e2bf5e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2020-2026 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs.tiles; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemProperties; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.TelephonyIntents; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon; +import com.android.systemui.res.R; + +import java.util.List; + +import javax.inject.Inject; + +public class DataSwitchTile extends QSTileImpl { + + public static final String TILE_SPEC = "dataswitch"; + + private boolean mCanSwitch = true; + private boolean mRegistered = false; + private int mSimCount = 0; + BroadcastReceiver mSimReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "mSimReceiver:onReceive"); + refreshState(); + } + }; + private final MyCallStateListener mPhoneStateListener; + private final SubscriptionManager mSubscriptionManager; + private final TelephonyManager mTelephonyManager; + private final PanelInteractor mPanelInteractor; + + class MyCallStateListener extends PhoneStateListener { + MyCallStateListener() { + } + + public void onCallStateChanged(int state, String arg1) { + mCanSwitch = mTelephonyManager.getCallState() == 0; + refreshState(); + } + } + + @Inject + public DataSwitchTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + PanelInteractor panelInteractor + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mSubscriptionManager = SubscriptionManager.from(host.getContext()); + mTelephonyManager = TelephonyManager.from(host.getContext()); + mPhoneStateListener = new MyCallStateListener(); + mPanelInteractor = panelInteractor; + } + + @Override + public boolean isAvailable() { + int count = TelephonyManager.getDefault().getPhoneCount(); + Log.d(TAG, "phoneCount: " + count); + return count >= 2; + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + if (listening) { + if (!mRegistered) { + IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + mContext.registerReceiver(mSimReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + mRegistered = true; + } + refreshState(); + } else if (mRegistered) { + mContext.unregisterReceiver(mSimReceiver); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + mRegistered = false; + } + } + + private void updateSimCount() { + String simState = SystemProperties.get("gsm.sim.state"); + Log.d(TAG, "DataSwitchTile:updateSimCount:simState=" + simState); + mSimCount = 0; + try { + String[] sims = TextUtils.split(simState, ","); + for (String sim : sims) { + if (!sim.isEmpty() + && !sim.equalsIgnoreCase(IccCardConstants.INTENT_VALUE_ICC_ABSENT) + && !sim.equalsIgnoreCase(IccCardConstants.INTENT_VALUE_ICC_NOT_READY)) { + mSimCount++; + } + } + } catch (Exception e) { + Log.e(TAG, "Error to parse sim state"); + } + Log.d(TAG, "DataSwitchTile:updateSimCount:mSimCount=" + mSimCount); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + if (!mCanSwitch) { + Log.d(TAG, "Call state=" + mTelephonyManager.getCallState()); + } else if (mSimCount == 0) { + Log.d(TAG, "handleClick:no sim card"); + } else if (mSimCount == 1) { + Log.d(TAG, "handleClick:only one sim card"); + } else { + AsyncTask.execute(() -> { + toggleMobileDataEnabled(); + refreshState(); + }); + mPanelInteractor.collapsePanels(); + } + } + + @Override + public Intent getLongClickIntent() { + return new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.qs_data_switch_label); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + boolean activeSIMZero; + if (arg == null) { + int defaultPhoneId = mSubscriptionManager.getPhoneId( + mSubscriptionManager.getDefaultDataSubscriptionId()); + Log.d(TAG, "default data phone id=" + defaultPhoneId); + activeSIMZero = defaultPhoneId == 0; + } else { + activeSIMZero = (Boolean) arg; + } + updateSimCount(); + switch (mSimCount) { + case 0: + state.icon = maybeLoadResourceIcon(R.drawable.ic_qs_data_switch_0); + state.value = false; + state.secondaryLabel = mContext.getString(R.string.tile_unavailable); + break; + case 1: + state.icon = maybeLoadResourceIcon(activeSIMZero + ? R.drawable.ic_qs_data_switch_1 + : R.drawable.ic_qs_data_switch_2); + state.value = false; + state.secondaryLabel = mContext.getString(R.string.tile_unavailable); + break; + case 2: + state.icon = maybeLoadResourceIcon(activeSIMZero + ? R.drawable.ic_qs_data_switch_1 + : R.drawable.ic_qs_data_switch_2); + state.value = true; + state.secondaryLabel = getActiveSlotName(); + break; + default: + state.icon = maybeLoadResourceIcon(R.drawable.ic_qs_data_switch_1); + state.value = false; + state.secondaryLabel = mContext.getString(R.string.tile_unavailable); + break; + } + if (mSimCount < 2) { + state.state = 0; + } else if (!mCanSwitch) { + state.state = 0; + Log.d(TAG, "call state isn't idle, set to unavailable."); + } else { + state.state = state.value ? 2 : 1; + } + + state.label = mContext.getString(R.string.qs_data_switch_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + /** + * Set whether to enable data for {@code subId}, also whether to disable data for other + * subscription + */ + private void toggleMobileDataEnabled() { + TelephonyManager telephonyManager; + boolean dataEnabled = false; + boolean foundActive = false; + int subId; + List subInfoList = + mSubscriptionManager.getActiveSubscriptionInfoList(true); + if (subInfoList != null) { + for (SubscriptionInfo subInfo : subInfoList) { + subId = subInfo.getSubscriptionId(); + telephonyManager = + mTelephonyManager.createForSubscriptionId(subId); + dataEnabled = telephonyManager.getDataEnabled(); + if (subInfo.isOpportunistic() && dataEnabled) { + // We never disable mobile data for opportunistic subscriptions. + continue; + } else { + dataEnabled = !dataEnabled && !foundActive; + telephonyManager.setDataEnabled(dataEnabled); + if (dataEnabled) mSubscriptionManager.setDefaultDataSubId(subId); + // Indicate we found sim with active data, disable data on remaining sim. + if (!foundActive) foundActive = dataEnabled; + } + Log.d(TAG, "Changed subID " + subId + " to " + + !dataEnabled); + } + } + } + + private String getActiveSlotName() { + TelephonyManager telephonyManager; + String mInitialState = mContext.getString(R.string.tile_unavailable); + List subInfoList = + mSubscriptionManager.getActiveSubscriptionInfoList(true); + if (subInfoList != null) { + for (SubscriptionInfo subInfo : subInfoList) { + telephonyManager = + mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId()); + if (telephonyManager.getDataEnabled()) { + // Active SIM found + return subInfo.getDisplayName().toString(); + } + } + } + return mInitialState; + } +} From ff6bdb4cac5d7dcc3cdc1cda957d214a9aad61b7 Mon Sep 17 00:00:00 2001 From: stofstik Date: Mon, 17 Aug 2015 17:42:53 +0200 Subject: [PATCH 0553/1315] SystemUI: Add tile to show volume panel Squashed: From: Joey Huab Date: Wed, 18 Mar 2020 16:23:51 +0000 Subject: QS: Use Settings.Panel intent for Volume Tile Signed-off-by: Joey Huab Signed-off-by: Pranav Vashi Change-Id: Ic583c24e304d1edc903127237fc9eb5c7eeb7e4d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_qs_volume_panel.xml | 27 +++++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 3 + .../android/systemui/lineage/LineageModule.kt | 22 ++++ .../android/systemui/qs/tiles/VolumeTile.java | 106 ++++++++++++++++++ 5 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_volume_panel.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_volume_panel.xml b/packages/SystemUI/res/drawable/ic_qs_volume_panel.xml new file mode 100644 index 000000000000..e7c51d2a61ca --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_volume_panel.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 1d11ac01a3de..ed0dcc20b7a1 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 04a3e8d60532..9dd5599762e6 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -140,4 +140,7 @@ Switch data card + + + Volume panel diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 40066ace54f2..9528c4036d36 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -35,6 +35,7 @@ import com.android.systemui.qs.tiles.ReadingModeTile import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile +import com.android.systemui.qs.tiles.VolumeTile import com.android.systemui.qs.tiles.VpnTile import com.android.systemui.qs.tiles.base.shared.model.QSTileConfig import com.android.systemui.qs.tiles.base.shared.model.QSTileUIConfig @@ -138,6 +139,12 @@ interface LineageModule { @StringKey(UsbTetherTile.TILE_SPEC) fun bindUsbTetherTile(usbTetherTile: UsbTetherTile): QSTileImpl<*> + /** Inject VolumeTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(VolumeTile.TILE_SPEC) + fun bindVolumeTile(volumeTile: VolumeTile): QSTileImpl<*> + /** Inject VpnTile into tileMap in QSModule */ @Binds @IntoMap @@ -381,6 +388,21 @@ interface LineageModule { category = TileCategory.CONNECTIVITY, ) + @Provides + @IntoMap + @StringKey(VolumeTile.TILE_SPEC) + fun provideVolumeTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(VolumeTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_volume_panel, + labelRes = R.string.quick_settings_volume_panel_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(VPN_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java new file mode 100644 index 000000000000..29808828e311 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/VolumeTile.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * Copyright (C) 2017 The LineageOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.media.AudioManager; +import android.provider.Settings; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; + +import javax.inject.Inject; + +public class VolumeTile extends QSTileImpl { + + public static final String TILE_SPEC = "volume_panel"; + + private static final Intent SOUND_SETTINGS = new Intent(Settings.Panel.ACTION_VOLUME); + + @Inject + public VolumeTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + AudioManager am = mContext.getSystemService(AudioManager.class); + am.adjustVolume(AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI); + } + + @Override + public Intent getLongClickIntent() { + return SOUND_SETTINGS; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.label = mContext.getString(R.string.quick_settings_volume_panel_label); + state.icon = ResourceIcon.get(R.drawable.ic_qs_volume_panel); // TODO needs own icon + state.state = Tile.STATE_ACTIVE; + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_volume_panel_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + // Do nothing + } +} From a6a3411412c56536d313f8a0aecd68c84f83d515 Mon Sep 17 00:00:00 2001 From: Adin Kwok Date: Sat, 7 Apr 2018 20:08:45 -0700 Subject: [PATCH 0554/1315] SystemUI: Add Smart Pixels tile Single tap enables/disables. Long press opens Smart Pixels settings. User is not allowed to enable or disable when Smart Pixels has been auto-enabled on battery saver. Squashed: From: Pranav Vashi Date: Sun, 7 Jul 2019 11:56:12 +0530 Subject: Fix long click intent for Smart Pixels tile [1/2] Signed-off-by: Pranav Vashi Change-Id: I535c84a0ca6360e1db351391e9d0fb9095896ee3 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_qs_smart_pixels.xml | 8 + packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 4 + .../android/systemui/lineage/LineageModule.kt | 22 +++ .../systemui/qs/tiles/SmartPixelsTile.java | 183 ++++++++++++++++++ 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml b/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml new file mode 100644 index 000000000000..525321baff96 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index ed0dcc20b7a1..df645eaee5c6 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 9dd5599762e6..cc4a92a01cf9 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -143,4 +143,8 @@ Volume panel + + + Smart Pixels + Auto-enabled Smart Pixels diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 9528c4036d36..227777880046 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile +import com.android.systemui.qs.tiles.SmartPixelsTile import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile @@ -121,6 +122,12 @@ interface LineageModule { @StringKey(ReadingModeTile.TILE_SPEC) fun bindReadingModeTile(readingModeTile: ReadingModeTile): QSTileImpl<*> + /** Inject SmartPixelsTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(SmartPixelsTile.TILE_SPEC) + fun bindSmartPixelsTile(smartPixelsTile: SmartPixelsTile): QSTileImpl<*> + /** Inject SoundTile into tileMap in QSModule */ @Binds @IntoMap @@ -343,6 +350,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(SmartPixelsTile.TILE_SPEC) + fun provideSmartPixelsTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(SmartPixelsTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_smart_pixels, + labelRes = R.string.quick_settings_smart_pixels + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.DISPLAY, + ) + @Provides @IntoMap @StringKey(SoundTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java new file mode 100644 index 000000000000..8755bd217fa3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2018 CarbonROM + * Copyright (C) 2018 Adin Kwok (adinkwok) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; +import com.android.systemui.statusbar.policy.BatteryController; + +import javax.inject.Inject; + +public class SmartPixelsTile extends QSTileImpl implements + BatteryController.BatteryStateChangeCallback { + + public static final String TILE_SPEC = "smartpixels"; + + + private static final Intent SMART_PIXELS_SETTINGS = new Intent("android.settings.SMART_PIXELS_SETTINGS"); + + private final BatteryController mBatteryController; + + private boolean mSmartPixelsEnable; + private boolean mSmartPixelsOnPowerSave; + private boolean mLowPowerMode; + private boolean mListening; + + @Nullable + private Icon mIcon = null; + + @Inject + public SmartPixelsTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + BatteryController batteryController) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mBatteryController = batteryController; + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + if (listening) { + mBatteryController.addCallback(this); + } else { + mBatteryController.removeCallback(this); + } + } + + @Override + public boolean isAvailable() { + return mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_supportSmartPixels); + } + + @Override + public void handleClick(@Nullable Expandable expandable) { + mSmartPixelsEnable = (Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, + 0, UserHandle.USER_CURRENT) == 1); + mSmartPixelsOnPowerSave = (Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 0, UserHandle.USER_CURRENT) == 1); + if (mLowPowerMode && mSmartPixelsOnPowerSave) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 0, UserHandle.USER_CURRENT); + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ENABLE, + 0, UserHandle.USER_CURRENT); + } else if (!mSmartPixelsEnable) { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ENABLE, + 1, UserHandle.USER_CURRENT); + } else { + Settings.System.putIntForUser(mContext.getContentResolver(), + Settings.System.SMART_PIXELS_ENABLE, + 0, UserHandle.USER_CURRENT); + } + refreshState(); + } + + @Override + public Intent getLongClickIntent() { + return SMART_PIXELS_SETTINGS; + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + mSmartPixelsEnable = (Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, + 0, UserHandle.USER_CURRENT) == 1); + mSmartPixelsOnPowerSave = (Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, + 0, UserHandle.USER_CURRENT) == 1); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_smart_pixels); + } + state.icon = mIcon; + if (mLowPowerMode && mSmartPixelsOnPowerSave) { + state.label = mContext.getString(R.string.quick_settings_smart_pixels_on_power_save); + state.value = true; + } else if (mSmartPixelsEnable) { + state.label = mContext.getString(R.string.quick_settings_smart_pixels); + state.value = true; + } else { + state.label = mContext.getString(R.string.quick_settings_smart_pixels); + state.value = false; + } + state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_smart_pixels); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public void onBatteryLevelChanged(int level, boolean plugged, boolean charging) { + // yurt + } + + @Override + public void onPowerSaveChanged(boolean active) { + mLowPowerMode = active; + refreshState(); + } +} From f0d180fd14d490aeedf75f8744a4e8967b507f17 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 16 Oct 2022 14:43:40 +0530 Subject: [PATCH 0555/1315] SystemUI: Add Weather tile based on OmniJaws client * Initial code based on 11.0 commit https://github.com/crdroidandroid/android_frameworks_base/commit/97734230d0c1f92f5bc87ff8a515b198d92cdd15 * Removed detailed adapter - deprecated now. :( * Simplify code and strings * Use location as tile label when enabled * handleClick will open settings when disabled, will open weather app when enabled * handleLongClick will always open settings Squashed: From: minaripenguin Date: Sun, 8 Sep 2024 19:11:39 +0800 Subject: CurrentWeather: Prevent possible memory leak Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi From: minaripenguin Date: Thu, 29 Aug 2024 09:12:16 +0800 Subject: Weather Tile: Launch Omnijaws weather activity if there are no weather apps available Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_qs_weather.xml | 10 + packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 5 + .../android/systemui/lineage/LineageModule.kt | 22 ++ .../systemui/qs/tiles/WeatherTile.java | 237 ++++++++++++++++++ 5 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_weather.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_weather.xml b/packages/SystemUI/res/drawable/ic_qs_weather.xml new file mode 100644 index 000000000000..edd144fcef82 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_weather.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index df645eaee5c6..c9b9743e5712 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index cc4a92a01cf9..e5d4b7bb1627 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -147,4 +147,9 @@ Smart Pixels Auto-enabled Smart Pixels + + + Error loading weather data + No weather data + Weather diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 227777880046..a53426217dad 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -38,6 +38,7 @@ import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile import com.android.systemui.qs.tiles.VolumeTile import com.android.systemui.qs.tiles.VpnTile +import com.android.systemui.qs.tiles.WeatherTile import com.android.systemui.qs.tiles.base.shared.model.QSTileConfig import com.android.systemui.qs.tiles.base.shared.model.QSTileUIConfig import com.android.systemui.res.R @@ -158,6 +159,12 @@ interface LineageModule { @StringKey(VpnTile.TILE_SPEC) fun bindVpnTile(vpnTile: VpnTile): QSTileImpl<*> + /** Inject WeatherTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(WeatherTile.TILE_SPEC) + fun bindWeatherTile(weatherTile: WeatherTile): QSTileImpl<*> + companion object { const val AMBIENT_DISPLAY_TILE_SPEC = "ambient_display" const val AOD_TILE_SPEC = "aod" @@ -439,5 +446,20 @@ interface LineageModule { instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.CONNECTIVITY, ) + + @Provides + @IntoMap + @StringKey(WeatherTile.TILE_SPEC) + fun provideWeatherTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(WeatherTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_weather, + labelRes = R.string.omnijaws_label_default + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java new file mode 100644 index 000000000000..5ed4a8c71c82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2017 The OmniROM project + * Copyright (C) 2022-2025 crDroid Android project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.service.quicksettings.Tile; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.matrixx.OmniJawsClient; +import com.android.internal.util.matrixx.Utils; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; + +import javax.inject.Inject; + +public class WeatherTile extends QSTileImpl implements OmniJawsClient.OmniJawsObserver { + + public static final String TILE_SPEC = "weather"; + private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; + + private static final String TAG = "WeatherTile"; + private static final boolean DEBUG = false; private Drawable mWeatherImage; + private OmniJawsClient.WeatherInfo mWeatherData; + private boolean mEnabled; + private final ActivityStarter mActivityStarter; + private String mFormattedCondition; + + private static final String[] ALTERNATIVE_WEATHER_APPS = { + "cz.martykan.forecastie", + "com.accuweather.android", + "com.wunderground.android.weather", + "com.samruston.weather", + "jp.miyavi.androiod.gnws", + }; + + @Inject + public WeatherTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); + mActivityStarter = activityStarter; + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + if (DEBUG) Log.d(TAG, "setListening " + listening); + mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); + + if (listening) { + OmniJawsClient.get().addObserver(mContext, this); + queryAndUpdateWeather(); + } else { + OmniJawsClient.get().removeObserver(mContext, this); + } + } + + @Override + public void weatherUpdated() { + if (DEBUG) Log.d(TAG, "weatherUpdated"); + queryAndUpdateWeather(); + } + + @Override + public void weatherError(int errorReason) { + if (DEBUG) Log.d(TAG, "weatherError " + errorReason); + if (errorReason != OmniJawsClient.EXTRA_ERROR_DISABLED) { + mWeatherData = null; + refreshState(); + } + } + + @Override + protected void handleDestroy() { + OmniJawsClient.get().removeObserver(mContext, this); + super.handleDestroy(); + } + + @Override + public boolean isAvailable() { + return OmniJawsClient.get().isOmniJawsServiceInstalled(mContext); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + if (DEBUG) Log.d(TAG, "handleClick"); + if (!mState.value || mWeatherData == null) { + mActivityStarter.postStartActivityDismissingKeyguard( + OmniJawsClient.get().getSettingsIntent(), 0); + } else { + PackageManager pm = mContext.getPackageManager(); + for (String app: ALTERNATIVE_WEATHER_APPS) { + if (Utils.isPackageInstalled(mContext, app)) { + Intent intent = pm.getLaunchIntentForPackage(app); + if (intent != null) { + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + } + } + } + if (Utils.isPackageInstalled(mContext, "com.google.android.googlequicksearchbox")) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("dynact://velour/weather/ProxyActivity")); + intent.setComponent(new ComponentName("com.google.android.googlequicksearchbox", + "com.google.android.apps.gsa.velour.DynamicActivityTrampoline")); + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + } else { + final Intent weatherActivityIntent = new Intent(); + weatherActivityIntent.setAction(Intent.ACTION_MAIN); + weatherActivityIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); + mActivityStarter.postStartActivityDismissingKeyguard(weatherActivityIntent, 0); + } + } + mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); + refreshState(); + } + + @Override + public Intent getLongClickIntent() { + if (DEBUG) Log.d(TAG, "getLongClickIntent"); + return OmniJawsClient.get().getSettingsIntent(); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + if (DEBUG) Log.d(TAG, "handleUpdateState " + mEnabled); + state.value = mEnabled; + state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.icon = ResourceIcon.get(R.drawable.ic_qs_weather); + state.label = mContext.getResources().getString(R.string.omnijaws_label_default); + state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_unknown); + if (mEnabled) { + if (mWeatherData == null || mWeatherImage == null) { + state.label = mContext.getResources().getString(R.string.omnijaws_label_default); + state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_error); + } else { + state.icon = new DrawableIcon(mWeatherImage); + state.label = mWeatherData.city; + state.secondaryLabel = mWeatherData.temp + mWeatherData.tempUnits + + " · " + mFormattedCondition; + } + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getResources().getString(R.string.omnijaws_label_default); + } + + private void queryAndUpdateWeather() { + if (DEBUG) Log.d(TAG, "queryAndUpdateWeather " + mEnabled); + try { + mWeatherData = null; + if (mEnabled) { + OmniJawsClient.get().queryWeather(mContext); + mWeatherData = OmniJawsClient.get().getWeatherInfo(); + mFormattedCondition = mWeatherData.condition; + if (mFormattedCondition.toLowerCase().contains("clouds")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clouds); + } else if (mFormattedCondition.toLowerCase().contains("rain")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_rain); + } else if (mFormattedCondition.toLowerCase().contains("clear")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clear); + } else if (mFormattedCondition.toLowerCase().contains("storm")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_storm); + } else if (mFormattedCondition.toLowerCase().contains("snow")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_snow); + } else if (mFormattedCondition.toLowerCase().contains("wind")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_wind); + } else if (mFormattedCondition.toLowerCase().contains("mist")) { + mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_mist); + } + if (mWeatherData != null) { + mWeatherImage = OmniJawsClient.get().getWeatherConditionImage(mContext, mWeatherData.conditionCode); + mWeatherImage = mWeatherImage.mutate(); + } + } + } catch(Exception e) { + // Do nothing + } + refreshState(); + } +} From 89a09c7831756af8cb18fe7782b5d18497d27636 Mon Sep 17 00:00:00 2001 From: Arindam Bhattacharjee Date: Fri, 30 May 2025 03:14:24 +0900 Subject: [PATCH 0556/1315] BluetoothPowerStatsCollector: Handle onBluetoothActivityEnergyInfoError gracefully Avoids crashing or logging unhandled exceptions when the Bluetooth energy info callback returns an error. This ensures the stats collection proceeds even if BluetoothActivityEnergyInfo is unavailable or fails. E BluetoothPowerStatsCollector: Cannot acquire BluetoothActivityEnergyInfo E BluetoothPowerStatsCollector: java.util.concurrent.ExecutionException: java.lang.RuntimeException: error: 9 E BluetoothPowerStatsCollector: at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:372) E BluetoothPowerStatsCollector: at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2072) E BluetoothPowerStatsCollector: at com.android.server.power.stats.BluetoothPowerStatsCollector.collectBluetoothActivityInfo(BluetoothPowerStatsCollector.java:192) E BluetoothPowerStatsCollector: at com.android.server.power.stats.BluetoothPowerStatsCollector.collectStats(BluetoothPowerStatsCollector.java:155) E BluetoothPowerStatsCollector: at com.android.server.power.stats.PowerStatsCollector.collectAndDeliverStats(PowerStatsCollector.java:176) E BluetoothPowerStatsCollector: at com.android.server.power.stats.PowerStatsCollector$$ExternalSyntheticLambda0.run(R8$$SyntheticClass:0) E BluetoothPowerStatsCollector: at android.os.Handler.handleCallback(Handler.java:995) E BluetoothPowerStatsCollector: at android.os.Handler.dispatchMessage(Handler.java:103) E BluetoothPowerStatsCollector: at android.os.Looper.loopOnce(Looper.java:248) E BluetoothPowerStatsCollector: at android.os.Looper.loop(Looper.java:338) E BluetoothPowerStatsCollector: at android.os.HandlerThread.run(HandlerThread.java:85) E BluetoothPowerStatsCollector: Caused by: java.lang.RuntimeException: error: 9 E BluetoothPowerStatsCollector: at com.android.server.power.stats.BluetoothPowerStatsCollector$1.onBluetoothActivityEnergyInfoError(BluetoothPowerStatsCollector.java:181) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothAdapter$OnBluetoothActivityEnergyInfoProxy.lambda$onError$2(BluetoothAdapter.java:1059) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothAdapter$OnBluetoothActivityEnergyInfoProxy$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothUtils.lambda$executeFromBinder$0(BluetoothUtils.java:343) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothUtils$$ExternalSyntheticLambda1.run(D8$$SyntheticClass:0) E BluetoothPowerStatsCollector: at com.android.server.SystemServerInitThreadPool$$ExternalSyntheticLambda0.execute(R8$$SyntheticClass:0) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothUtils.executeFromBinder(BluetoothUtils.java:343) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothAdapter$OnBluetoothActivityEnergyInfoProxy.onError(BluetoothAdapter.java:1058) E BluetoothPowerStatsCollector: at android.bluetooth.BluetoothAdapter.requestControllerActivityEnergyInfo(BluetoothAdapter.java:2694) E BluetoothPowerStatsCollector: at com.android.server.power.stats.BatteryStatsImpl$BluetoothStatsRetrieverImpl.requestControllerActivityEnergyInfo(BatteryStatsImpl.java:485) E BluetoothPowerStatsCollector: at com.android.server.power.stats.BluetoothPowerStatsCollector.collectBluetoothActivityInfo(BluetoothPowerStatsCollector.java:170) E BluetoothPowerStatsCollector: ... 8 more Change-Id: I143e701db08ab514c9f161445d515542c6a6cf71 Signed-off-by: Arindam Bhattacharjee Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/power/stats/BluetoothPowerStatsCollector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java index e0ea7b0359b2..913a62d4d958 100644 --- a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java @@ -177,8 +177,8 @@ public void onBluetoothActivityEnergyInfoAvailable( @Override public void onBluetoothActivityEnergyInfoError(int error) { - immediateFuture.completeExceptionally( - new RuntimeException("error: " + error)); + Slog.w(TAG, "BluetoothActivityEnergyInfo request failed with error code: " + error); + immediateFuture.complete(null); } }); @@ -191,7 +191,7 @@ public void onBluetoothActivityEnergyInfoError(int error) { activityInfo = immediateFuture.get(BLUETOOTH_ACTIVITY_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS); } catch (Exception e) { - Slog.e(TAG, "Cannot acquire BluetoothActivityEnergyInfo", e); + Slog.w(TAG, "BluetoothActivityEnergyInfo unavailable, skipping stats collection", e); activityInfo = null; } From b8542823ab14500f20b13ae791558fe959fec574 Mon Sep 17 00:00:00 2001 From: jhonboy121 Date: Tue, 14 Sep 2021 20:23:04 +0530 Subject: [PATCH 0557/1315] SystemUI: Add refresh rate tile Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_refresh_rate.xml | 24 ++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 4 + .../android/systemui/lineage/LineageModule.kt | 22 ++ .../systemui/qs/tiles/RefreshRateTile.kt | 255 ++++++++++++++++++ 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_refresh_rate.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt diff --git a/packages/SystemUI/res/drawable/ic_refresh_rate.xml b/packages/SystemUI/res/drawable/ic_refresh_rate.xml new file mode 100644 index 000000000000..80abf8eb6ad8 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_refresh_rate.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index c9b9743e5712..c3a740ad935f 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index e5d4b7bb1627..af4335fe362c 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -152,4 +152,8 @@ Error loading weather data No weather data Weather + + + Refresh rate + Auto diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index a53426217dad..0d4580540f52 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile +import com.android.systemui.qs.tiles.RefreshRateTile import com.android.systemui.qs.tiles.SmartPixelsTile import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile @@ -123,6 +124,12 @@ interface LineageModule { @StringKey(ReadingModeTile.TILE_SPEC) fun bindReadingModeTile(readingModeTile: ReadingModeTile): QSTileImpl<*> + /** Inject RefreshRateTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(RefreshRateTile.TILE_SPEC) + fun bindRefreshRateTile(refreshRateTile: RefreshRateTile): QSTileImpl<*> + /** Inject SmartPixelsTile into tileMap in QSModule */ @Binds @IntoMap @@ -357,6 +364,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(RefreshRateTile.TILE_SPEC) + fun provideRefreshRateTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(RefreshRateTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_refresh_rate, + labelRes = R.string.refresh_rate_tile_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.DISPLAY, + ) + @Provides @IntoMap @StringKey(SmartPixelsTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt new file mode 100644 index 000000000000..9782f37b8fa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * 2021 AOSP-Krypton Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.qs.tiles + +import android.content.ComponentName +import android.content.Intent +import android.database.ContentObserver +import android.hardware.display.DisplayManager +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.DeviceConfig +import android.provider.Settings.System.MIN_REFRESH_RATE +import android.provider.Settings.System.PEAK_REFRESH_RATE +import android.service.quicksettings.Tile +import android.util.Log +import android.view.Display + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent +import com.android.internal.logging.MetricsLogger +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.qs.QSTile.Icon +import com.android.systemui.plugins.qs.QSTile.State +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QsEventLogger +import com.android.systemui.res.R +import com.android.systemui.util.settings.SystemSettings + +import javax.inject.Inject + +class RefreshRateTile @Inject constructor( + host: QSHost, + uiEventLogger: QsEventLogger, + @Background backgroundLooper: Looper, + @Main private val mainHandler: Handler, + falsingManager: FalsingManager, + metricsLogger: MetricsLogger, + statusBarStateController: StatusBarStateController, + activityStarter: ActivityStarter, + qsLogger: QSLogger, + private val systemSettings: SystemSettings, +): QSTileImpl( + host, + uiEventLogger, + backgroundLooper, + mainHandler, + falsingManager, + metricsLogger, + statusBarStateController, + activityStarter, + qsLogger, +) { + + private val settingsObserver: SettingsObserver + private val tileLabel: String + private val autoModeLabel: String + private val defaultPeakRefreshRate: Float + + private var ignoreSettingsChange = false + private var refreshRateMode = Mode.MIN + private var peakRefreshRate = DEFAULT_REFRESH_RATE + + init { + with (mContext.resources) { + tileLabel = getString(R.string.refresh_rate_tile_label) + autoModeLabel = getString(R.string.auto_mode_label) + defaultPeakRefreshRate = getDefaultPeakRefreshRate(getInteger( + com.android.internal.R.integer.config_defaultPeakRefreshRate).toFloat()) + } + + val display: Display? = mContext.getSystemService( + DisplayManager::class.java)!!.getDisplay(Display.DEFAULT_DISPLAY) + display?.let { + it.getSupportedModes().forEach({ mode -> + mode.refreshRate.let { rr -> + if (rr > peakRefreshRate) peakRefreshRate = rr + } + }) + } ?: run { Log.w(Companion.TAG, "No valid default display") } + logD("peakRefreshRate = $peakRefreshRate, defaultPeakRefreshRate = $defaultPeakRefreshRate") + settingsObserver = SettingsObserver() + } + + override fun newTileState() = + State().also { + it.icon = icon + it.state = Tile.STATE_ACTIVE + } + + override fun getLongClickIntent() = displaySettingsIntent + + override fun isAvailable(): Boolean { + val displayManager = mContext.getSystemService(DisplayManager::class.java) + val display: Display? = displayManager?.getDisplay(Display.DEFAULT_DISPLAY) + display?.let { + val supportedRefreshRates = it.supportedModes.map { mode -> mode.refreshRate }.distinct() + return supportedRefreshRates.size > 1 + } + return false + } + + override fun getTileLabel(): CharSequence = tileLabel + + override protected fun handleInitialize() { + logD("handleInitialize") + updateMode() + settingsObserver.observe() + } + + override protected fun handleClick(expandable: Expandable?) { + logD("handleClick") + refreshRateMode = getNextMode(refreshRateMode) + logD("refreshRateMode = $refreshRateMode") + updateRefreshRateForMode(refreshRateMode) + refreshState() + } + + override protected fun handleUpdateState(state: State, arg: Any?) { + if (state.label == null) { + state.label = tileLabel + state.contentDescription = tileLabel + } + logD("handleUpdateState, state = $state") + state.secondaryLabel = getTitleForMode(refreshRateMode) + logD("secondaryLabel = ${state.secondaryLabel}") + } + + override fun getMetricsCategory(): Int = MetricsEvent.MATRIXX + + override fun destroy() { + settingsObserver.unobserve() + super.destroy() + } + + private fun updateMode() { + val minRate = systemSettings.getFloat(MIN_REFRESH_RATE, DEFAULT_REFRESH_RATE) + val maxRate = systemSettings.getFloat(PEAK_REFRESH_RATE, defaultPeakRefreshRate) + logD("minRate = $minRate, maxRate = $maxRate") + + if (minRate == maxRate) { + if (minRate == DEFAULT_REFRESH_RATE) refreshRateMode = Mode.MIN + else refreshRateMode = Mode.MAX + } else { + refreshRateMode = Mode.AUTO + } + logD("refreshRateMode = $refreshRateMode") + } + + private fun getDefaultPeakRefreshRate(def: Float): Float { + return DeviceConfig.getFloat(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, def) + } + + private fun getNextMode(mode: Mode) = + when (mode) { + Mode.AUTO -> Mode.MIN + Mode.MIN -> Mode.MAX + Mode.MAX -> Mode.AUTO + } + + private fun updateRefreshRateForMode(mode: Mode) { + logD("updateRefreshRateForMode, mode = $mode") + var minRate: Float; var maxRate: Float + when (mode) { + Mode.AUTO -> { + minRate = DEFAULT_REFRESH_RATE + maxRate = peakRefreshRate + } + Mode.MAX -> { + minRate = peakRefreshRate + maxRate = peakRefreshRate + } + Mode.MIN -> { + minRate = DEFAULT_REFRESH_RATE + maxRate = DEFAULT_REFRESH_RATE + } + } + ignoreSettingsChange = true + systemSettings.putFloat(MIN_REFRESH_RATE, minRate) + systemSettings.putFloat(PEAK_REFRESH_RATE, maxRate) + ignoreSettingsChange = false + } + + private fun getTitleForMode(mode: Mode) = + when (mode) { + Mode.AUTO -> autoModeLabel + Mode.MAX -> peakRefreshRate.toInt().toString() + "Hz" + Mode.MIN -> DEFAULT_REFRESH_RATE.toInt().toString() + "Hz" + } + + private enum class Mode { + MIN, + MAX, + AUTO, + } + + private inner class SettingsObserver: ContentObserver(mainHandler) { + private var isObserving = false + + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (!ignoreSettingsChange) updateMode() + } + + fun observe() { + if (isObserving) return + isObserving = true + systemSettings.registerContentObserverSync(MIN_REFRESH_RATE, this) + systemSettings.registerContentObserverSync(PEAK_REFRESH_RATE, this) + } + + fun unobserve() { + if (!isObserving) return + isObserving = false + systemSettings.unregisterContentObserverSync(this) + } + } + + companion object { + const val TILE_SPEC = "refresh_rate" + private const val TAG = "RefreshRateTile" + private const val DEBUG = false + + private const val DEFAULT_REFRESH_RATE = 60f + + private val icon: Icon = ResourceIcon.get(R.drawable.ic_refresh_rate) + private val displaySettingsIntent = Intent().setComponent(ComponentName("com.android.settings", + "com.android.settings.Settings\$DisplaySettingsActivity")) + + private fun logD(msg: String) { + if (DEBUG) Log.d(TAG, msg) + } + } +} From ec1adccdab2a9b8c0d7a94cbdd24fe0750098fa9 Mon Sep 17 00:00:00 2001 From: ShevT Date: Mon, 19 Dec 2022 16:24:06 +0300 Subject: [PATCH 0558/1315] SystemUI: Add Screenshot tile OmniROM: From: maxwen Date: Wed, 14 Oct 2020 17:17:31 +0200 Subject: [PATCH] SystemUI: qs tiles revenge -screenshot [micky387] * edit to the new A12 Tiles API * Add secondaryLabel for the long press screenshot tile From: micky387 Date: Wed, 14 Dec 2022 13:12:26 +0100 Subject: [PATCH] SystemUI: Update ScreenshotTile after merge Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 3 + .../android/systemui/lineage/LineageModule.kt | 22 +++ .../systemui/qs/tiles/ScreenshotTile.java | 143 ++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenshotTile.java diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index c3a740ad935f..e3b69736ee02 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index af4335fe362c..3125764facae 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -156,4 +156,7 @@ Refresh rate Auto + + + Long press - selected region diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 0d4580540f52..7442609f9193 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile import com.android.systemui.qs.tiles.RefreshRateTile +import com.android.systemui.qs.tiles.ScreenshotTile import com.android.systemui.qs.tiles.SmartPixelsTile import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile @@ -130,6 +131,12 @@ interface LineageModule { @StringKey(RefreshRateTile.TILE_SPEC) fun bindRefreshRateTile(refreshRateTile: RefreshRateTile): QSTileImpl<*> + /** Inject ScreenshotTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(ScreenshotTile.TILE_SPEC) + fun bindScreenshotTile(screenshotTile: ScreenshotTile): QSTileImpl<*> + /** Inject SmartPixelsTile into tileMap in QSModule */ @Binds @IntoMap @@ -379,6 +386,21 @@ interface LineageModule { category = TileCategory.DISPLAY, ) + @Provides + @IntoMap + @StringKey(ScreenshotTile.TILE_SPEC) + fun provideScreenshotTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(ScreenshotTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = com.android.internal.R.drawable.ic_screenshot, + labelRes = com.android.internal.R.string.global_action_screenshot + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.UTILITIES, + ) + @Provides @IntoMap @StringKey(SmartPixelsTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenshotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenshotTile.java new file mode 100644 index 000000000000..c021026cc131 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenshotTile.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2020 The OmniROM Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; +import android.service.quicksettings.Tile; +import android.view.WindowManager; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; +import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; + +import androidx.annotation.Nullable; + +import com.android.internal.util.ScreenshotHelper; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.internal.R; + +import javax.inject.Inject; + +/** Quick settings tile: Screenshot **/ +public class ScreenshotTile extends QSTileImpl { + + public static final String TILE_SPEC = "screenshot"; + + private final PanelInteractor mPanelInteractor; + + @Nullable + private Icon mIcon = null; + + @Inject + public ScreenshotTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + PanelInteractor panelInteractor + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mPanelInteractor = panelInteractor; + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) {} + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void handleClick(@Nullable Expandable expandable) { + mPanelInteractor.collapsePanels(); + final ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); + mHandler.postDelayed(() -> { + screenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); + }, 1000); + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + public void handleLongClick(@Nullable Expandable expandable) { + mPanelInteractor.collapsePanels(); + final ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); + mHandler.postDelayed(() -> { + screenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); + }, 1000); + refreshState(); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.global_action_screenshot); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.label = mContext.getString( + R.string.global_action_screenshot); + state.contentDescription = mContext.getString( + R.string.global_action_screenshot); + state.secondaryLabel = mContext.getString( + com.android.systemui.res.R.string.screenshot_long_press); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_screenshot); + } + state.icon = mIcon; + state.value = true; + state.state = Tile.STATE_INACTIVE; + } +} From 20a905ad03d667bd4df24db8b6de94509368f834 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Sun, 22 Jan 2023 23:11:14 +0300 Subject: [PATCH 0559/1315] SystemUI: Add Locale Tile Toggle the primary default language - Bring it to Oreo - LorDClockaN - Bring it to Pie, 11, 12 - eyosen - Bring it to T - semdoc - Bring it to A14 - neobuddy89 Change-Id: I3e336eddb360ea1a796727657652ad9301007380 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_qs_locale.xml | 23 ++ .../res/drawable/ic_qs_locale_pending.xml | 23 ++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 4 + .../android/systemui/lineage/LineageModule.kt | 22 ++ .../android/systemui/qs/tiles/LocaleTile.java | 198 ++++++++++++++++++ 6 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_locale.xml create mode 100644 packages/SystemUI/res/drawable/ic_qs_locale_pending.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_locale.xml b/packages/SystemUI/res/drawable/ic_qs_locale.xml new file mode 100644 index 000000000000..b38cf4cd22fe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_locale.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_locale_pending.xml b/packages/SystemUI/res/drawable/ic_qs_locale_pending.xml new file mode 100644 index 000000000000..4ead2cb64de7 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_locale_pending.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e3b69736ee02..e765f673d34e 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 3125764facae..8c722d8c5bf4 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -159,4 +159,8 @@ Long press - selected region + + + Language + Configure multiple languages first diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 7442609f9193..05ac414d6afd 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.tiles.CompassTile import com.android.systemui.qs.tiles.DataSwitchTile import com.android.systemui.qs.tiles.FPSInfoTile import com.android.systemui.qs.tiles.HeadsUpTile +import com.android.systemui.qs.tiles.LocaleTile import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile import com.android.systemui.qs.tiles.ProfilesTile @@ -101,6 +102,12 @@ interface LineageModule { @StringKey(HeadsUpTile.TILE_SPEC) fun bindHeadsUpTile(headsUpTile: HeadsUpTile): QSTileImpl<*> + /** Inject LocaleTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(LocaleTile.TILE_SPEC) + fun bindLocaleTile(localeTile: LocaleTile): QSTileImpl<*> + /** Inject OnTheGoTile into tileMap in QSModule */ @Binds @IntoMap @@ -311,6 +318,21 @@ interface LineageModule { category = TileCategory.ACCESSIBILITY, ) + @Provides + @IntoMap + @StringKey(LocaleTile.TILE_SPEC) + fun provideLocaleTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(LocaleTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_locale, + labelRes = R.string.quick_settings_locale_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.ACCESSIBILITY, + ) + @Provides @IntoMap @StringKey(OnTheGoTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java new file mode 100644 index 000000000000..0224bb20c237 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2017 AICP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.LocaleList; +import android.os.Looper; +import android.service.quicksettings.Tile; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.android.internal.app.LocalePicker; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; + +import java.util.Locale; + +import javax.inject.Inject; + +/** Quick settings tile: Locale **/ +public class LocaleTile extends QSTileImpl { + + public static final String TILE_SPEC = "locale"; + + private boolean mListening; + + private LocaleList mLocaleList; + + // If not null: update pending + private Locale currentLocaleBackup; + + // Allow multiple clicks to find the desired locale without immediately applying + private static final int TOGGLE_DELAY = 800; + + private final PanelInteractor mPanelInteractor; + + @Inject + public LocaleTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + PanelInteractor panelInteractor + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mPanelInteractor = panelInteractor; + updateLocaleList(); + } + + @Override + public State newTileState() { + return new QSTile.State(); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + if (checkToggleDisabled()) return; + toggleLocale(); + } + + @Override + protected void handleSecondaryClick(@Nullable Expandable expandable) { + if (checkToggleDisabled()) return; + toggleLocale(); + } + + private void toggleLocale() { + if (currentLocaleBackup == null) { + currentLocaleBackup = mLocaleList.get(0); + } + Locale[] newLocales = new Locale[mLocaleList.size()]; + for (int i = 0; i < newLocales.length; i++) { + newLocales[i] = mLocaleList.get((i+1)%newLocales.length); + } + mLocaleList = new LocaleList(newLocales); + mHandler.removeCallbacks(applyLocale); + mHandler.postDelayed(applyLocale, TOGGLE_DELAY); + refreshState(); + } + + private Runnable applyLocale = new Runnable() { + @Override + public void run() { + if (!mLocaleList.get(0).equals(currentLocaleBackup)) { + mPanelInteractor.collapsePanels(); + LocalePicker.updateLocales(mLocaleList); + } + currentLocaleBackup = null; + } + }; + + @Override + public Intent getLongClickIntent() { + return new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.LanguageSettings")); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.quick_settings_locale_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + protected void handleUpdateState(State state, Object arg) { + state.icon = maybeLoadResourceIcon( + currentLocaleBackup == null || currentLocaleBackup.equals(mLocaleList.get(0)) ? + R.drawable.ic_qs_locale : + R.drawable.ic_qs_locale_pending); + state.label = mLocaleList.get(0).getDisplayLanguage(); + } + + @Override + public void handleSetListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (listening) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_LOCALE_CHANGED); + mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + mContext.unregisterReceiver(mReceiver); + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) { + updateLocaleList(); + refreshState(); + } + } + }; + + private void updateLocaleList() { + if (currentLocaleBackup != null) return; + mLocaleList = LocaleList.getAdjustedDefault(); + } + + private boolean checkToggleDisabled() { + updateLocaleList(); + if (mLocaleList.size() <= 1) { + handleLongClick(null); + Toast.makeText(mContext, + mContext.getString(R.string.quick_settings_locale_more_locales_toast), + Toast.LENGTH_LONG).show(); + return true; + } else { + return false; + } + } +} From 93f74a814b70c012988c074c7fa0fd09f2691a81 Mon Sep 17 00:00:00 2001 From: Andrew Fluck Date: Wed, 9 Nov 2022 11:33:50 -0300 Subject: [PATCH 0560/1315] SystemUI: Re-designed caffeine tile icon Signed-off-by: Gustavo Mendes Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/drawable/ic_qs_caffeine.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/drawable/ic_qs_caffeine.xml b/packages/SystemUI/res/drawable/ic_qs_caffeine.xml index 2c3ba974a7e1..274e423b8905 100644 --- a/packages/SystemUI/res/drawable/ic_qs_caffeine.xml +++ b/packages/SystemUI/res/drawable/ic_qs_caffeine.xml @@ -23,5 +23,5 @@ + android:pathData="M15.723 13H8.277L8.095 11H15.905L15.723 13ZM8.913 20L8.731 18H15.269L15.087 20H8.913ZM8.721 4H15.279L15.613 5H8.387L8.721 4ZM7 7H17V9H7V7ZM18 5H17.721L16.949 2.684C16.812 2.275 16.431 2 16 2H8C7.569 2 7.187 2.275 7.051 2.684L6.279 5H6C5.448 5 5 5.448 5 6V10C5 10.526 5.383 11 6.086 11L7.004 21.091C7.051 21.605 7.483 22 8 22H16C16.517 22 16.949 21.605 16.996 21.091L17.914 11C18.617 11 19 10.526 19 10V6C19 5.448 18.552 5 18 5V5Z" /> From 612671ede9e173f6e0d717755b381536dd541a8b Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Tue, 12 Jul 2022 20:47:45 +0300 Subject: [PATCH 0561/1315] SystemUI: CastTile: Open cast settings on long click And also Get rid of old and unused detail adapter Some other minor improvements Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/qs/tiles/CastTile.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index a8e49637586b..1606a1d69152 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -76,6 +76,9 @@ public class CastTile extends QSTileImpl { private static final String INTERACTION_JANK_TAG = TILE_SPEC; + private static final Intent CAST_SETTINGS = + new Intent(Settings.ACTION_CAST_SETTINGS); + private final CastController mController; private final KeyguardStateController mKeyguard; private final DialogTransitionAnimator mDialogTransitionAnimator; @@ -127,7 +130,6 @@ public CastTile( @Override public BooleanState newTileState() { BooleanState state = new BooleanState(); - state.handlesLongClick = false; return state; } @@ -148,12 +150,7 @@ protected void handleUserSwitch(int newUserId) { @Override public Intent getLongClickIntent() { - return new Intent(Settings.ACTION_CAST_SETTINGS); - } - - @Override - protected void handleLongClick(@Nullable Expandable expandable) { - handleClick(expandable); + return CAST_SETTINGS; } @Override From c9d35bdec3e4b91ee46fa673c4816e1745d81b1e Mon Sep 17 00:00:00 2001 From: Anushek Prasal Date: Thu, 13 May 2021 23:01:53 +0530 Subject: [PATCH 0562/1315] SystemUI: Use secondary label for language QS tile Signed-off-by: Anushek Prasal Change-Id: I21b475316155862e43fb9a8bc81225b215809bbe Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java index 0224bb20c237..100484eb7d67 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocaleTile.java @@ -152,7 +152,8 @@ protected void handleUpdateState(State state, Object arg) { currentLocaleBackup == null || currentLocaleBackup.equals(mLocaleList.get(0)) ? R.drawable.ic_qs_locale : R.drawable.ic_qs_locale_pending); - state.label = mLocaleList.get(0).getDisplayLanguage(); + state.label = mContext.getString(R.string.quick_settings_locale_label); + state.secondaryLabel = mLocaleList.get(0).getDisplayLanguage(); } @Override From 6881c7fa11aa628282b352c3127f128e540af7c8 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 9 Jan 2024 21:27:25 +0800 Subject: [PATCH 0563/1315] SystemUI: Add affordance shorcut for AI Voice Assistant * reference: https://t.me/MishaalAndroidNews/1414 * chatgpt is only temporary, additional ai assistant will be added once we found another ai assistant that is launchable via activity. Change-Id: I7be6ff0577eed41d714ac87bb2ddac0eb78d6a49 Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/ic_assistant.xml | 10 +++ .../SystemUI/res/values/matrixx_strings.xml | 3 + .../AssistantKeyguardQuickAffordanceConfig.kt | 80 +++++++++++++++++++ .../BuiltInKeyguardQuickAffordanceKeys.kt | 1 + .../KeyguardDataQuickAffordanceModule.kt | 2 + 5 files changed, 96 insertions(+) create mode 100644 packages/SystemUI/res/drawable/ic_assistant.xml create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt diff --git a/packages/SystemUI/res/drawable/ic_assistant.xml b/packages/SystemUI/res/drawable/ic_assistant.xml new file mode 100644 index 000000000000..78c9b663e5bb --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_assistant.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 8c722d8c5bf4..c5815e70387b 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -163,4 +163,7 @@ Language Configure multiple languages first + + + AI Assistant diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..dd1d7ef12d43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.app.StatusBarManager +import android.content.Context +import android.content.Intent +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +import com.android.internal.util.matrixx.Utils + +@SysUISingleton +class AssistantKeyguardQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context +) : KeyguardQuickAffordanceConfig { + + override val key: String + get() = BuiltInKeyguardQuickAffordanceKeys.ASSISTANT + + override fun pickerName(): String = context.getString(R.string.accessibility_assistant_button) + + override val pickerIconResourceId: Int + get() = R.drawable.ic_assistant + + override val lockScreenState: Flow + get() = + flowOf( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( + R.drawable.ic_assistant, + ContentDescription.Resource(R.string.accessibility_assistant_button) + ) + ) + ) + + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { + return if (Utils.isPackageInstalled(context, "com.openai.chatgpt")) { + super.getPickerScreenState() + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } + } + + override fun onTriggered( + expandable: Expandable?, + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + val intent = Intent().setClassName("com.openai.chatgpt", "com.openai.voice.VoiceModeActivity") + return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = intent, + canShowWhileLocked = true, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index 80675d373b8e..01bc7cc90e7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -24,6 +24,7 @@ package com.android.systemui.keyguard.data.quickaffordance */ object BuiltInKeyguardQuickAffordanceKeys { // Please keep alphabetical order of const names to simplify future maintenance. + const val ASSISTANT = "assistant" const val CAMERA = "camera" const val CREATE_NOTE = "create_note" const val DO_NOT_DISTURB = "do_not_disturb" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index 787a9837b860..8b6ef0af02d9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -33,6 +33,7 @@ interface KeyguardDataQuickAffordanceModule { @Provides @ElementsIntoSet fun quickAffordanceConfigs( + assistant: AssistantKeyguardQuickAffordanceConfig, camera: CameraQuickAffordanceConfig, doNotDisturb: DoNotDisturbQuickAffordanceConfig, flashlight: FlashlightQuickAffordanceConfig, @@ -43,6 +44,7 @@ interface KeyguardDataQuickAffordanceModule { videoCamera: VideoCameraQuickAffordanceConfig, ): Set { return setOf( + assistant, camera, doNotDisturb, flashlight, From 3f96120e58d65246a8c379a6765c146d15f84383 Mon Sep 17 00:00:00 2001 From: aswin7469 Date: Fri, 28 Jun 2024 00:25:15 +0530 Subject: [PATCH 0564/1315] SystemUI: Update ChatGPT quickaffordance activity * changed on chatgpt app v1.2024.170+ Change-Id: I2817c6134bebd95268ca5ad39f2f18a27a8ca2fc Signed-off-by: aswin7469 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt index dd1d7ef12d43..c6a7e9d5fe13 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/AssistantKeyguardQuickAffordanceConfig.kt @@ -71,7 +71,7 @@ constructor( override fun onTriggered( expandable: Expandable?, ): KeyguardQuickAffordanceConfig.OnTriggeredResult { - val intent = Intent().setClassName("com.openai.chatgpt", "com.openai.voice.VoiceModeActivity") + val intent = Intent().setClassName("com.openai.chatgpt", "com.openai.voice.assistant.AssistantActivity") return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( intent = intent, canShowWhileLocked = true, From ba07d07c9968c4120085eaa8d92c847b9bd7abc4 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Thu, 11 Apr 2024 21:54:05 +0300 Subject: [PATCH 0565/1315] SystemUI: Add private DNS QS tile Will toggle private DNS between off and last used mode If no mode was selected will toggle to opportunistic Shows the active mode in subtitle text as such: Off for off Automatic for opportunistic and the hostname string for manual Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 15 ++ .../SystemUI/res/drawable/ic_settings_dns.xml | 10 + packages/SystemUI/res/values/config.xml | 2 +- .../android/systemui/lineage/LineageModule.kt | 22 +++ .../android/systemui/qs/tiles/DnsTile.java | 183 ++++++++++++++++++ 5 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_settings_dns.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/DnsTile.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8ffb642b460e..0b2660c35f62 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3048,6 +3048,21 @@ public final class Settings { public static final String ACTION_APP_PERMISSIONS_SETTINGS = "android.settings.APP_PERMISSIONS_SETTINGS"; + /** + * Activity Action: Show screen that lets user configure private DNS + *

+ * In some cases, a matching Activity may not exist, so ensure you safeguard against this. + *

+ * Input: Nothing + *

+ * Output: Nothing + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PRIVATE_DNS_SETTING = + "com.android.settings.PRIVATE_DNS_SETTINGS"; + // End of Intent actions for Settings /** diff --git a/packages/SystemUI/res/drawable/ic_settings_dns.xml b/packages/SystemUI/res/drawable/ic_settings_dns.xml new file mode 100644 index 000000000000..be1e554ea931 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_settings_dns.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e765f673d34e..dbf5ec880ff6 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 05ac414d6afd..289abc1a05af 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -26,6 +26,7 @@ import com.android.systemui.qs.tiles.CPUInfoTile import com.android.systemui.qs.tiles.CaffeineTile import com.android.systemui.qs.tiles.CompassTile import com.android.systemui.qs.tiles.DataSwitchTile +import com.android.systemui.qs.tiles.DnsTile import com.android.systemui.qs.tiles.FPSInfoTile import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.LocaleTile @@ -90,6 +91,12 @@ interface LineageModule { @StringKey(DataSwitchTile.TILE_SPEC) fun bindDataSwitchTile(dataSwitchTile: DataSwitchTile): QSTileImpl<*> + /** Inject DnsTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(DnsTile.TILE_SPEC) + fun bindDnsTile(dnsTile: DnsTile): QSTileImpl<*> + /** Inject FPSInfoTile into tileMap in QSModule */ @Binds @IntoMap @@ -288,6 +295,21 @@ interface LineageModule { category = TileCategory.CONNECTIVITY, ) + @Provides + @IntoMap + @StringKey(DnsTile.TILE_SPEC) + fun provideDnsTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(DnsTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_settings_dns, + labelRes = com.android.settingslib.R.string.select_private_dns_configuration_title + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY, + ) + @Provides @IntoMap @StringKey(FPSInfoTile.TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DnsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DnsTile.java new file mode 100644 index 000000000000..daa003eecff6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DnsTile.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2024 Yet Another AOSP Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; + +import android.content.Intent; +import android.net.ConnectivitySettingsManager; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.service.quicksettings.Tile; +import android.widget.Switch; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Prefs; +import com.android.systemui.animation.Expandable; +import com.android.systemui.res.R; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.settings.SettingObserver; + +import javax.inject.Inject; + +/** Quick settings tile: DNS Tile **/ +public class DnsTile extends QSTileImpl { + + public static final String TILE_SPEC = "dns"; + private static final String KEY_PREV_MODE = "dns_tile_prev_mode"; + + private final SettingObserver mSetting; + private boolean mListening; + @Nullable + private Icon mIcon = null; + + @Inject + public DnsTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + GlobalSettings globalSettings) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mSetting = new SettingObserver(globalSettings, mHandler, Settings.Global.PRIVATE_DNS_MODE) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void handleSetListening(boolean listening) { + super.handleSetListening(listening); + if (mListening == listening) return; + mListening = listening; + if (listening) { + refreshState(); + } + mSetting.setListening(listening); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(Settings.ACTION_PRIVATE_DNS_SETTING); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + // don't toggle if not needed, just refresh state instead + final int mode = ConnectivitySettingsManager.getPrivateDnsMode(mContext); + final boolean stateEnabled = mState.value; + final boolean isEnabled = mode != PRIVATE_DNS_MODE_OFF; + if (stateEnabled && !isEnabled || !stateEnabled && isEnabled) { + refreshState(); + return; + } + + setPrivateDnsEnabled(!stateEnabled); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString( + com.android.settingslib.R.string.select_private_dns_configuration_title); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + state.label = mContext.getString( + com.android.settingslib.R.string.select_private_dns_configuration_title); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_settings_dns); + } + state.icon = mIcon; + state.expandedAccessibilityClassName = Switch.class.getName(); + state.contentDescription = state.label; + + final int mode = ConnectivitySettingsManager.getPrivateDnsMode(mContext); + final boolean isTileActive = mode != PRIVATE_DNS_MODE_OFF; + state.value = isTileActive; + state.state = isTileActive ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.secondaryLabel = getSecondaryLabel(mode); + state.stateDescription = state.secondaryLabel; + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + private String getSecondaryLabel(int mode) { + if (mode == PRIVATE_DNS_MODE_OFF) { + return mContext.getString( + com.android.settingslib.R.string.private_dns_mode_off); + } else if (mode == PRIVATE_DNS_MODE_OPPORTUNISTIC) { + return mContext.getString( + com.android.settingslib.R.string.private_dns_mode_opportunistic); + } + // PRIVATE_DNS_MODE_PROVIDER_HOSTNAME + return ConnectivitySettingsManager.getPrivateDnsHostname(mContext); + } + + private void setPrivateDnsEnabled(boolean enabled) { + if (!enabled) { + // save current mode for returning + // double check it is not off! + final int mode = ConnectivitySettingsManager.getPrivateDnsMode(mContext); + if (mode == PRIVATE_DNS_MODE_OFF) return; + Prefs.putInt(mContext, KEY_PREV_MODE, mode); + ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OFF); + return; + } + // return to the last state + int mode = Prefs.getInt(mContext, KEY_PREV_MODE, PRIVATE_DNS_MODE_OPPORTUNISTIC); + // never toggle off to off - use opportunistic + if (mode == PRIVATE_DNS_MODE_OFF) mode = PRIVATE_DNS_MODE_OPPORTUNISTIC; + ConnectivitySettingsManager.setPrivateDnsMode(mContext, mode); + } +} From 779344ba4e5ae3b9bd197c046c7b0ef8f59aedf0 Mon Sep 17 00:00:00 2001 From: cjh1249131356 Date: Mon, 11 Apr 2022 19:41:45 +0800 Subject: [PATCH 0566/1315] SystemUI: Introduce preferred network tile Co-authored-by: ShevT Signed-off-by: cjh1249131356 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/ic_qs_preferred_network.xml | 8 + packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 6 + .../android/systemui/lineage/LineageModule.kt | 22 + .../qs/tiles/PreferredNetworkTile.java | 593 ++++++++++++++++++ 5 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_preferred_network.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/PreferredNetworkTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_preferred_network.xml b/packages/SystemUI/res/drawable/ic_qs_preferred_network.xml new file mode 100644 index 000000000000..4d641409ea3d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_preferred_network.xml @@ -0,0 +1,8 @@ + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index dbf5ec880ff6..a3e698cd841f 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns,preferred_network diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index c5815e70387b..f3f33d273406 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -166,4 +166,10 @@ AI Assistant + + + Preferred network + 4G / LTE + 5G / NR + Unsupported diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 289abc1a05af..15c4a9040a58 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.HeadsUpTile import com.android.systemui.qs.tiles.LocaleTile import com.android.systemui.qs.tiles.OnTheGoTile import com.android.systemui.qs.tiles.PowerShareTile +import com.android.systemui.qs.tiles.PreferredNetworkTile import com.android.systemui.qs.tiles.ProfilesTile import com.android.systemui.qs.tiles.ReadingModeTile import com.android.systemui.qs.tiles.RefreshRateTile @@ -127,6 +128,12 @@ interface LineageModule { @StringKey(PowerShareTile.TILE_SPEC) fun bindPowerShareTile(powerShareTile: PowerShareTile): QSTileImpl<*> + /** Inject PreferredNetworkTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(PreferredNetworkTile.TILE_SPEC) + fun bindPreferredNetworkTile(preferredNetworkTile: PreferredNetworkTile): QSTileImpl<*> + /** Inject ProfilesTile into tileMap in QSModule */ @Binds @IntoMap @@ -385,6 +392,21 @@ interface LineageModule { category = TileCategory.UTILITIES, ) + @Provides + @IntoMap + @StringKey(PreferredNetworkTile.TILE_SPEC) + fun providePreferredNetworkTile(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(PreferredNetworkTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_preferred_network, + labelRes = R.string.qs_preferred_network_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY, + ) + @Provides @IntoMap @StringKey(PROFILES_TILE_SPEC) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/PreferredNetworkTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/PreferredNetworkTile.java new file mode 100644 index 000000000000..625bc4ec55da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/PreferredNetworkTile.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2022 Nameless-AOSP Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.CDMA; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.EVDO; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.GSM; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.LTE; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.NR; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.RAF_TD_SCDMA; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.RAF_UNKNOWN; +import static com.android.systemui.qs.tiles.PreferredNetworkTile.RadioConstants.WCDMA; + +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.service.quicksettings.Tile; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import com.android.systemui.res.R; +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; + +import javax.inject.Inject; + +public class PreferredNetworkTile extends QSTileImpl { + + public static final String TILE_SPEC = "preferred_network"; + + @Nullable + private Icon mIcon = null; + + private final TelephonyManager mTelephonyManager; + + @Inject + public PreferredNetworkTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + mTelephonyManager = TelephonyManager.from(host.getContext()); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public State newTileState() { + return new State(); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + final int mode = getPreferredNetworkMode(); + final int newMode = TelephonyManagerConstants.getTargetMode(mode); + if (newMode == -1) return; + final int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + mTelephonyManager.createForSubscriptionId(subId).setAllowedNetworkTypesForReason( + TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER, + getRafFromNetworkType(newMode)); + refreshState(); + } + + @Override + public Intent getLongClickIntent() { + Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); + int dataSub = SubscriptionManager.getDefaultDataSubscriptionId(); + if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + intent.putExtra(Settings.EXTRA_SUB_ID, + SubscriptionManager.getDefaultDataSubscriptionId()); + } + return intent; + } + + @Override + protected void handleUpdateState(State state, Object arg) { + state.label = mContext.getResources().getString(R.string.qs_preferred_network_label); + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_preferred_network); + } + state.icon = mIcon; + final int mode = getPreferredNetworkMode(); + final int newMode = TelephonyManagerConstants.getTargetMode(mode); + state.state = newMode == -1 ? Tile.STATE_UNAVAILABLE : Tile.STATE_ACTIVE; + state.secondaryLabel = newMode == -1 ? mContext.getResources().getString(R.string.qs_preferred_network_unsupported) + : (TelephonyManagerConstants.is5gMode(mode) ? + mContext.getResources().getString(R.string.qs_preferred_network_nr) + : mContext.getResources().getString(R.string.qs_preferred_network_lte)); + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.qs_preferred_network_label); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.MATRIXX; + } + + @Override + public void handleSetListening(boolean listening) { + } + + private int getPreferredNetworkMode() { + final int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + return getNetworkTypeFromRaf( + (int) mTelephonyManager.createForSubscriptionId(subId).getAllowedNetworkTypesForReason( + TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER)); + } + + private static int getNetworkTypeFromRaf(int raf) { + raf = getAdjustedRaf(raf); + + switch (raf) { + case (GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_WCDMA_PREF; + case GSM: + return TelephonyManagerConstants.NETWORK_MODE_GSM_ONLY; + case WCDMA: + return TelephonyManagerConstants.NETWORK_MODE_WCDMA_ONLY; + case (CDMA | EVDO): + return TelephonyManagerConstants.NETWORK_MODE_CDMA_EVDO; + case (LTE | CDMA | EVDO): + return TelephonyManagerConstants.NETWORK_MODE_LTE_CDMA_EVDO; + case (LTE | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_GSM_WCDMA; + case (LTE | CDMA | EVDO | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA; + case LTE: + return TelephonyManagerConstants.NETWORK_MODE_LTE_ONLY; + case (LTE | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_WCDMA; + case CDMA: + return TelephonyManagerConstants.NETWORK_MODE_CDMA_NO_EVDO; + case EVDO: + return TelephonyManagerConstants.NETWORK_MODE_EVDO_NO_CDMA; + case (GSM | WCDMA | CDMA | EVDO): + return TelephonyManagerConstants.NETWORK_MODE_GLOBAL; + case RAF_TD_SCDMA: + return TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_ONLY; + case (RAF_TD_SCDMA | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_WCDMA; + case (LTE | RAF_TD_SCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA; + case (RAF_TD_SCDMA | GSM): + return TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_GSM; + case (LTE | RAF_TD_SCDMA | GSM): + return TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_GSM; + case (RAF_TD_SCDMA | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA; + case (LTE | RAF_TD_SCDMA | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA; + case (LTE | RAF_TD_SCDMA | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA; + case (RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + case (LTE | RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + case (NR): + return TelephonyManagerConstants.NETWORK_MODE_NR_ONLY; + case (NR | LTE): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE; + case (NR | LTE | CDMA | EVDO): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_CDMA_EVDO; + case (NR | LTE | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_GSM_WCDMA; + case (NR | LTE | CDMA | EVDO | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA; + case (NR | LTE | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_WCDMA; + case (NR | LTE | RAF_TD_SCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA; + case (NR | LTE | RAF_TD_SCDMA | GSM): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_GSM; + case (NR | LTE | RAF_TD_SCDMA | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA; + case (NR | LTE | RAF_TD_SCDMA | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA; + case (NR | LTE | RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA): + return TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + default: + return TelephonyManagerConstants.NETWORK_MODE_UNKNOWN; + } + } + + private static long getRafFromNetworkType(int type) { + switch (type) { + case TelephonyManagerConstants.NETWORK_MODE_WCDMA_PREF: + return GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_GSM_ONLY: + return GSM; + case TelephonyManagerConstants.NETWORK_MODE_WCDMA_ONLY: + return WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_GSM_UMTS: + return GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_CDMA_EVDO: + return CDMA | EVDO; + case TelephonyManagerConstants.NETWORK_MODE_LTE_CDMA_EVDO: + return LTE | CDMA | EVDO; + case TelephonyManagerConstants.NETWORK_MODE_LTE_GSM_WCDMA: + return LTE | GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA: + return LTE | CDMA | EVDO | GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_ONLY: + return LTE; + case TelephonyManagerConstants.NETWORK_MODE_LTE_WCDMA: + return LTE | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_CDMA_NO_EVDO: + return CDMA; + case TelephonyManagerConstants.NETWORK_MODE_EVDO_NO_CDMA: + return EVDO; + case TelephonyManagerConstants.NETWORK_MODE_GLOBAL: + return GSM | WCDMA | CDMA | EVDO; + case TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_ONLY: + return RAF_TD_SCDMA; + case TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_WCDMA: + return RAF_TD_SCDMA | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA: + return LTE | RAF_TD_SCDMA; + case TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_GSM: + return RAF_TD_SCDMA | GSM; + case TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_GSM: + return LTE | RAF_TD_SCDMA | GSM; + case TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA: + return RAF_TD_SCDMA | GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA: + return LTE | RAF_TD_SCDMA | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA: + return LTE | RAF_TD_SCDMA | GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA; + case TelephonyManagerConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return LTE | RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_ONLY): + return NR; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE): + return NR | LTE; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_CDMA_EVDO): + return NR | LTE | CDMA | EVDO; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_GSM_WCDMA): + return NR | LTE | GSM | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA): + return NR | LTE | CDMA | EVDO | GSM | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_WCDMA): + return NR | LTE | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA): + return NR | LTE | RAF_TD_SCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_GSM): + return NR | LTE | RAF_TD_SCDMA | GSM; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA): + return NR | LTE | RAF_TD_SCDMA | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA): + return NR | LTE | RAF_TD_SCDMA | GSM | WCDMA; + case (TelephonyManagerConstants.NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA): + return NR | LTE | RAF_TD_SCDMA | CDMA | EVDO | GSM | WCDMA; + default: + return RAF_UNKNOWN; + } + } + + private static int getAdjustedRaf(int raf) { + raf = ((GSM & raf) > 0) ? (GSM | raf) : raf; + raf = ((WCDMA & raf) > 0) ? (WCDMA | raf) : raf; + raf = ((CDMA & raf) > 0) ? (CDMA | raf) : raf; + raf = ((EVDO & raf) > 0) ? (EVDO | raf) : raf; + raf = ((LTE & raf) > 0) ? (LTE | raf) : raf; + raf = ((NR & raf) > 0) ? (NR | raf) : raf; + return raf; + } + + static class TelephonyManagerConstants { + + // Network modes are in turn copied from RILConstants + // with one difference: NETWORK_MODE_CDMA is named NETWORK_MODE_CDMA_EVDO + + public static final int NETWORK_MODE_UNKNOWN = -1; + + /** + * GSM, WCDMA (WCDMA preferred) + */ + public static final int NETWORK_MODE_WCDMA_PREF = 0; + + /** + * GSM only + */ + public static final int NETWORK_MODE_GSM_ONLY = 1; + + /** + * WCDMA only + */ + public static final int NETWORK_MODE_WCDMA_ONLY = 2; + + /** + * GSM, WCDMA (auto mode, according to PRL) + */ + public static final int NETWORK_MODE_GSM_UMTS = 3; + + /** + * CDMA and EvDo (auto mode, according to PRL) + * this is NETWORK_MODE_CDMA in RILConstants.java + */ + public static final int NETWORK_MODE_CDMA_EVDO = 4; + + /** + * CDMA only + */ + public static final int NETWORK_MODE_CDMA_NO_EVDO = 5; + + /** + * EvDo only + */ + public static final int NETWORK_MODE_EVDO_NO_CDMA = 6; + + /** + * GSM, WCDMA, CDMA, and EvDo (auto mode, according to PRL) + */ + public static final int NETWORK_MODE_GLOBAL = 7; + + /** + * LTE, CDMA and EvDo + */ + public static final int NETWORK_MODE_LTE_CDMA_EVDO = 8; + + /** + * LTE, GSM and WCDMA + */ + public static final int NETWORK_MODE_LTE_GSM_WCDMA = 9; + + /** + * LTE, CDMA, EvDo, GSM, and WCDMA + */ + public static final int NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA = 10; + + /** + * LTE only mode. + */ + public static final int NETWORK_MODE_LTE_ONLY = 11; + + /** + * LTE and WCDMA + */ + public static final int NETWORK_MODE_LTE_WCDMA = 12; + + /** + * TD-SCDMA only + */ + public static final int NETWORK_MODE_TDSCDMA_ONLY = 13; + + /** + * TD-SCDMA and WCDMA + */ + public static final int NETWORK_MODE_TDSCDMA_WCDMA = 14; + + /** + * LTE and TD-SCDMA + */ + public static final int NETWORK_MODE_LTE_TDSCDMA = 15; + + /** + * TD-SCDMA and GSM + */ + public static final int NETWORK_MODE_TDSCDMA_GSM = 16; + + /** + * TD-SCDMA, GSM and LTE + */ + public static final int NETWORK_MODE_LTE_TDSCDMA_GSM = 17; + + /** + * TD-SCDMA, GSM and WCDMA + */ + public static final int NETWORK_MODE_TDSCDMA_GSM_WCDMA = 18; + + /** + * LTE, TD-SCDMA and WCDMA + */ + public static final int NETWORK_MODE_LTE_TDSCDMA_WCDMA = 19; + + /** + * LTE, TD-SCDMA, GSM, and WCDMA + */ + public static final int NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA = 20; + + /** + * TD-SCDMA, CDMA, EVDO, GSM and WCDMA + */ + public static final int NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 21; + + /** + * LTE, TDCSDMA, CDMA, EVDO, GSM and WCDMA + */ + public static final int NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 22; + + /** + * NR 5G only mode + */ + public static final int NETWORK_MODE_NR_ONLY = 23; + + /** + * NR 5G, LTE + */ + public static final int NETWORK_MODE_NR_LTE = 24; + + /** + * NR 5G, LTE, CDMA and EvDo + */ + public static final int NETWORK_MODE_NR_LTE_CDMA_EVDO = 25; + + /** + * NR 5G, LTE, GSM and WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_GSM_WCDMA = 26; + + /** + * NR 5G, LTE, CDMA, EvDo, GSM and WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA = 27; + + /** + * NR 5G, LTE and WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_WCDMA = 28; + + /** + * NR 5G, LTE and TDSCDMA + */ + public static final int NETWORK_MODE_NR_LTE_TDSCDMA = 29; + + /** + * NR 5G, LTE, TD-SCDMA and GSM + */ + public static final int NETWORK_MODE_NR_LTE_TDSCDMA_GSM = 30; + + /** + * NR 5G, LTE, TD-SCDMA, WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA = 31; + + /** + * NR 5G, LTE, TD-SCDMA, GSM and WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA = 32; + + /** + * NR 5G, LTE, TD-SCDMA, CDMA, EVDO, GSM and WCDMA + */ + public static final int NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 33; + + public static int getTargetMode(int mode) { + switch (mode) { + case NETWORK_MODE_LTE_CDMA_EVDO: + return NETWORK_MODE_NR_LTE_CDMA_EVDO; + case NETWORK_MODE_LTE_GSM_WCDMA: + return NETWORK_MODE_NR_LTE_GSM_WCDMA; + case NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA: + return NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA; + case NETWORK_MODE_LTE_ONLY: + return NETWORK_MODE_NR_LTE; + case NETWORK_MODE_LTE_WCDMA: + return NETWORK_MODE_NR_LTE_WCDMA; + case NETWORK_MODE_LTE_TDSCDMA: + return NETWORK_MODE_NR_LTE_TDSCDMA; + case NETWORK_MODE_LTE_TDSCDMA_GSM: + return NETWORK_MODE_NR_LTE_TDSCDMA_GSM; + case NETWORK_MODE_LTE_TDSCDMA_WCDMA: + return NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA; + case NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA: + return NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA; + case NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + case NETWORK_MODE_NR_ONLY: + return NETWORK_MODE_LTE_ONLY; + case NETWORK_MODE_NR_LTE: + return NETWORK_MODE_LTE_ONLY; + case NETWORK_MODE_NR_LTE_CDMA_EVDO: + return NETWORK_MODE_LTE_CDMA_EVDO; + case NETWORK_MODE_NR_LTE_GSM_WCDMA: + return NETWORK_MODE_LTE_GSM_WCDMA; + case NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA: + return NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA; + case NETWORK_MODE_NR_LTE_WCDMA: + return NETWORK_MODE_LTE_WCDMA; + case NETWORK_MODE_NR_LTE_TDSCDMA: + return NETWORK_MODE_LTE_TDSCDMA; + case NETWORK_MODE_NR_LTE_TDSCDMA_GSM: + return NETWORK_MODE_LTE_TDSCDMA_GSM; + case NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA: + return NETWORK_MODE_LTE_TDSCDMA_WCDMA; + case NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA: + return NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA; + case NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + default: + return -1; + } + } + + public static boolean is5gMode(int mode) { + return mode == NETWORK_MODE_NR_ONLY || + mode == NETWORK_MODE_NR_LTE || + mode == NETWORK_MODE_NR_LTE_CDMA_EVDO || + mode == NETWORK_MODE_NR_LTE_GSM_WCDMA || + mode == NETWORK_MODE_NR_LTE_CDMA_EVDO_GSM_WCDMA || + mode == NETWORK_MODE_NR_LTE_WCDMA || + mode == NETWORK_MODE_NR_LTE_TDSCDMA || + mode == NETWORK_MODE_NR_LTE_TDSCDMA_GSM || + mode == NETWORK_MODE_NR_LTE_TDSCDMA_WCDMA || + mode == NETWORK_MODE_NR_LTE_TDSCDMA_GSM_WCDMA || + mode == NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA; + } + } + + static class RadioConstants { + // 2G + public static final int RAF_UNKNOWN = (int) TelephonyManager.NETWORK_TYPE_BITMASK_UNKNOWN; + public static final int RAF_GSM = (int) TelephonyManager.NETWORK_TYPE_BITMASK_GSM; + public static final int RAF_GPRS = (int) TelephonyManager.NETWORK_TYPE_BITMASK_GPRS; + public static final int RAF_EDGE = (int) TelephonyManager.NETWORK_TYPE_BITMASK_EDGE; + public static final int RAF_IS95A = (int) TelephonyManager.NETWORK_TYPE_BITMASK_CDMA; + public static final int RAF_IS95B = (int) TelephonyManager.NETWORK_TYPE_BITMASK_CDMA; + public static final int RAF_1xRTT = (int) TelephonyManager.NETWORK_TYPE_BITMASK_1xRTT; + // 3G + public static final int RAF_EVDO_0 = (int) TelephonyManager.NETWORK_TYPE_BITMASK_EVDO_0; + public static final int RAF_EVDO_A = (int) TelephonyManager.NETWORK_TYPE_BITMASK_EVDO_A; + public static final int RAF_EVDO_B = (int) TelephonyManager.NETWORK_TYPE_BITMASK_EVDO_B; + public static final int RAF_EHRPD = (int) TelephonyManager.NETWORK_TYPE_BITMASK_EHRPD; + public static final int RAF_HSUPA = (int) TelephonyManager.NETWORK_TYPE_BITMASK_HSUPA; + public static final int RAF_HSDPA = (int) TelephonyManager.NETWORK_TYPE_BITMASK_HSDPA; + public static final int RAF_HSPA = (int) TelephonyManager.NETWORK_TYPE_BITMASK_HSPA; + public static final int RAF_HSPAP = (int) TelephonyManager.NETWORK_TYPE_BITMASK_HSPAP; + public static final int RAF_UMTS = (int) TelephonyManager.NETWORK_TYPE_BITMASK_UMTS; + public static final int RAF_TD_SCDMA = (int) TelephonyManager.NETWORK_TYPE_BITMASK_TD_SCDMA; + // 4G + public static final int RAF_LTE = (int) TelephonyManager.NETWORK_TYPE_BITMASK_LTE; + public static final int RAF_LTE_CA = (int) TelephonyManager.NETWORK_TYPE_BITMASK_LTE_CA; + // 5G + public static final int RAF_NR = (int) TelephonyManager.NETWORK_TYPE_BITMASK_NR; + + // Grouping of RAFs + // 2G + public static final int GSM = RAF_GSM | RAF_GPRS | RAF_EDGE; + public static final int CDMA = RAF_IS95A | RAF_IS95B | RAF_1xRTT; + // 3G + public static final int EVDO = RAF_EVDO_0 | RAF_EVDO_A | RAF_EVDO_B | RAF_EHRPD; + public static final int HS = RAF_HSUPA | RAF_HSDPA | RAF_HSPA | RAF_HSPAP; + public static final int WCDMA = HS | RAF_UMTS; + // 4G + public static final int LTE = RAF_LTE | RAF_LTE_CA; + // 5G + public static final int NR = RAF_NR; + } +} From d7df1f469d3e2fade1106714ce23d5038b31f525 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 8 Aug 2024 20:13:33 +0530 Subject: [PATCH 0567/1315] base: Add support for LMOFreeform service Change-Id: I03ebd2e0e175f91fbc83fb9221724d7be5daf7ca Signed-off-by: Adithya R Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- Android.bp | 1 + .../display/DisplayManagerInternal.java | 15 ++++++++++ services/core/Android.bp | 1 + .../server/display/DisplayManagerService.java | 29 +++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/Android.bp b/Android.bp index 939c2417a627..da4b8fca8d44 100644 --- a/Android.bp +++ b/Android.bp @@ -142,6 +142,7 @@ filegroup { ":deviceproductinfoconstants_aidl", ":adbrootservice_aidl", + ":lmofreeform_aidl", // For the generated R.java and Manifest.java ":framework-res{.aapt.srcjar}", diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 2174513c83e5..b149d03f351a 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -24,17 +24,21 @@ import android.hardware.SensorManager; import android.hardware.input.HostUsiVersion; import android.os.Handler; +import android.os.IBinder; import android.os.PowerManager; import android.util.IntArray; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.Transaction; import android.window.DisplayWindowPolicyController; import android.window.ScreenCaptureInternal; +import com.libremobileos.freeform.ILMOFreeformDisplayCallback; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -501,6 +505,17 @@ public abstract RefreshRateRange getRefreshRateForDisplayAndSensor( */ public abstract IntArray getDisplayIds(); + // LMOFreeform + public abstract void createFreeformLocked(String name, ILMOFreeformDisplayCallback callback, + int width, int height, int densityDpi, boolean secure, boolean ownContentOnly, + boolean shouldShowSystemDecorations, Surface surface, float refreshRate, + long presentationDeadlineNanos); + + public abstract void resizeFreeform(IBinder appToken, int width, int height, + int densityDpi); + + public abstract void releaseFreeform(IBinder appToken); + /** * Get group id for given display id */ diff --git a/services/core/Android.bp b/services/core/Android.bp index e08be355a068..f2b14432fd85 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -134,6 +134,7 @@ java_library_static { ":display-layout-config", ":display-topology", ":device-state-config", + ":lmofreeform-display-adapter-java", "java/com/android/server/EventLogTags.logtags", "java/com/android/server/am/EventLogTags.logtags", "java/com/android/server/wm/EventLogTags.logtags", diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index a64431f3e426..fc30a6a7374f 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -208,6 +208,8 @@ import com.android.server.wm.SurfaceAnimationThread; import com.android.server.wm.WindowManagerInternal; +import com.libremobileos.freeform.ILMOFreeformDisplayCallback; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -563,6 +565,8 @@ public synchronized void requestDisplayState(int displayId, int state, float bri // only be used for the devices in projected mode. private boolean mIncludeDefaultDisplayInTopology; + private LMOFreeformDisplayAdapter mFreeformDisplayAdapter; + private final BroadcastReceiver mIdleModeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -2386,6 +2390,7 @@ private void registerAdditionalDisplayAdapters() { if (shouldRegisterNonEssentialDisplayAdaptersLocked()) { registerOverlayDisplayAdapterLocked(); registerWifiDisplayAdapterLocked(); + registerFreeformDisplayAdapterLocked(); } } } @@ -2406,6 +2411,13 @@ private void registerWifiDisplayAdapterLocked() { } } + private void registerFreeformDisplayAdapterLocked() { + mFreeformDisplayAdapter = new LMOFreeformDisplayAdapter( + mSyncRoot, mContext, mHandler, mDisplayDeviceRepo, mLogicalDisplayMapper, + mUiHandler, mFlags); + registerDisplayAdapterLocked(mFreeformDisplayAdapter); + } + private boolean shouldRegisterNonEssentialDisplayAdaptersLocked() { // In safe mode, we disable non-essential display adapters to give the user // an opportunity to fix broken settings or other problems that might affect @@ -6435,6 +6447,23 @@ public void reloadTopologies(final int userId) { scheduleTopologiesReload(mCurrentUserId, /*isUserSwitching=*/ false); } } + + public void createFreeformLocked(String name, ILMOFreeformDisplayCallback callback, + int width, int height, int densityDpi, boolean secure, boolean ownContentOnly, + boolean shouldShowSystemDecorations, Surface surface, float refreshRate, + long presentationDeadlineNanos) { + mFreeformDisplayAdapter.createFreeformLocked(name, callback, width, height, densityDpi, + secure, ownContentOnly, shouldShowSystemDecorations, surface, refreshRate, + presentationDeadlineNanos); + } + + public void resizeFreeform(IBinder appToken, int width, int height, int densityDpi) { + mFreeformDisplayAdapter.resizeFreeform(appToken, width, height, densityDpi); + } + + public void releaseFreeform(IBinder appToken) { + mFreeformDisplayAdapter.releaseFreeform(appToken); + } } class DesiredDisplayModeSpecsObserver From 35cf3054a1786f79d749f3f9103c3c9a44e308d4 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 8 Aug 2024 20:14:44 +0530 Subject: [PATCH 0568/1315] services: Add freeform system service neobuddy89: Create package to include in fwb base itself. Change-Id: Icb3933846fc38f7fe1679e7f1fd916c2c137671d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/AndroidManifest.xml | 4 ++ services/Android.bp | 2 + services/freeform/Android.bp | 27 +++++++++ services/freeform/jarjar-rules.txt | 2 + .../server/display/FreeformService.java | 59 +++++++++++++++++++ .../java/com/android/server/SystemServer.java | 5 ++ 6 files changed, 99 insertions(+) create mode 100644 services/freeform/Android.bp create mode 100644 services/freeform/jarjar-rules.txt create mode 100644 services/freeform/java/com/android/server/display/FreeformService.java diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c18048f512ae..bea6387ea053 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -949,6 +949,10 @@ android:knownCerts="@array/config_setContactsDefaultAccountKnownSigners" android:featureFlag="android.provider.new_default_account_api_enabled"/> + + + diff --git a/services/Android.bp b/services/Android.bp index 7d3d37ca56b3..481694280bf5 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -133,6 +133,7 @@ filegroup { ":services.coverage-sources", ":services.credentials-sources", ":services.devicepolicy-sources", + ":services.freeform-sources", ":services.midi-sources", ":services.musicsearch-sources", ":services.net-sources", @@ -339,6 +340,7 @@ java_library { "services.credentials", "services.devicepolicy", "services.flags", + "services.freeform", "services.midi", "services.musicsearch", "services.net", diff --git a/services/freeform/Android.bp b/services/freeform/Android.bp new file mode 100644 index 000000000000..3920451176ca --- /dev/null +++ b/services/freeform/Android.bp @@ -0,0 +1,27 @@ +// +// SPDX-FileCopyrightText: 2023 The LibreMobileOS Foundation +// SPDX-FileCopyrightText: 2024-2025 crDroid Android Project +// SPDX-License-Identifier: Apache-2.0 +// + +filegroup { + name: "services.freeform-sources", + srcs: [ + "java/**/*.java", + "java/**/*.kt" + ], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.freeform", + defaults: ["platform_service_defaults"], + srcs: [":services.freeform-sources"], + libs: ["services.core"], + static_libs: [ + "kotlinx_coroutines", + "lmofreeform-server", + ], + jarjar_rules: "jarjar-rules.txt", +} diff --git a/services/freeform/jarjar-rules.txt b/services/freeform/jarjar-rules.txt new file mode 100644 index 000000000000..a50af24f3ddd --- /dev/null +++ b/services/freeform/jarjar-rules.txt @@ -0,0 +1,2 @@ +rule kotlin.** com.libremobileos.server.jarjar.@0 +rule kotlinx.** com.libremobileos.server.jarjar.@0 diff --git a/services/freeform/java/com/android/server/display/FreeformService.java b/services/freeform/java/com/android/server/display/FreeformService.java new file mode 100644 index 000000000000..af5c3a5484ec --- /dev/null +++ b/services/freeform/java/com/android/server/display/FreeformService.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023-2024 LibreMobileOS Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import android.content.Context; +import android.hardware.display.DisplayManagerInternal; +import android.util.Slog; + +import com.android.server.SystemService; + +import com.libremobileos.freeform.server.LMOFreeformService; +import com.libremobileos.freeform.server.LMOFreeformServiceHolder; +import com.libremobileos.freeform.server.LMOFreeformUIService; + +public class FreeformService extends SystemService { + + private static final String TAG = "FreeformService"; + + public FreeformService(Context context) { + super(context); + } + + @Override + public void onStart() { + // noop + } + + @Override + public void onBootPhase(@BootPhase int phase) { + if (phase != PHASE_ACTIVITY_MANAGER_READY || isSafeMode()) return; + + Slog.d(TAG, "PHASE_ACTIVITY_MANAGER_READY, going to init!"); + + DisplayManagerInternal displayManager = getLocalService(DisplayManagerInternal.class); + if (displayManager == null) { + Slog.e(TAG, "Cannot init: DisplayManagerInternal is null!"); + return; + } + + LMOFreeformService service = new LMOFreeformService(displayManager); + LMOFreeformUIService uiService = + new LMOFreeformUIService(getContext(), displayManager, service); + LMOFreeformServiceHolder.init(uiService, service); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index cfbf3b9b2c2b..640a74004fe8 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -175,6 +175,7 @@ import com.android.server.devicestate.DeviceStateManagerService; import com.android.server.display.AutoAODService; import com.android.server.display.DisplayManagerService; +import com.android.server.display.FreeformService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; import com.android.server.emergency.EmergencyAffordanceService; @@ -2816,6 +2817,10 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { t.traceEnd(); } + t.traceBegin("FreeformService"); + mSystemServiceManager.startService(FreeformService.class); + t.traceEnd(); + if (!isWatch) { // We don't run this on watches as there are no plans to use the data logged // on watch devices. From c6598f2dc91b4b357a9aa052627f726a28efd72c Mon Sep 17 00:00:00 2001 From: Adithya R Date: Sat, 31 Aug 2024 06:18:29 +0530 Subject: [PATCH 0569/1315] wm: Add API to listen for secure content in display Required by LMOFreeform. Change-Id: I97143a8b35af1432b9b12319bab5c1dfec9f9b34 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/wm/DisplayContent.java | 14 ++++++++++ .../server/wm/WindowManagerInternal.java | 16 ++++++++++++ .../server/wm/WindowManagerService.java | 26 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 27427724f0c3..0a160687dc25 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -796,6 +796,8 @@ public void onIdMatch(InsetsSource source1, InsetsSource source2) { /** Last window to hold the screen locked. */ private WindowState mLastWakeLockHoldingWindow; + private boolean mHasSecureContent; + /** * Whether display is allowed to ignore all activity size restrictions. * @see #isDisplayIgnoreActivitySizeRestrictions @@ -5151,6 +5153,11 @@ boolean isInputMethodClientFocus(int uid, int pid) { return imeLayeringTarget.mSession.mUid == uid && imeLayeringTarget.mSession.mPid == pid; } + boolean hasSecureWindowOnScreen() { + final WindowState win = getWindow(w -> w.isOnScreen() && w.isSecureLocked()); + return win != null; + } + // TODO: Super unexpected long method that should be broken down... void applySurfaceChangesTransaction() { final WindowSurfacePlacer surfacePlacer = mWmService.mWindowPlacerLocked; @@ -5286,6 +5293,13 @@ private void performLayoutNoTrace(boolean initial, boolean updateInputWindows) { if (updateInputWindows) { mInputMonitor.updateInputWindowsLw(false /*force*/); } + + // Notify if display added or removed a secure window + final boolean hasSecureContent = hasSecureWindowOnScreen(); + if (hasSecureContent != mHasSecureContent) { + mHasSecureContent = hasSecureContent; + mWmService.notifyDisplaySecureContentChange(mDisplayId, hasSecureContent); + } } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 8931af7c0474..4216ada8dd65 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -439,6 +439,14 @@ default void dragRecipientEntered(IWindow window) {} default void dragRecipientExited(IWindow window) {} } + /** + * Listener interface for secure content showing up on the display. + */ + public interface DisplaySecureContentListener { + public void onDisplayHasSecureWindowOnScreenChanged( + int displayId, boolean hasSecureWindowOnScreen); + } + /** * Request the interface to access features implemented by AccessibilityController. */ @@ -1249,4 +1257,12 @@ public abstract void requestAssistScreenshot(IAssistDataReceiver receiver, * @throws RuntimeException if the payload cannot be written to the settings file. */ public abstract void restoreDisplayWindowSettings(int userId, byte[] payload); + + /** + * Register/unregister callbacks for secure content showing up on the display. + */ + public abstract void registerDisplaySecureContentListener( + DisplaySecureContentListener listener); + public abstract void unregisterDisplaySecureContentListener( + DisplaySecureContentListener listener); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dd4c992494b0..20a67fa90160 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -797,6 +797,9 @@ public void binderDied() { WindowManagerInternal.OnHardKeyboardStatusChangeListener mHardKeyboardStatusChangeListener; WindowManagerInternal.OnImeRequestedChangedListener mOnImeRequestedChangedListener; + private ArraySet + mDisplaySecureContentListeners = new ArraySet<>(); + SettingsObserver mSettingsObserver; final EmbeddedWindowController mEmbeddedWindowController; final AnrController mAnrController; @@ -5817,6 +5820,15 @@ void notifyHardKeyboardStatusChange() { } } + void notifyDisplaySecureContentChange(int displayId, boolean hasSecureWindowOnScreen) { + synchronized (mGlobalLock) { + mDisplaySecureContentListeners.forEach((listener) -> { + listener.onDisplayHasSecureWindowOnScreenChanged( + displayId, hasSecureWindowOnScreen); + }); + } + } + // ------------------------------------------------------------- // Input Events and Focus Management // ------------------------------------------------------------- @@ -9214,6 +9226,20 @@ public void requestAssistScreenshot(IAssistDataReceiver receiver, IBinder activi } WindowManagerService.this.requestAssistScreenshotInternal(receiver, displayId); } + + @Override + public void registerDisplaySecureContentListener(DisplaySecureContentListener listener) { + synchronized (mGlobalLock) { + mDisplaySecureContentListeners.add(listener); + } + } + + @Override + public void unregisterDisplaySecureContentListener(DisplaySecureContentListener listener) { + synchronized (mGlobalLock) { + mDisplaySecureContentListeners.remove(listener); + } + } } /** Called to inform window manager if non-Vr UI shoul be disabled or not. */ From 419339571fffbc8d30887209c6db3e3fbfa0907e Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 31 Oct 2024 09:01:57 +0530 Subject: [PATCH 0570/1315] wm: Show rounded corners on freeform window on internal display Change-Id: I743aba4200a4d8786c09811de8a2cf5c574b885f Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../windowdecor/CaptionWindowDecoration.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 83ee5694ce3f..951dfdc249f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -32,6 +32,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; @@ -54,6 +55,7 @@ import android.window.DesktopModeFlags; import android.window.WindowContainerTransaction; +import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; @@ -220,7 +222,7 @@ static void updateRelayoutParams( boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, - InsetsState displayInsetsState, + DisplayController displayController, boolean hasGlobalFocus, @NonNull Region globalExclusionRegion, boolean shouldSetBackground, @@ -246,6 +248,8 @@ static void updateRelayoutParams( || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion); relayoutParams.mInSyncWithTransition = inSyncWithTransition; + relayoutParams.mCornerRadius = + getCornerRadius(context, displayController.getDisplay(taskInfo.displayId)); if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { // If the app is requesting to customize the caption bar, allow input to fall @@ -265,7 +269,8 @@ static void updateRelayoutParams( ); relayoutParams.mCaptionTopPadding = getTopPadding(relayoutParams, - taskInfo.getConfiguration().windowConfiguration.getBounds(), displayInsetsState); + taskInfo.getConfiguration().windowConfiguration.getBounds(), + displayController.getInsetsState(taskInfo.displayId)); // Set opaque background for all freeform tasks to prevent freeform tasks below // from being visible if freeform task window above is translucent. // Otherwise if fluid resize is enabled, add a background to freeform tasks. @@ -290,7 +295,7 @@ void relayout(RunningTaskInfo taskInfo, updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + mDisplayController, hasGlobalFocus, globalExclusionRegion, mDesktopConfig.shouldSetBackground(taskInfo), inSyncWithTransition); @@ -350,6 +355,19 @@ void relayout(RunningTaskInfo taskInfo, }); } + private static int getCornerRadius(Context context, Display display) { + // Show rounded corners only on the internal display as we can't get rounded corners for + // external displays. + if (display.getType() != Display.TYPE_INTERNAL) { + return 0; + } + final TypedArray ta = context.obtainStyledAttributes( + new int[]{android.R.attr.dialogCornerRadius}); + final int cornerRadius = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + return cornerRadius; + } + /** * Sets up listeners when a new root view is created. */ From f71c6a4014a9b231a285564af11b94c8530b6e42 Mon Sep 17 00:00:00 2001 From: nift4 Date: Sat, 29 Apr 2023 14:53:56 +0200 Subject: [PATCH 0571/1315] WindowManager: finally proper desktop mode handling * The previous approach turned out to break Miracast screen mirroring, because of the windowing mode. * Instead of hardcoding desktop, use FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS to detect our custom display, as it's impossible for that flag to be set on a non-default display on AOSP. * Make InputManagerCallback check desktop mode per-display, to match new behaviour. Change-Id: I4a701a2cb219e49d3f89aac35720a3ea5e37ec52 Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/DisplayContent.java | 5 +++++ .../java/com/android/server/wm/InputManagerCallback.java | 9 +++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 0a160687dc25..2beb54236d55 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4364,6 +4364,11 @@ InsetsControlTarget getImeControlTarget() { return mWmService.mDisplayWindowSettings.getImePolicyLocked(this); } + boolean forceDesktopMode() { + return ("VNC".equals(mDisplay.getName()) || mWmService.mForceDesktopModeOnExternalDisplays) + && !isDefaultDisplay && !isPrivate(); + } + /** @see WindowManagerInternal#onToggleImeRequested */ void onShowImeRequested() { if (mInputMethodWindow == null) { diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index bfbdef1a92ce..9d24cba7b9ba 100644 --- a/services/core/java/com/android/server/wm/InputManagerCallback.java +++ b/services/core/java/com/android/server/wm/InputManagerCallback.java @@ -259,16 +259,13 @@ public int getPointerLayer() { @Override public int getPointerDisplayId() { synchronized (mService.mGlobalLock) { - // If desktop mode is not enabled, show on the default display. - if (!mService.mForceDesktopModeOnExternalDisplays) { - return DEFAULT_DISPLAY; - } - // Look for the topmost freeform display. int firstExternalDisplayId = DEFAULT_DISPLAY; for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) { final DisplayContent displayContent = mService.mRoot.mChildren.get(i); - if (displayContent.getDisplayInfo().state == Display.STATE_OFF) { + if (displayContent.getDisplayInfo().state == Display.STATE_OFF + || !displayContent.forceDesktopMode()) { + // If desktop mode is not enabled, show on the default display. continue; } // Heuristic solution here. Currently when "Freeform windows" developer option is From cf73a97832ea97c9e5c425c6fd93936b140ff324 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Mon, 2 Sep 2024 06:57:35 +0530 Subject: [PATCH 0572/1315] wm: Skip freeform displays from forcing desktop mode Since our floating window is created as a separate display, don't force them into desktop mode too when the developer setting is enabled. Change-Id: I341a4929294d0919499e8be115f76de722d6d00d Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/DisplayContent.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 2beb54236d55..92a45a1e81bb 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -130,6 +130,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SLEEP_TOKEN; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN; +import static com.android.server.display.LMOFreeformDisplayAdapter.UNIQUE_ID_PREFIX; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; @@ -563,6 +564,8 @@ public void onIdMatch(InsetsSource source1, InsetsSource source2) { // TODO(multi-display): remove some of the usages. boolean isDefaultDisplay; + private boolean isFreeformDisplay; + /** Save allocating when calculating rects */ private final Rect mTmpRect = new Rect(); private final Region mTmpRegion = new Region(); @@ -1171,6 +1174,7 @@ public void onLowMemory() { mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp * mDisplayMetrics.densityDpi / DENSITY_DEFAULT; isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY; + isFreeformDisplay = mDisplayInfo.uniqueId.startsWith(UNIQUE_ID_PREFIX); mInsetsStateController = new InsetsStateController(this); initializeDisplayBaseInfo(); mDisplayFrames = new DisplayFrames(mInsetsStateController.getRawInsetsState(), @@ -4366,7 +4370,7 @@ InsetsControlTarget getImeControlTarget() { boolean forceDesktopMode() { return ("VNC".equals(mDisplay.getName()) || mWmService.mForceDesktopModeOnExternalDisplays) - && !isDefaultDisplay && !isPrivate(); + && !isDefaultDisplay && !isFreeformDisplay && !isPrivate(); } /** @see WindowManagerInternal#onToggleImeRequested */ @@ -5799,7 +5803,7 @@ && isPublicSecondaryDisplayWithDesktopModeForceEnabled()) { * also check {@link #isSystemDecorationsSupported()} to avoid breaking any security policy. */ boolean isPublicSecondaryDisplayWithDesktopModeForceEnabled() { - if (!mWmService.mForceDesktopModeOnExternalDisplays || isDefaultDisplay || isPrivate()) { + if (!mWmService.mForceDesktopModeOnExternalDisplays || isDefaultDisplay || isPrivate() && !isFreeformDisplay) { return false; } if (!isWindowingModeSupported(WINDOWING_MODE_FREEFORM)) { From 7cb4419d8af8e74903a6677f35dc16f04ffa3721 Mon Sep 17 00:00:00 2001 From: "xingyun.wang" Date: Wed, 15 Jan 2025 15:41:14 +0800 Subject: [PATCH 0573/1315] wm: Fixed screen flickering when launching horizontal apps from vertical apps The reason for the flashing screen is that when starting a horizontal application from a vertical application, the application relaunch Bug: 390049399 Test: 1.turn on Auto_rotate 2.enter the vertical application 3.launching a horizontal application from a vertical application Google: 3452389 Change-Id: If1654352818686e6b6fe4310425652099aff5573 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/wm/DisplayContent.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 92a45a1e81bb..3b79e0650ee9 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2153,7 +2153,9 @@ void continueUpdateOrientationForDiffOrienLaunchingApp() { return; } // The orientation of display is not changed. - clearFixedRotationLaunchingApp(); + if (!mTransitionController.isCollecting(this)) { + clearFixedRotationLaunchingApp(); + } } /** From c3cdd17681a1b3e1fc98048b309eccab7b516f80 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Sat, 20 Jul 2024 20:11:34 +0800 Subject: [PATCH 0574/1315] wm: Ensure freeform tasks bounds gets updated when launching tasks Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/ActivityTaskSupervisor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index ac8b92854e68..8c458ae6f6a4 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -3070,6 +3070,12 @@ int startActivityFromRecents(int callingPid, int callingUid, int taskId, mService.continueWindowLayout(); } } + if (activityOptions != null) { + final int windowingMode = activityOptions.getLaunchWindowingMode(); + if (windowingMode == WINDOWING_MODE_FREEFORM) { + task.setBounds(activityOptions.getLaunchBounds()); + } + } taskCallingUid = task.mCallingUid; callingPackage = task.mCallingPackage; callingFeatureId = task.mCallingFeatureId; From 1da72a5a48c8838370ac30ade164a1c92189cb90 Mon Sep 17 00:00:00 2001 From: jhonboy121 Date: Wed, 10 Nov 2021 19:32:43 +0530 Subject: [PATCH 0575/1315] services: WindowOrientationListener: bail out if rotation resolver service instance is null * this shouldn't be happening at all, so I'm just gonna add a null check here * here's the stack trace E SensorManager: Exception dispatching input event. E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: android.ui E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.rotationresolver.RotationResolverInternal.resolveRotation(android.rotationresolver.RotationResolverInternal$RotationResolverCallbackInternal, java.lang.String, int, int, long, android.os.CancellationSignal)' on a null object reference E AndroidRuntime: at com.android.server.wm.WindowOrientationListener$OrientationSensorJudge.onSensorChanged(WindowOrientationListener.java:1193) E AndroidRuntime: at android.hardware.SystemSensorManager$SensorEventQueue.dispatchSensorEvent(SystemSensorManager.java:886) E AndroidRuntime: at android.os.MessageQueue.nativePollOnce(Native Method) E AndroidRuntime: at android.os.MessageQueue.next(MessageQueue.java:335) E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:161) E AndroidRuntime: at android.os.Looper.loop(Looper.java:288) E AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:67) E AndroidRuntime: at com.android.server.ServiceThread.run(ServiceThread.java:44) E AndroidRuntime: at com.android.server.UiThread.run(UiThread.java:45) Change-Id: I5e6f9c4af8bb9fba2e9d85696958232878c7a815 Signed-off-by: jhonboy121 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/WindowOrientationListener.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/core/java/com/android/server/wm/WindowOrientationListener.java b/services/core/java/com/android/server/wm/WindowOrientationListener.java index d26616edfb44..0cdd058a0999 100644 --- a/services/core/java/com/android/server/wm/WindowOrientationListener.java +++ b/services/core/java/com/android/server/wm/WindowOrientationListener.java @@ -1177,6 +1177,10 @@ public void onSensorChanged(SensorEvent event) { return; } } + if (mRotationResolverService == null) { + // Bail out because RotationResolverManagerService wasn't started + return; + } String packageName = null; if (mActivityTaskManagerInternal != null) { From cbf10a179869169a07b4308efebabb5f8cd9460e Mon Sep 17 00:00:00 2001 From: Zhang Lian Date: Fri, 5 Sep 2025 17:46:28 +0800 Subject: [PATCH 0576/1315] Fix timeout issue when Google Calculator app remains occluded after power key double-click On a Pixel phone, open com.google.android.calculator and repeatedly press the power key twice in a short period of time, the calculator app will be shown on the keyguard, then click the power key twice again, we will find the screen is dark, caused by KEYGUARD_UNOCCLUDE Transition timed out for display not ready Reproduce the Method: 1. Open com.google.android.calculator 2. Continue to click the power key twice in a short period of time 3. When the top app is calculator, click the power key twice again Test result: UI is dark when powered on Change-Id: If7461b1c5827a6dc132ef70af38a78f86c3fe7d6 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/KeyguardController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 01e141ed731d..bc07397ba65e 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -289,7 +289,9 @@ void setKeyguardShown(int displayId, boolean keyguardShowing, boolean aodShowing private void setWakeTransitionReady() { if (mWindowManager.mAtmService.getTransitionController().getCollectingTransitionType() - == WindowManager.TRANSIT_WAKE) { + == WindowManager.TRANSIT_WAKE + || mWindowManager.mAtmService.getTransitionController().getCollectingTransitionType() + == WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE) { mWindowManager.mAtmService.getTransitionController().setReady( mRootWindowContainer.getDefaultDisplay()); } From 9243873e9f37ba585fffe70c80ad16658c4d6e68 Mon Sep 17 00:00:00 2001 From: Chris Crump Date: Sat, 25 Nov 2023 11:35:30 +0000 Subject: [PATCH 0577/1315] base: Initial SenseProvider for FaceSense service Thanks to Tobi to figure out the fix for FaceSense needing a reboot to work after registering a face model. Also thanks to someone5678 for fixing compile on QPR1. neobuddy89: Updated for A15. Squashed: From: COSMIC Date: Fri, 13 Dec 2024 13:38:26 +0530 Subject: services: Adapt face sense to A15-QPR1 Signed-off-by: Pranav Vashi From: Dmitrii Date: Sun, 14 Dec 2025 03:11:10 +0000 Subject: [PATCH] fwb: adapt faceunlock to a16 qpr2 Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi Change-Id: I1b83429a793a50fba292a38dbf5aa8a54ae5ace3 Co-authored-by: Adithya R Co-authored-by: Tobias Merkel Co-authored-by: someone5678 Co-authored-by: aswin7469 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../privacy/AppOpsPrivacyItemMonitor.kt | 8 + .../phone/BiometricUnlockController.java | 2 +- .../phone/KeyguardBypassController.kt | 11 +- services/core/Android.bp | 1 + .../sensors/AuthenticationClient.java | 2 +- .../biometrics/sensors/face/FaceService.java | 25 + .../face/sense/BiometricTestSessionImpl.java | 234 ++++ .../face/sense/FaceAuthenticationClient.java | 217 ++++ .../sensors/face/sense/FaceEnrollClient.java | 134 ++ .../sense/FaceGenerateChallengeClient.java | 111 ++ .../face/sense/FaceGetFeatureClient.java | 101 ++ .../face/sense/FaceInternalCleanupClient.java | 86 ++ .../sense/FaceInternalEnumerateClient.java | 64 + .../sensors/face/sense/FaceRemovalClient.java | 63 + .../face/sense/FaceResetLockoutClient.java | 83 ++ .../face/sense/FaceRevokeChallengeClient.java | 55 + .../face/sense/FaceSetFeatureClient.java | 93 ++ .../sense/FaceUpdateActiveUserClient.java | 85 ++ .../sensors/face/sense/SenseProvider.java | 1113 +++++++++++++++++ .../sensors/face/sense/SenseUtils.java | 61 + .../sensors/face/sense/TestHal.java | 144 +++ 21 files changed, 2687 insertions(+), 6 deletions(-) create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/BiometricTestSessionImpl.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceAuthenticationClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceEnrollClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGenerateChallengeClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGetFeatureClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalCleanupClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalEnumerateClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRemovalClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceResetLockoutClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRevokeChallengeClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceSetFeatureClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/FaceUpdateActiveUserClient.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/SenseProvider.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/SenseUtils.java create mode 100644 services/core/java/com/android/server/biometrics/sensors/face/sense/TestHal.java diff --git a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt index 967a16b1c0dd..1c3373c53f84 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt @@ -132,6 +132,10 @@ constructor( if (code in OPS_LOCATION && !locationAvailable) { return } + // Hide incoming chip from sense caller package + if (packageName == "co.aospa.sense") { + return + } if ( userTracker.userProfiles.any { it.id == UserHandle.getUserId(uid) } || code in USER_INDEPENDENT_OPS @@ -384,6 +388,10 @@ constructor( AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE else -> return null } + // Hide incoming chip from sense caller package + if (appOpItem.packageName == "co.aospa.sense") { + return null + } val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 318d35ec3bd3..6c4a7b3a409a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -686,7 +686,7 @@ private void logCalculateModeForFingerprint(boolean unlockingAllowed, boolean de final boolean unlockingAllowed = mUpdateMonitor.isUnlockingWithBiometricAllowed(isStrongBiometric); final boolean deviceDreaming = mUpdateMonitor.isDreaming(); - final boolean bypass = mKeyguardBypassController.getBypassEnabled() + final boolean bypass = mKeyguardBypassController.getBypassEnabledBiometric() || mAuthController.isUdfpsFingerDown(); final boolean isBouncerShowing = mKeyguardViewController.primaryBouncerIsOrWillBeShowing() || mKeyguardTransitionInteractor.getCurrentState() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index cd8977b26e2a..3deb4f90ed47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -118,6 +118,8 @@ constructor( notifyListeners() } + var bypassEnabledBiometric: Boolean = false + var bouncerShowing: Boolean = false var launchingAffordance: Boolean = false var qsExpanded = false @@ -155,7 +157,7 @@ constructor( 1 else 0 tunerService.addTunable( - { key, _ -> bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0 }, + { key, _ -> bypassEnabledBiometric = tunerService.getValue(key, dismissByDefault) != 0 }, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD, ) lockscreenUserManager.addUserChangedListener( @@ -198,8 +200,8 @@ constructor( biometricSourceType: BiometricSourceType, isStrongBiometric: Boolean, ): Boolean { - if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) { - val can = canBypass() + if (bypassEnabledBiometric) { + val can = biometricSourceType != BiometricSourceType.FACE || canBypass() if (!can && (isPulseExpanding || qsExpanded)) { pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric) } @@ -227,7 +229,7 @@ constructor( /** If keyguard can be dismissed because of bypass. */ fun canBypass(): Boolean { - if (bypassEnabled) { + if (bypassEnabledBiometric) { return when { bouncerShowing -> true keyguardTransitionInteractor.getCurrentState() == KeyguardState.ALTERNATE_BOUNCER -> @@ -261,6 +263,7 @@ constructor( pw.println(" mPendingUnlock: $pendingUnlock") } pw.println(" bypassEnabled: $bypassEnabled") + pw.println(" bypassEnabledBiometric: $bypassEnabledBiometric") pw.println(" canBypass: ${canBypass()}") pw.println(" bouncerShowing: $bouncerShowing") pw.println( diff --git a/services/core/Android.bp b/services/core/Android.bp index f2b14432fd85..81fc233d3818 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -253,6 +253,7 @@ java_library_static { "clipboard_flags_lib", "aconfig_adbdauth_flags_java_lib", "AvfBuildFlags", + "vendor.aospa.biometrics.face", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 090353b8f7c5..48b35269c749 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -116,7 +116,7 @@ public AuthenticationClient(@NonNull Context context, @NonNull Supplier lazyD } @LockoutTracker.LockoutMode - private int handleFailedAttempt(int userId) { + protected int handleFailedAttempt(int userId) { if (mLockoutTracker != null) { mLockoutTracker.addFailedAttemptForUser(getTargetUserId()); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index ab44ee723f43..ab9b00af8b79 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -26,6 +26,7 @@ import android.content.Context; import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.SensorProperties; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; @@ -39,6 +40,7 @@ import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceEnrollOptions; import android.hardware.face.FaceSensorConfigurations; +import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.FaceServiceReceiver; import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; @@ -73,6 +75,8 @@ import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.face.aidl.FaceProvider; +import com.android.server.biometrics.sensors.face.sense.SenseProvider; +import com.android.server.biometrics.sensors.face.sense.SenseUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -659,6 +663,24 @@ public void getFeature(final IBinder token, int userId, int feature, new ClientMonitorCallbackConverter(receiver), opPackageName); } + private List getSenseProviders() { + final List providers = new ArrayList<>(); + if (SenseUtils.canUseProvider()) { + FaceSensorPropertiesInternal props = new FaceSensorPropertiesInternal( + SenseProvider.DEVICE_ID, + SensorProperties.STRENGTH_WEAK, + 1, /** maxEnrollmentsPerUser **/ + new ArrayList(), + FaceSensorProperties.TYPE_RGB, + false, /** supportsFaceDetection **/ + false, /** supportsSelfIllumination **/ + false); /** resetLockoutRequiresChallenge **/ + SenseProvider provider = new SenseProvider(getContext(), mBiometricStateCallback, props, mLockoutResetDispatcher); + providers.add(provider); + } + return providers; + } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) public void registerAuthenticators( FaceSensorConfigurations faceSensorConfigurations) { @@ -674,10 +696,13 @@ public void registerAuthenticators( private List getProviders( FaceSensorConfigurations faceSensorConfigurations) { final List providers = new ArrayList<>(); + /* final Pair filteredSensorProps = filterAvailableHalInstances( faceSensorConfigurations); providers.add(mFaceProviderFunction.getFaceProvider(filteredSensorProps, faceSensorConfigurations.getResetLockoutRequiresChallenge())); + */ + providers.addAll(getSenseProviders()); return providers; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/BiometricTestSessionImpl.java new file mode 100644 index 000000000000..90dc55a7ed50 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/BiometricTestSessionImpl.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.ITestSession; +import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.face.Face; +import android.hardware.face.FaceAuthenticationFrame; +import android.hardware.face.FaceEnrollFrame; +import android.hardware.face.IFaceServiceReceiver; +import android.os.Binder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricStateCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.face.FaceUtils; + +import java.util.List; +import java.util.Random; + +/** + * A test session implementation for {@link SenseProvider}. See + * {@link android.hardware.biometrics.BiometricTestSession}. + */ +public class BiometricTestSessionImpl extends ITestSession.Stub { + + private static final String TAG = "face/sense/BiometricTestSessionImpl"; + + @NonNull private final Context mContext; + private final int mSensorId; + @NonNull private final ITestSessionCallback mCallback; + @NonNull private final Random mRandom; + + @NonNull private final SenseProvider.HalResultController mHalResultController; + @NonNull private final SenseProvider mSenseProvider; + + /** + * Internal receiver currently only used for enroll. Results do not need to be forwarded to the + * test, since enrollment is a platform-only API. The authentication path is tested through + * the public BiometricPrompt APIs and does not use this receiver. + */ + private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() { + @Override + public void onEnrollResult(Face face, int remaining) { + + } + + @Override + public void onAcquired(int acquireInfo, int vendorCode) { + + } + + @Override + public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) { + + } + + @Override + public void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) { + + } + + @Override + public void onAuthenticationFailed() { + + } + + @Override + public void onError(int error, int vendorCode) { + + } + + @Override + public void onRemoved(Face face, int remaining) { + + } + + @Override + public void onFeatureSet(boolean success, int feature) { + + } + + @Override + public void onFeatureGet(boolean success, int[] features, boolean[] featureState) { + + } + + @Override + public void onChallengeGenerated(int sensorId, int userId, long challenge) { + + } + + @Override + public void onAuthenticationFrame(FaceAuthenticationFrame frame) { + + } + + @Override + public void onEnrollmentFrame(FaceEnrollFrame frame) { + + } + }; + + BiometricTestSessionImpl(@NonNull Context context, int sensorId, + @NonNull ITestSessionCallback callback, @NonNull SenseProvider provider, + @NonNull SenseProvider.HalResultController halResultController) { + mContext = context; + mSensorId = sensorId; + mCallback = callback; + mSenseProvider = provider; + mHalResultController = halResultController; + mRandom = new Random(); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void setTestHalEnabled(boolean enabled) { + super.setTestHalEnabled_enforcePermission(); + + mSenseProvider.setTestHalEnabled(enabled); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void startEnroll(int userId) { + super.startEnroll_enforcePermission(); + + mSenseProvider.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver, + mContext.getOpPackageName(), new int[0] /* disabledFeatures */, + null /* previewSurface */, false /* debugConsent */, null); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void finishEnroll(int userId) { + super.finishEnroll_enforcePermission(); + + mHalResultController.onEnrollResult(1, userId, 0); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void acceptAuthentication(int userId) { + super.acceptAuthentication_enforcePermission(); + + // Fake authentication with any of the existing faces + List faces = FaceUtils.getInstance(mSensorId) + .getBiometricsForUser(mContext, userId); + if (faces.isEmpty()) { + Slog.w(TAG, "No faces, returning"); + return; + } + final int fid = faces.get(0).getBiometricId(); + byte[] hat = {0}; + mHalResultController.onAuthenticated(0 /* deviceId */, userId, hat); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void rejectAuthentication(int userId) { + super.rejectAuthentication_enforcePermission(); + + mHalResultController.onAuthenticated(0 /* deviceId */, userId, null); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void notifyAcquired(int userId, int acquireInfo) { + super.notifyAcquired_enforcePermission(); + + mHalResultController.onAcquired(userId, 0 /* deviceId */, 0 /* vendorCode */); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void notifyError(int userId, int errorCode) { + super.notifyError_enforcePermission(); + + mHalResultController.onError(0 /* deviceId */, 0 /* vendorCode */); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public void cleanupInternalState(int userId) { + super.cleanupInternalState_enforcePermission(); + + mSenseProvider.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() { + @Override + public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { + try { + mCallback.onCleanupStarted(clientMonitor.getTargetUserId()); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + } + } + + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + try { + mCallback.onCleanupFinished(clientMonitor.getTargetUserId()); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + } + } + }); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public int getSensorId() { + super.getSensorId_enforcePermission(); + return mSensorId; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceAuthenticationClient.java new file mode 100644 index 000000000000..32da449a0505 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceAuthenticationClient.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_NOT_DETECTED; +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_RECALIBRATE; +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_SENSOR_DIRTY; +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_VENDOR; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.Resources; +import android.hardware.SensorPrivacyManager; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.hardware.face.FaceAuthenticateOptions; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.R; +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.BiometricNotificationUtils; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.PerformanceTracker; +import com.android.server.biometrics.sensors.face.UsageStats; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +/** + * Face-specific authentication client supporting the {@link android.hardware.biometrics.face.V1_0} + * HIDL interface. + */ +class FaceAuthenticationClient + extends AuthenticationClient { + + private static final String TAG = "FaceAuthenticationClient"; + + private final UsageStats mUsageStats; + + private final int[] mBiometricPromptIgnoreList; + private final int[] mBiometricPromptIgnoreListVendor; + private final int[] mKeyguardIgnoreList; + private final int[] mKeyguardIgnoreListVendor; + + private int mLastAcquire; + + FaceAuthenticationClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, + @NonNull IBinder token, long requestId, + @NonNull ClientMonitorCallbackConverter listener, long operationId, + boolean restricted, @NonNull FaceAuthenticateOptions options, int cookie, + boolean requireConfirmation, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + boolean isStrongBiometric, @NonNull LockoutTracker lockoutTracker, + @NonNull UsageStats usageStats, boolean allowBackgroundAuthentication, + @Authenticators.Types int sensorStrength) { + super(context, lazyDaemon, token, listener, operationId, restricted, + options, cookie, requireConfirmation, logger, biometricContext, + isStrongBiometric, null /* taskStackListener */, + lockoutTracker, allowBackgroundAuthentication, true /* shouldVibrate */, + sensorStrength); + setRequestId(requestId); + mUsageStats = usageStats; + + final Resources resources = getContext().getResources(); + mBiometricPromptIgnoreList = resources.getIntArray( + R.array.config_face_acquire_biometricprompt_ignorelist); + mBiometricPromptIgnoreListVendor = resources.getIntArray( + R.array.config_face_acquire_vendor_biometricprompt_ignorelist); + mKeyguardIgnoreList = resources.getIntArray( + R.array.config_face_acquire_keyguard_ignorelist); + mKeyguardIgnoreListVendor = resources.getIntArray( + R.array.config_face_acquire_vendor_keyguard_ignorelist); + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + mState = STATE_STARTED; + } + + @NonNull + @Override + protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) { + return new ClientMonitorCompositeCallback( + getLogger().getAmbientLightProbe(true /* startWithClient */), callback); + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().authenticate(mOperationId); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting auth", e); + onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + protected void stopHalOperation() { + try { + getFreshDaemon().cancel(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting cancel", e); + onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + public boolean wasUserDetected() { + // Do not provide haptic feedback if the user was not detected, and an error (usually + // ERROR_TIMEOUT) is received. + return mLastAcquire != FACE_ACQUIRED_NOT_DETECTED + && mLastAcquire != FACE_ACQUIRED_SENSOR_DIRTY; + } + + @Override + protected void handleLifecycleAfterAuth(boolean authenticated) { + // For face, the authentication lifecycle ends either when + // 1) Authenticated == true + // 2) Error occurred + // 3) Authenticated == false + mCallback.onClientFinished(this, true /* success */); + } + + @Override + public void onAuthenticated(BiometricAuthenticator.Identifier identifier, + boolean authenticated, ArrayList token) { + super.onAuthenticated(identifier, authenticated, token); + + mState = STATE_STOPPED; + mUsageStats.addEvent(new UsageStats.AuthenticationEvent( + getStartTimeMs(), + System.currentTimeMillis() - getStartTimeMs() /* latency */, + authenticated, + 0 /* error */, + 0 /* vendorError */, + getTargetUserId())); + } + + @Override + public void onError(@BiometricConstants.Errors int error, int vendorCode) { + mUsageStats.addEvent(new UsageStats.AuthenticationEvent( + getStartTimeMs(), + System.currentTimeMillis() - getStartTimeMs() /* latency */, + false /* authenticated */, + error, + vendorCode, + getTargetUserId())); + + super.onError(error, vendorCode); + } + + private int[] getAcquireIgnorelist() { + return isBiometricPrompt() ? mBiometricPromptIgnoreList : mKeyguardIgnoreList; + } + + private int[] getAcquireVendorIgnorelist() { + return isBiometricPrompt() ? mBiometricPromptIgnoreListVendor : mKeyguardIgnoreListVendor; + } + + private boolean shouldSend(int acquireInfo, int vendorCode) { + if (acquireInfo == FACE_ACQUIRED_VENDOR) { + return !Utils.listContains(getAcquireVendorIgnorelist(), vendorCode); + } else { + return !Utils.listContains(getAcquireIgnorelist(), acquireInfo); + } + } + + @Override + public void onAcquired(int acquireInfo, int vendorCode) { + mLastAcquire = acquireInfo; + + if (acquireInfo == FACE_ACQUIRED_RECALIBRATE) { + BiometricNotificationUtils.showReEnrollmentNotification(getContext()); + } + @LockoutTracker.LockoutMode final int lockoutMode = + getLockoutTracker().getLockoutModeForUser(getTargetUserId()); + if (lockoutMode == LockoutTracker.LOCKOUT_NONE) { + PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId()); + pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation()); + } + + final boolean shouldSend = shouldSend(acquireInfo, vendorCode); + onAcquiredInternal(acquireInfo, vendorCode, shouldSend); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceEnrollClient.java new file mode 100644 index 000000000000..875aab5a264f --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceEnrollClient.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_VENDOR; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.face.Face; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.Surface; + +import com.android.internal.R; +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; +import com.android.server.biometrics.sensors.EnrollClient; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceEnrollClient extends EnrollClient { + + private static final String TAG = "FaceEnrollClient"; + + @NonNull private final int[] mDisabledFeatures; + @NonNull private final int[] mEnrollIgnoreList; + @NonNull private final int[] mEnrollIgnoreListVendor; + + FaceEnrollClient(@NonNull Context context, @NonNull Supplier lazyDaemon, + @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener, int userId, + @NonNull byte[] hardwareAuthToken, @NonNull String owner, long requestId, + @NonNull BiometricUtils utils, @NonNull int[] disabledFeatures, int timeoutSec, + @Nullable Surface previewSurface, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + int enrollReason) { + super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils, + timeoutSec, sensorId, false /* shouldVibrate */, logger, biometricContext, enrollReason); + setRequestId(requestId); + mDisabledFeatures = Arrays.copyOf(disabledFeatures, disabledFeatures.length); + mEnrollIgnoreList = getContext().getResources() + .getIntArray(R.array.config_face_acquire_enroll_ignorelist); + mEnrollIgnoreListVendor = getContext().getResources() + .getIntArray(R.array.config_face_acquire_vendor_enroll_ignorelist); + } + + @NonNull + @Override + protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) { + return new ClientMonitorCompositeCallback( + getLogger().getAmbientLightProbe(true /* startWithClient */), callback); + } + + @Override + protected boolean hasReachedEnrollmentLimit() { + final int limit = getContext().getResources().getInteger( + com.android.internal.R.integer.config_faceMaxTemplatesPerUser); + final int enrolled = mBiometricUtils.getBiometricsForUser(getContext(), getTargetUserId()) + .size(); + if (enrolled >= limit) { + Slog.w(TAG, "Too many faces registered, user: " + getTargetUserId()); + return true; + } + return false; + } + + @Override + public void onAcquired(int acquireInfo, int vendorCode) { + final boolean shouldSend; + if (acquireInfo == FACE_ACQUIRED_VENDOR) { + shouldSend = !Utils.listContains(mEnrollIgnoreListVendor, vendorCode); + } else { + shouldSend = !Utils.listContains(mEnrollIgnoreList, acquireInfo); + } + onAcquiredInternal(acquireInfo, vendorCode, shouldSend); + } + + @Override + protected void startHalOperation() { + final ArrayList token = new ArrayList<>(); + for (byte b : mHardwareAuthToken) { + token.add(Byte.valueOf(b)); + } + final ArrayList disabledFeatures = new ArrayList<>(); + for (int disabledFeature : mDisabledFeatures) { + disabledFeatures.add(disabledFeature); + } + + try { + getFreshDaemon().enroll(SenseUtils.toByteArray(token), mTimeoutSec, SenseUtils.toIntArray(disabledFeatures)); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting enroll", e); + onError(BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS, 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + protected void stopHalOperation() { + try { + getFreshDaemon().cancel(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting cancel", e); + onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGenerateChallengeClient.java new file mode 100644 index 000000000000..9a9414ff9449 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGenerateChallengeClient.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.face.IFaceServiceReceiver; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.util.Preconditions; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.GenerateChallengeClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceGenerateChallengeClient extends GenerateChallengeClient { + + private static final String TAG = "FaceGenerateChallengeClient"; + static final int CHALLENGE_TIMEOUT_SEC = 600; // 10 minutes + private static final ClientMonitorCallback EMPTY_CALLBACK = new ClientMonitorCallback() { + }; + + private final long mCreatedAt; + private List mWaiting; + private Long mChallengeResult; + + FaceGenerateChallengeClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, @NonNull IBinder token, + @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, long now) { + super(context, lazyDaemon, token, listener, userId, owner, sensorId, logger, + biometricContext); + mCreatedAt = now; + mWaiting = new ArrayList<>(); + } + + @Override + protected void startHalOperation() { + mChallengeResult = null; + try { + mChallengeResult = Long.valueOf(getFreshDaemon().generateChallenge(CHALLENGE_TIMEOUT_SEC)); + // send the result to the original caller via mCallback and any waiting callers + // that called reuseResult + sendChallengeResult(getListener(), mCallback); + for (IFaceServiceReceiver receiver : mWaiting) { + sendChallengeResult(new ClientMonitorCallbackConverter(receiver), EMPTY_CALLBACK); + } + } catch (RemoteException e) { + Slog.e(TAG, "generateChallenge failed", e); + mCallback.onClientFinished(this, false /* success */); + } finally { + mWaiting = null; + } + } + + /** @return An arbitrary time value for caching provided to the constructor. */ + public long getCreatedAt() { + return mCreatedAt; + } + + /** + * Reuse the result of this operation when it is available. The receiver will be notified + * immediately if a challenge has already been generated. + * + * @param receiver receiver to be notified of challenge result + */ + public void reuseResult(@NonNull IFaceServiceReceiver receiver) { + if (mWaiting != null) { + mWaiting.add(receiver); + } else { + sendChallengeResult(new ClientMonitorCallbackConverter(receiver), EMPTY_CALLBACK); + } + } + + private void sendChallengeResult(@NonNull ClientMonitorCallbackConverter receiver, + @NonNull ClientMonitorCallback ownerCallback) { + Preconditions.checkState(mChallengeResult != null, "result not available"); + try { + receiver.onChallengeGenerated(getSensorId(), getTargetUserId(), mChallengeResult); + ownerCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + ownerCallback.onClientFinished(this, false /* success */); + } + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGetFeatureClient.java new file mode 100644 index 000000000000..fdcb76db4eb2 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceGetFeatureClient.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.HalClientMonitor; + +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceGetFeatureClient extends HalClientMonitor { + + private static final String TAG = "FaceGetFeatureClient"; + + private final int mFeature; + private final int mFaceId; + private boolean mValue; + + FaceGetFeatureClient(@NonNull Context context, @NonNull Supplier lazyDaemon, + @NonNull IBinder token, @Nullable ClientMonitorCallbackConverter listener, int userId, + @NonNull String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + int feature, int faceId) { + super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, + logger, biometricContext, false /* restricted */); + mFeature = feature; + mFaceId = faceId; + } + + @Override + public void unableToStart() { + try { + if (getListener() != null) { + getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to send error", e); + } + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + startHalOperation(); + } + + @Override + protected void startHalOperation() { + try { + final boolean result = getFreshDaemon().getFeature(mFeature, mFaceId); + int[] features = new int[1]; + features[0] = mFeature; + boolean[] featureState = {result}; + mValue = result; + + if (getListener() != null) { + getListener().onFeatureGet(result, features, featureState); + } + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to getFeature", e); + mCallback.onClientFinished(this, false /* success */); + } + } + + boolean getValue() { + return mValue; + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_GET_FEATURE; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalCleanupClient.java new file mode 100644 index 000000000000..da363e2ee3f0 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalCleanupClient.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.face.Face; +import android.os.IBinder; +import android.util.Slog; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.InternalCleanupClient; +import com.android.server.biometrics.sensors.InternalEnumerateClient; +import com.android.server.biometrics.sensors.RemovalClient; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +class FaceInternalCleanupClient extends InternalCleanupClient { + private static final String TAG = "FaceInternalCleanupClient"; + + FaceInternalCleanupClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, int userId, @NonNull String owner, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, + @NonNull BiometricUtils utils, @NonNull Map authenticatorIds) { + super(context, lazyDaemon, userId, owner, sensorId, logger, biometricContext, + utils, authenticatorIds); + } + + @Override + protected InternalEnumerateClient getEnumerateClient(Context context, + Supplier lazyDaemon, IBinder token, int userId, String owner, + List enrolledList, BiometricUtils utils, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext) { + return new FaceInternalEnumerateClient(context, lazyDaemon, token, userId, owner, + enrolledList, utils, sensorId, logger, biometricContext); + } + + @Override + protected RemovalClient getRemovalClient(Context context, + Supplier lazyDaemon, IBinder token, + int biometricId, int userId, String owner, BiometricUtils utils, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + Map authenticatorIds, int reason) { + // Internal remove does not need to send results to anyone. Cleanup (enumerate + remove) + // is all done internally. + return new FaceRemovalClient(context, lazyDaemon, token, + null /* ClientMonitorCallbackConverter */, biometricId, userId, owner, utils, + sensorId, logger, biometricContext, authenticatorIds, reason); + } + + @Override + protected void onAddUnknownTemplate(int userId, + @NonNull BiometricAuthenticator.Identifier identifier) { + Slog.w(TAG, "Adding unknown template for user: " + userId); + mBiometricUtils.addBiometricForUser(getContext(), userId, (Face) identifier); + } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FACE; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalEnumerateClient.java new file mode 100644 index 000000000000..c86d0361f9e8 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceInternalEnumerateClient.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.face.Face; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.InternalEnumerateClient; + +import java.util.List; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +class FaceInternalEnumerateClient extends InternalEnumerateClient { + private static final String TAG = "FaceInternalEnumerateClient"; + + FaceInternalEnumerateClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, @NonNull IBinder token, int userId, + @NonNull String owner, @NonNull List enrolledList, + @NonNull BiometricUtils utils, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext) { + super(context, lazyDaemon, token, userId, owner, enrolledList, utils, sensorId, + logger, biometricContext); + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().enumerate(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting enumerate", e); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FACE; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRemovalClient.java new file mode 100644 index 000000000000..54494cb90a6e --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRemovalClient.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.face.Face; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.RemovalClient; + +import java.util.Map; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +class FaceRemovalClient extends RemovalClient { + private static final String TAG = "FaceRemovalClient"; + + private final int mBiometricId; + + FaceRemovalClient(@NonNull Context context, @NonNull Supplier lazyDaemon, + @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener, + int biometricId, int userId, @NonNull String owner, @NonNull BiometricUtils utils, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, + @NonNull Map authenticatorIds, int reason) { + super(context, lazyDaemon, token, listener, userId, owner, utils, sensorId, logger, + biometricContext, authenticatorIds, reason); + mBiometricId = biometricId; + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().remove(mBiometricId); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting remove", e); + mCallback.onClientFinished(this, false /* success */); + } + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceResetLockoutClient.java new file mode 100644 index 000000000000..fdacca292a6b --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceResetLockoutClient.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.HalClientMonitor; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceResetLockoutClient extends HalClientMonitor { + + private static final String TAG = "FaceResetLockoutClient"; + + private final byte[] mHardwareAuthToken; + + FaceResetLockoutClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, int userId, String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + @NonNull byte[] hardwareAuthToken) { + super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, + 0 /* cookie */, sensorId, logger, biometricContext, + true /* restricted */); + + mHardwareAuthToken = (byte[]) hardwareAuthToken.clone(); + } + + @Override + public void unableToStart() { + // Nothing to do here + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + startHalOperation(); + } + + public boolean interruptsPrecedingClients() { + return true; + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().resetLockout(mHardwareAuthToken); + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to reset lockout", e); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_RESET_LOCKOUT; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRevokeChallengeClient.java new file mode 100644 index 000000000000..7f42df49d0d9 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceRevokeChallengeClient.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.RevokeChallengeClient; + +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceRevokeChallengeClient extends RevokeChallengeClient { + + private static final String TAG = "FaceRevokeChallengeClient"; + + FaceRevokeChallengeClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, @NonNull IBinder token, + int userId, @NonNull String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext) { + super(context, lazyDaemon, token, userId, owner, sensorId, logger, biometricContext); + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().revokeChallenge(); + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "revokeChallenge failed", e); + mCallback.onClientFinished(this, false /* success */); + } + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceSetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceSetFeatureClient.java new file mode 100644 index 000000000000..b25ede17e19f --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceSetFeatureClient.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.HalClientMonitor; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceSetFeatureClient extends HalClientMonitor { + + private static final String TAG = "FaceSetFeatureClient"; + + private final int mFeature; + private final boolean mEnabled; + private final byte[] mHardwareAuthToken; + private final int mFaceId; + + FaceSetFeatureClient(@NonNull Context context, @NonNull Supplier lazyDaemon, + @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener, int userId, + @NonNull String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + int feature, boolean enabled, byte[] hardwareAuthToken, int faceId) { + super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, + logger, biometricContext, false /* restricted */); + mFeature = feature; + mEnabled = enabled; + mFaceId = faceId; + + mHardwareAuthToken = (byte[]) hardwareAuthToken.clone(); + } + + @Override + public void unableToStart() { + try { + getListener().onFeatureSet(false /* success */, mFeature); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to send error", e); + } + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + startHalOperation(); + } + + @Override + protected void startHalOperation() { + try { + getFreshDaemon().setFeature(mFeature, mEnabled, mHardwareAuthToken, mFaceId); + getListener().onFeatureSet(true, mFeature); + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to set feature: " + mFeature + " to enabled: " + mEnabled, e); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_SET_FEATURE; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceUpdateActiveUserClient.java new file mode 100644 index 000000000000..168eaf55ea9e --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/FaceUpdateActiveUserClient.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.Environment; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.HalClientMonitor; + +import java.io.File; +import java.util.Map; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; + +public class FaceUpdateActiveUserClient extends HalClientMonitor { + private static final String TAG = "FaceUpdateActiveUserClient"; + private static final String FACE_DATA_DIR = "facedata"; + + private final boolean mHasEnrolledBiometrics; + @NonNull private final Map mAuthenticatorIds; + + FaceUpdateActiveUserClient(@NonNull Context context, + @NonNull Supplier lazyDaemon, int userId, @NonNull String owner, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics, + @NonNull Map authenticatorIds) { + super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, + 0 /* cookie */, sensorId, logger, biometricContext, + true /* restricted */); + mHasEnrolledBiometrics = hasEnrolledBiometrics; + mAuthenticatorIds = authenticatorIds; + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + startHalOperation(); + } + + @Override + public void unableToStart() { + // Nothing to do here + } + + @Override + protected void startHalOperation() { + try { + final ISenseService daemon = getFreshDaemon(); + mAuthenticatorIds.put(getTargetUserId(), + mHasEnrolledBiometrics ? daemon.getAuthenticatorId() : 0L); + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to setActiveUser: " + e); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_UPDATE_ACTIVE_USER; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseProvider.java new file mode 100644 index 000000000000..31e679b982c2 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseProvider.java @@ -0,0 +1,1113 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.SynchronousUserSwitchObserver; +import android.app.UserSwitchObserver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.ITestSession; +import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.biometrics.face.V1_0.IBiometricsFace; +import android.hardware.face.Face; +import android.hardware.face.FaceAuthenticateOptions; +import android.hardware.face.FaceEnrollOptions; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceServiceReceiver; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.util.Slog; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.AuthenticationStatsCollector; +import com.android.server.biometrics.SensorServiceStateProto; +import com.android.server.biometrics.SensorStateProto; +import com.android.server.biometrics.UserStateProto; +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.AcquisitionClient; +import com.android.server.biometrics.sensors.AuthenticationConsumer; +import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; +import com.android.server.biometrics.sensors.BiometricNotificationUtils; +import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; +import com.android.server.biometrics.sensors.EnumerateConsumer; +import com.android.server.biometrics.sensors.ErrorConsumer; +import com.android.server.biometrics.sensors.LockoutResetDispatcher; +import com.android.server.biometrics.sensors.LockoutTracker; +import com.android.server.biometrics.sensors.PerformanceTracker; +import com.android.server.biometrics.sensors.RemovalConsumer; +import com.android.server.biometrics.sensors.face.FaceUtils; +import com.android.server.biometrics.sensors.face.LockoutHalImpl; +import com.android.server.biometrics.sensors.face.ServiceProvider; +import com.android.server.biometrics.sensors.face.UsageStats; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Clock; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import vendor.aospa.biometrics.face.ISenseService; +import vendor.aospa.biometrics.face.ISenseServiceReceiver; + +public class SenseProvider implements ServiceProvider { + + private static final String TAG = "SenseProvider"; + + private static final String BIND_SENSE_ACTION = "co.aospa.sense.BIND"; + private static final String PACKAGE_NAME = "co.aospa.sense"; + private static final String SERVICE_NAME = "co.aospa.sense.SenseService"; + + public static final int DEVICE_ID = 1008; + private static final int ENROLL_TIMEOUT_SEC = 75; + private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000; + private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS = + FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC * 1000; + @VisibleForTesting + public static Clock sSystemClock = Clock.systemUTC(); + + private boolean mIsBinding; + private boolean mTestHalEnabled; + + @NonNull private final FaceSensorPropertiesInternal mSensorProperties; + @NonNull private final BiometricStateCallback mBiometricStateCallback; + @NonNull private final Context mContext; + @NonNull private final BiometricScheduler mScheduler; + @NonNull private final Handler mHandler; + @NonNull private final Supplier mLazyDaemon; + @NonNull private final LockoutHalImpl mLockoutTracker; + @NonNull private final UsageStats mUsageStats; + @NonNull private final Map mAuthenticatorIds; + @Nullable private IBiometricsFace mDaemon; + @NonNull private final HalResultController mHalResultController; + @NonNull private final BiometricContext mBiometricContext; + @Nullable private AuthenticationStatsCollector mAuthenticationStatsCollector; + SparseArray mServices; + // for requests that do not use biometric prompt + @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0); + private int mCurrentUserId = UserHandle.USER_NULL; + private final int mSensorId; + private final List mGeneratedChallengeCount = new ArrayList<>(); + private FaceGenerateChallengeClient mGeneratedChallengeCache = null; + + private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + mCurrentUserId = newUserId; + ISenseService service = getDaemon(); + if (service == null) { + bindService(mCurrentUserId); + } + } + }; + + public static class HalResultController extends ISenseServiceReceiver.Stub { + /** + * Interface to sends results to the HalResultController's owner. + */ + public interface Callback { + /** + * Invoked when the HAL sends ERROR_HW_UNAVAILABLE. + */ + void onHardwareUnavailable(); + } + + private final int mSensorId; + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + @NonNull private final BiometricScheduler mScheduler; + @Nullable private Callback mCallback; + @NonNull private final LockoutHalImpl mLockoutTracker; + @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; + + + HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler, + @NonNull BiometricScheduler scheduler, @NonNull LockoutHalImpl lockoutTracker, + @NonNull LockoutResetDispatcher lockoutResetDispatcher) { + mSensorId = sensorId; + mContext = context; + mHandler = handler; + mScheduler = scheduler; + mLockoutTracker = lockoutTracker; + mLockoutResetDispatcher = lockoutResetDispatcher; + } + + public void setCallback(@Nullable Callback callback) { + mCallback = callback; + } + + @Override + public void onEnrollResult(int faceId, int userId, int remaining) { + mHandler.post(() -> { + final CharSequence name = FaceUtils.getLegacyInstance(mSensorId) + .getUniqueName(mContext, userId); + final Face face = new Face(name, faceId, Long.valueOf(DEVICE_ID)); + + final BaseClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof FaceEnrollClient)) { + Slog.e(TAG, "onEnrollResult for non-enroll client: " + + Utils.getClientName(client)); + return; + } + + final FaceEnrollClient enrollClient = (FaceEnrollClient) client; + enrollClient.onEnrollResult(face, remaining); + }); + } + + @Override + public void onAuthenticated(int faceId, int userId, byte[] token) { + mHandler.post(() -> { + final BaseClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof AuthenticationConsumer)) { + Slog.e(TAG, "onAuthenticated for non-authentication consumer: " + + Utils.getClientName(client)); + return; + } + + final AuthenticationConsumer authenticationConsumer = + (AuthenticationConsumer) client; + final boolean authenticated = faceId != 0; + final Face face = new Face("", faceId, DEVICE_ID); + authenticationConsumer.onAuthenticated(face, authenticated, SenseUtils.toByteArrayList(token)); + }); + } + + @Override + public void onAcquired(int userId, int acquiredInfo, int vendorCode) { + mHandler.post(() -> { + final BaseClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof AcquisitionClient)) { + Slog.e(TAG, "onAcquired for non-acquire client: " + + Utils.getClientName(client)); + return; + } + + final AcquisitionClient acquisitionClient = + (AcquisitionClient) client; + acquisitionClient.onAcquired(acquiredInfo, vendorCode); + }); + } + + @Override + public void onError(int error, int vendorCode) { + mHandler.post(() -> { + final BaseClientMonitor client = mScheduler.getCurrentClient(); + Slog.d(TAG, "handleError" + + ", client: " + (client != null ? client.getOwnerString() : null) + + ", error: " + error + + ", vendorCode: " + vendorCode); + if (!(client instanceof ErrorConsumer)) { + Slog.e(TAG, "onError for non-error consumer: " + Utils.getClientName( + client)); + return; + } + + final ErrorConsumer errorConsumer = (ErrorConsumer) client; + errorConsumer.onError(error, vendorCode); + + if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) { + Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE"); + if (mCallback != null) { + mCallback.onHardwareUnavailable(); + } + } + }); + } + + @Override + public void onRemoved(int[] faceIds, int userId) { + mHandler.post(() -> { + final BaseClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof RemovalConsumer)) { + Slog.e(TAG, "onRemoved for non-removal consumer: " + + Utils.getClientName(client)); + return; + } + + final RemovalConsumer removalConsumer = (RemovalConsumer) client; + + if (faceIds.length > 0) { + // Convert to old fingerprint-like behavior, where remove() receives + // one removal at a time. This way, remove can share some more common code. + for (int i = 0; i < faceIds.length; i++) { + final int id = faceIds[i]; + final Face face = new Face("", id, Long.valueOf(DEVICE_ID)); + final int remaining = (faceIds.length - i) - 1; + Slog.d(TAG, "Removed, faceId: " + id + ", remaining: " + remaining); + removalConsumer.onRemoved(face, remaining); + } + } else { + removalConsumer.onRemoved(null, 0 /* remaining */); + } + + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, UserHandle.USER_CURRENT); + }); + } + + @Override + public void onEnumerate(int[] faceIds, int userId) { + mHandler.post(() -> { + final BaseClientMonitor client = mScheduler.getCurrentClient(); + if (!(client instanceof EnumerateConsumer)) { + Slog.e(TAG, "onEnumerate for non-enumerate consumer: " + + Utils.getClientName(client)); + return; + } + + final EnumerateConsumer enumerateConsumer = (EnumerateConsumer) client; + + if (faceIds.length > 0) { + // Convert to old fingerprint-like behavior, where enumerate() receives one + // template at a time. This way, enumerate can share some more common code. + for (int i = 0; i < faceIds.length; i++) { + final Face face = new Face("", faceIds[i], Long.valueOf(DEVICE_ID)); + enumerateConsumer.onEnumerationResult(face, (faceIds.length - i) - 1); + } + } else { + // For face, the HIDL contract is to receive an empty list when there are no + // templates enrolled. Send a null identifier since we don't consume them + // anywhere, and send remaining == 0 so this code can be shared with Face@1.1 + enumerateConsumer.onEnumerationResult(null /* identifier */, 0); + } + }); + } + + @Override + public void onLockoutChanged(long duration) { + mHandler.post(() -> { + Slog.d(TAG, "onLockoutChanged: " + duration); + final @LockoutTracker.LockoutMode int lockoutMode; + if (duration == 0) { + lockoutMode = LockoutTracker.LOCKOUT_NONE; + } else if (duration == -1 || duration == Long.MAX_VALUE) { + lockoutMode = LockoutTracker.LOCKOUT_PERMANENT; + } else { + lockoutMode = LockoutTracker.LOCKOUT_TIMED; + } + + mLockoutTracker.setCurrentUserLockoutMode(lockoutMode); + + if (duration == 0) { + mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId); + } + }); + } + } + + @VisibleForTesting + public SenseProvider(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, + @NonNull FaceSensorPropertiesInternal sensorProps, + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull BiometricScheduler scheduler) { + mServices = new SparseArray<>(); + mIsBinding = false; + mSensorProperties = sensorProps; + mContext = context; + mBiometricStateCallback = biometricStateCallback; + mSensorId = sensorProps.sensorId; + mScheduler = scheduler; + mHandler = new Handler(Looper.getMainLooper()); + mBiometricContext = BiometricContext.getInstance(context); + mUsageStats = new UsageStats(context); + mAuthenticatorIds = new HashMap<>(); + mLazyDaemon = SenseProvider.this::getDaemon; + mLockoutTracker = new LockoutHalImpl(); + mHalResultController = new HalResultController(sensorProps.sensorId, context, mHandler, + mScheduler, mLockoutTracker, lockoutResetDispatcher); + mHalResultController.setCallback(() -> { + mDaemon = null; + mCurrentUserId = UserHandle.USER_NULL; + }); + mCurrentUserId = ActivityManager.getCurrentUser(); + + AuthenticationStatsBroadcastReceiver mBroadcastReceiver = + new AuthenticationStatsBroadcastReceiver( + mContext, + BiometricsProtoEnums.MODALITY_FACE, + (AuthenticationStatsCollector collector) -> { + Slog.d(TAG, "Initializing AuthenticationStatsCollector"); + mAuthenticationStatsCollector = collector; + }); + + try { + ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to register user switch observer"); + } + } + + public SenseProvider(Context context, BiometricStateCallback biometricStateCallback, FaceSensorPropertiesInternal sensorProps, LockoutResetDispatcher lockoutResetDispatcher) { + this(context, biometricStateCallback, sensorProps, lockoutResetDispatcher, new BiometricScheduler<>(context, 0, null)); + } + + private synchronized ISenseService getDaemon() { + if (mTestHalEnabled) { + final TestHal testHal = new TestHal(mContext, mSensorId); + testHal.setCallback(mHalResultController); + return testHal; + } + + ISenseService service = getService(mCurrentUserId); + if (service == null) { + bindService(mCurrentUserId); + } + return service; + } + + @Override + public boolean containsSensor(int sensorId) { + return mSensorId == sensorId; + } + + @Override + @NonNull + public List getSensorProperties() { + final List properties = new ArrayList<>(); + properties.add(mSensorProperties); + return properties; + } + + @NonNull + @Override + public FaceSensorPropertiesInternal getSensorProperties(int sensorId) { + return mSensorProperties; + } + + @Override + @NonNull + public List getEnrolledFaces(int sensorId, int userId) { + return FaceUtils.getLegacyInstance(mSensorId).getBiometricsForUser(mContext, userId); + } + + @Override + public boolean hasEnrollments(int sensorId, int userId) { + return !getEnrolledFaces(sensorId, userId).isEmpty(); + } + + @Override + @LockoutTracker.LockoutMode + public int getLockoutModeForUser(int sensorId, int userId) { + return mLockoutTracker.getLockoutModeForUser(userId); + } + + @Override + public long getAuthenticatorId(int sensorId, int userId) { + return mAuthenticatorIds.getOrDefault(userId, 0L); + } + + @Override + public boolean isHardwareDetected(int sensorId) { + return getDaemon() != null; + } + + private boolean isGeneratedChallengeCacheValid() { + return mGeneratedChallengeCache != null + && sSystemClock.millis() - mGeneratedChallengeCache.getCreatedAt() + < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS; + } + + private void incrementChallengeCount() { + mGeneratedChallengeCount.add(0, sSystemClock.millis()); + } + + private int decrementChallengeCount() { + final long now = sSystemClock.millis(); + // ignore values that are old in case generate/revoke calls are not matched + // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing + mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS); + if (!mGeneratedChallengeCount.isEmpty()) { + mGeneratedChallengeCount.remove(0); + } + return mGeneratedChallengeCount.size(); + } + + /** + * {@link IBiometricsFace} only supports a single in-flight challenge but there are cases where + * two callers both need challenges (e.g. resetLockout right before enrollment). + */ + @Override + public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + try { + receiver.onChallengeGenerated(sensorId, userId, 0L); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + incrementChallengeCount(); + + if (isGeneratedChallengeCacheValid()) { + Slog.d(TAG, "Current challenge is cached and will be reused"); + mGeneratedChallengeCache.reuseResult(receiver); + return; + } + + scheduleUpdateActiveUserWithoutHandler(userId); + + final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext, + mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId, + opPackageName, mSensorId, + createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, sSystemClock.millis()); + mGeneratedChallengeCache = client; + mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { + @Override + public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { + if (client != clientMonitor) { + Slog.e(TAG, "scheduleGenerateChallenge onClientStarted, mismatched client." + + " Expecting: " + client + ", received: " + clientMonitor); + } + } + }); + }); + } + + @Override + public void scheduleRevokeChallenge(int sensorId, int userId, @NonNull IBinder token, + @NonNull String opPackageName, long challenge) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + return; + } + final boolean shouldRevoke = decrementChallengeCount() == 0; + if (!shouldRevoke) { + Slog.w(TAG, "scheduleRevokeChallenge skipped - challenge still in use: " + + mGeneratedChallengeCount); + return; + } + + Slog.d(TAG, "scheduleRevokeChallenge executing - no active clients"); + mGeneratedChallengeCache = null; + + final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext, + mLazyDaemon, token, userId, opPackageName, mSensorId, + createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + if (client != clientMonitor) { + Slog.e(TAG, "scheduleRevokeChallenge, mismatched client." + + "Expecting: " + client + ", received: " + clientMonitor); + } + } + }); + }); + } + + @Override + public long scheduleEnroll(int sensorId, @NonNull IBinder token, + @NonNull byte[] hardwareAuthToken, int userId, @NonNull IFaceServiceReceiver receiver, + @NonNull String opPackageName, @NonNull int[] disabledFeatures, + @Nullable Surface previewSurface, boolean debugConsent, + @NonNull FaceEnrollOptions options) { + final long id = mRequestCounter.incrementAndGet(); + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + try { + receiver.onError(2, 0); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + scheduleUpdateActiveUserWithoutHandler(userId); + + BiometricNotificationUtils.cancelFaceReEnrollNotification(mContext); + + final FaceEnrollClient client = new FaceEnrollClient(mContext, mLazyDaemon, token, + new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, + opPackageName, id, FaceUtils.getLegacyInstance(mSensorId), disabledFeatures, + ENROLL_TIMEOUT_SEC, previewSurface, mSensorId, + createLogger(BiometricsProtoEnums.ACTION_ENROLL, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, + mContext.getResources().getInteger( + com.android.internal.R.integer.config_faceMaxTemplatesPerUser)); + + mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { + @Override + public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { + mBiometricStateCallback.onClientStarted(clientMonitor); + } + + @Override + public void onBiometricAction(int action) { + mBiometricStateCallback.onBiometricAction(action); + } + + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + mBiometricStateCallback.onClientFinished(clientMonitor, success); + if (success) { + // Update authenticatorIds + scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId()); + } + } + }); + }); + return id; + } + + @Override + public void cancelEnrollment(int sensorId, @NonNull IBinder token, long requestId) { + mHandler.post(() -> mScheduler.cancelEnrollment(token, requestId)); + } + + @Override + public long scheduleFaceDetect(@NonNull IBinder token, + @NonNull ClientMonitorCallbackConverter callback, + @NonNull FaceAuthenticateOptions options, int statsClient) { + throw new IllegalStateException("Face detect not supported by IBiometricsFace@1.0. Did you" + + "forget to check the supportsFaceDetection flag?"); + } + + @Override + public void cancelFaceDetect(int sensorId, @NonNull IBinder token, long requestId) { + throw new IllegalStateException("Face detect not supported by IBiometricsFace@1.0. Did you" + + "forget to check the supportsFaceDetection flag?"); + } + + @Override + public void scheduleAuthenticate(@NonNull IBinder token, long operationId, + int cookie, @NonNull ClientMonitorCallbackConverter receiver, + @NonNull FaceAuthenticateOptions options, long requestId, boolean restricted, + int statsClient, boolean allowBackgroundAuthentication) { + mHandler.post(() -> { + final int userId = options.getUserId(); + if (getDaemon() == null) { + bindService(mCurrentUserId); + try { + receiver.onError(1008, 0, 1, 0); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + scheduleUpdateActiveUserWithoutHandler(userId); + + final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorId); + final FaceAuthenticationClient client = new FaceAuthenticationClient(mContext, + mLazyDaemon, token, requestId, receiver, operationId, restricted, + options, cookie, false /* requireConfirmation */, + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + mBiometricContext, isStrongBiometric, mLockoutTracker, + mUsageStats, allowBackgroundAuthentication, + Utils.getCurrentStrength(mSensorId)); + mScheduler.scheduleClientMonitor(client); + }); + } + + @Override + public long scheduleAuthenticate(@NonNull IBinder token, long operationId, + int cookie, @NonNull ClientMonitorCallbackConverter receiver, + @NonNull FaceAuthenticateOptions options, boolean restricted, + int statsClient, boolean allowBackgroundAuthentication) { + final long id = mRequestCounter.incrementAndGet(); + + scheduleAuthenticate(token, operationId, cookie, receiver, + options, id, restricted, statsClient, allowBackgroundAuthentication); + + return id; + } + + @Override + public void cancelAuthentication(int sensorId, @NonNull IBinder token, long requestId) { + mHandler.post(() -> mScheduler.cancelAuthenticationOrDetection(token, requestId)); + } + + @Override + public void scheduleRemove(int sensorId, @NonNull IBinder token, int faceId, int userId, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + try { + receiver.onError(1, 0); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + scheduleUpdateActiveUserWithoutHandler(userId); + + final FaceRemovalClient client = new FaceRemovalClient(mContext, mLazyDaemon, token, + new ClientMonitorCallbackConverter(receiver), faceId, userId, opPackageName, + FaceUtils.getLegacyInstance(mSensorId), mSensorId, + createLogger(BiometricsProtoEnums.ACTION_REMOVE, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, mAuthenticatorIds, + BiometricsProtoEnums.UNENROLL_REASON_USER_REQUEST); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); + }); + } + + @Override + public void scheduleRemoveAll(int sensorId, @NonNull IBinder token, int userId, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + try { + receiver.onError(1, 0); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + scheduleUpdateActiveUserWithoutHandler(userId); + + // For IBiometricsFace@1.0, remove(0) means remove all enrollments + final FaceRemovalClient client = new FaceRemovalClient(mContext, mLazyDaemon, token, + new ClientMonitorCallbackConverter(receiver), 0 /* faceId */, userId, + opPackageName, + FaceUtils.getLegacyInstance(mSensorId), mSensorId, + createLogger(BiometricsProtoEnums.ACTION_REMOVE, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, mAuthenticatorIds, + BiometricsProtoEnums.UNENROLL_REASON_USER_REQUEST); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); + }); + } + + @Override + public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + } + if (getEnrolledFaces(sensorId, userId).isEmpty()) { + Slog.w(TAG, "Ignoring lockout reset, no templates enrolled for user: " + userId); + return; + } + + scheduleUpdateActiveUserWithoutHandler(userId); + + final FaceResetLockoutClient client = new FaceResetLockoutClient(mContext, + mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId, + createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, hardwareAuthToken); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); + }); + } + + @Override + public void scheduleSetFeature(int sensorId, @NonNull IBinder token, int userId, int feature, + boolean enabled, @NonNull byte[] hardwareAuthToken, + @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + return; + } + final List faces = getEnrolledFaces(sensorId, userId); + if (faces.isEmpty()) { + Slog.w(TAG, "Ignoring setFeature, no templates enrolled for user: " + userId); + return; + } + + scheduleUpdateActiveUserWithoutHandler(userId); + + final int faceId = faces.get(0).getBiometricId(); + final FaceSetFeatureClient client = new FaceSetFeatureClient(mContext, + mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId, + opPackageName, mSensorId, BiometricLogger.ofUnknown(mContext, mHandler), + mBiometricContext, + feature, enabled, hardwareAuthToken, faceId); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); + }); + } + + @Override + public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature, + @Nullable ClientMonitorCallbackConverter listener, @NonNull String opPackageName) { + mHandler.post(() -> { + if (getDaemon() == null) { + bindService(mCurrentUserId); + if (listener != null) { + try { + listener.onError(1008, 0, 1, 0); + return; + } catch (RemoteException e) { + e.printStackTrace(); + return; + } + } + return; + } + final List faces = getEnrolledFaces(sensorId, userId); + if (faces.isEmpty()) { + Slog.w(TAG, "Ignoring getFeature, no templates enrolled for user: " + userId); + return; + } + + scheduleUpdateActiveUserWithoutHandler(userId); + + final int faceId = faces.get(0).getBiometricId(); + final FaceGetFeatureClient client = new FaceGetFeatureClient(mContext, mLazyDaemon, + token, listener, userId, opPackageName, mSensorId, + BiometricLogger.ofUnknown(mContext, mHandler), mBiometricContext, + feature, faceId); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { + @Override + public void onClientFinished( + @NonNull BaseClientMonitor clientMonitor, boolean success) { + if (success && feature == BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION) { + final int settingsValue = client.getValue() ? 1 : 0; + Slog.d(TAG, "Updating attention value for user: " + userId + + " to value: " + settingsValue); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED, + settingsValue, userId); + } + } + }); + }); + } + + private void scheduleInternalCleanup(int userId, + @Nullable ClientMonitorCallback callback) { + mHandler.post(() -> { + scheduleUpdateActiveUserWithoutHandler(userId); + + final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext, + mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId, + createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, + FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback, + mBiometricStateCallback)); + }); + } + + @Override + public void scheduleInternalCleanup(int sensorId, int userId, + @Nullable ClientMonitorCallback callback) { + scheduleInternalCleanup(userId, mBiometricStateCallback); + } + + @Override + public void scheduleInternalCleanup(int sensorId, int userId, + @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) { + scheduleInternalCleanup(userId, mBiometricStateCallback); + } + + @Override + public void startPreparedClient(int sensorId, int cookie) { + mHandler.post(() -> { + mScheduler.startPreparedClient(cookie); + }); + } + + @Override + public void dumpProtoState(int sensorId, ProtoOutputStream proto, + boolean clearSchedulerBuffer) { + final long sensorToken = proto.start(SensorServiceStateProto.SENSOR_STATES); + + proto.write(SensorStateProto.SENSOR_ID, mSensorProperties.sensorId); + proto.write(SensorStateProto.MODALITY, SensorStateProto.FACE); + proto.write(SensorStateProto.CURRENT_STRENGTH, + Utils.getCurrentStrength(mSensorProperties.sensorId)); + proto.write(SensorStateProto.SCHEDULER, mScheduler.dumpProtoState(clearSchedulerBuffer)); + + for (UserInfo user : UserManager.get(mContext).getUsers()) { + final int userId = user.getUserHandle().getIdentifier(); + + final long userToken = proto.start(SensorStateProto.USER_STATES); + proto.write(UserStateProto.USER_ID, userId); + proto.write(UserStateProto.NUM_ENROLLED, FaceUtils.getLegacyInstance(mSensorId) + .getBiometricsForUser(mContext, userId).size()); + proto.end(userToken); + } + + proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN, + mSensorProperties.resetLockoutRequiresHardwareAuthToken); + proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE, + mSensorProperties.resetLockoutRequiresChallenge); + + proto.end(sensorToken); + } + + @Override + public void dumpProtoMetrics(int sensorId, FileDescriptor fd) { + } + + @Override + public void dumpInternal(int sensorId, PrintWriter pw) { + PerformanceTracker performanceTracker = + PerformanceTracker.getInstanceForSensorId(mSensorId); + + JSONObject dump = new JSONObject(); + try { + dump.put("service", TAG); + + JSONArray sets = new JSONArray(); + for (UserInfo user : UserManager.get(mContext).getUsers()) { + final int userId = user.getUserHandle().getIdentifier(); + final int c = FaceUtils.getLegacyInstance(mSensorId) + .getBiometricsForUser(mContext, userId).size(); + JSONObject set = new JSONObject(); + set.put("id", userId); + set.put("count", c); + set.put("accept", performanceTracker.getAcceptForUser(userId)); + set.put("reject", performanceTracker.getRejectForUser(userId)); + set.put("acquire", performanceTracker.getAcquireForUser(userId)); + set.put("lockout", performanceTracker.getTimedLockoutForUser(userId)); + set.put("permanentLockout", performanceTracker.getPermanentLockoutForUser(userId)); + // cryptoStats measures statistics about secure face transactions + // (e.g. to unlock password storage, make secure purchases, etc.) + set.put("acceptCrypto", performanceTracker.getAcceptCryptoForUser(userId)); + set.put("rejectCrypto", performanceTracker.getRejectCryptoForUser(userId)); + set.put("acquireCrypto", performanceTracker.getAcquireCryptoForUser(userId)); + sets.put(set); + } + + dump.put("prints", sets); + } catch (JSONException e) { + Slog.e(TAG, "dump formatting failure", e); + } + pw.println(dump); + pw.println("HAL deaths since last reboot: " + performanceTracker.getHALDeathCount()); + + mScheduler.dump(pw); + mUsageStats.print(pw); + } + + private void scheduleLoadAuthenticatorIds() { + // Note that this can be performed on the scheduler (as opposed to being done immediately + // when the HAL is (re)loaded, since + // 1) If this is truly the first time it's being performed (e.g. system has just started), + // this will be run very early and way before any applications need to generate keys. + // 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has + // just been reloaded), the framework already has a cache of the authenticatorIds. This + // is safe because authenticatorIds only change when A) new template has been enrolled, + // or B) all templates are removed. + mHandler.post(() -> { + for (UserInfo user : UserManager.get(mContext).getAliveUsers()) { + final int targetUserId = user.id; + if (!mAuthenticatorIds.containsKey(targetUserId)) { + scheduleUpdateActiveUserWithoutHandler(targetUserId); + } + } + }); + } + + /** + * Schedules the {@link FaceUpdateActiveUserClient} without posting the work onto the handler. + * Many/most APIs are user-specific. However, the HAL requires explicit "setActiveUser" + * invocation prior to authenticate/enroll/etc. Thus, internally we usually want to schedule + * this operation on the same lambda/runnable as those operations so that the ordering is + * correct. + */ + private void scheduleUpdateActiveUserWithoutHandler(int targetUserId) { + final boolean hasEnrolled = !getEnrolledFaces(mSensorId, targetUserId).isEmpty(); + final FaceUpdateActiveUserClient client = new FaceUpdateActiveUserClient(mContext, + mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorId, + createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, + BiometricsProtoEnums.CLIENT_UNKNOWN), + mBiometricContext, hasEnrolled, mAuthenticatorIds); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + if (success) { + mCurrentUserId = targetUserId; + } else { + Slog.w(TAG, "Failed to change user, still: " + mCurrentUserId); + } + } + }); + } + + public class SenseServiceConnection implements ServiceConnection { + private int mUserId; + + public SenseServiceConnection(int userId) { + mUserId = userId; + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + Slog.d(TAG, "Service connected : " + mUserId); + ISenseService senseService = ISenseService.Stub.asInterface(service); + if (senseService != null) { + synchronized (mServices) { + try { + senseService.setCallback(mHalResultController); + mServices.put(mUserId, senseService); + mHandler.post(() -> { + updateSchedule(); + }); + } catch (RemoteException e) { + e.printStackTrace(); + } + mIsBinding = false; + } + } + } + + public void updateSchedule() { + scheduleInternalCleanup(mUserId, null); + scheduleGetFeature(mSensorId, new Binder(), mUserId, 1, null, mContext.getOpPackageName()); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Slog.d(TAG, "Service disconnected : " + mUserId); + mServices.remove(mUserId); + mIsBinding = false; + if (mUserId == mCurrentUserId) { + mHandler.post(() -> { + updateResetSchedule(); + }); + } + mContext.unbindService(this); + } + + public void updateResetSchedule() { + BaseClientMonitor client = mScheduler.getCurrentClient(); + if (client != null && (client instanceof ErrorConsumer)) { + ErrorConsumer errorConsumer = (ErrorConsumer) client; + errorConsumer.onError(5, 0); + } + bindService(mUserId); + mScheduler.recordCrashState(); + mScheduler.reset(); + } + } + + private boolean isServiceEnabled() { + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(BIND_SENSE_ACTION); + intent.setClassName(PACKAGE_NAME, SERVICE_NAME); + ResolveInfo info = pm.resolveService(intent, 131072); + if (info != null && info.serviceInfo.isEnabled()) { + return true; + } + return false; + } + + private ISenseService getService(int userId) { + if (userId == -10000) { + scheduleUpdateActiveUserWithoutHandler(ActivityManager.getCurrentUser()); + } + return mServices.get(mCurrentUserId); + } + + public boolean bindService(int userId) { + Slog.d(TAG, "bindService " + userId); + if (!isServiceEnabled()) { + Slog.d(TAG, "Service disabled"); + return false; + } else if (mIsBinding) { + Slog.d(TAG, "Service is binding"); + return true; + } else { + if (userId != -10000 && getService(userId) == null) { + try { + Intent intent = new Intent(BIND_SENSE_ACTION); + intent.setClassName(PACKAGE_NAME, SERVICE_NAME); + boolean result = mContext.bindServiceAsUser(intent, new SenseServiceConnection(userId), 1, UserHandle.of(userId)); + if (result) { + mIsBinding = true; + } + return result; + } catch (SecurityException e) { + e.printStackTrace(); + } + } + return false; + } + } + + private BiometricLogger createLogger(int statsAction, int statsClient) { + return new BiometricLogger(mContext, mHandler, BiometricsProtoEnums.MODALITY_FACE, + statsAction, statsClient, mAuthenticationStatsCollector); + } + + /** + * Sends a debug message to the HAL with the provided FileDescriptor and arguments. + */ + public void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args) { } + + void setTestHalEnabled(boolean enabled) { + mTestHalEnabled = enabled; + } + + @NonNull + @Override + public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, + @NonNull String opPackageName) { + return new BiometricTestSessionImpl(mContext, mSensorId, callback, this, + mHalResultController); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseUtils.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseUtils.java new file mode 100644 index 000000000000..9e49fa1e7848 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/SenseUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.os.SystemProperties; + +import java.util.ArrayList; + +public class SenseUtils { + + public static boolean canUseProvider() { + return SystemProperties.getBoolean("ro.face.sense_service", false); + } + + public static ArrayList toByteArrayList(byte[] in) { + if (in == null) { + return null; + } + ArrayList out = new ArrayList<>(in.length); + for (byte c : in) { + out.add(Byte.valueOf(c)); + } + return out; + } + + public static byte[] toByteArray(ArrayList in) { + if (in == null) { + return null; + } + byte[] out = new byte[in.size()]; + for (int i = 0; i < in.size(); i++) { + out[i] = in.get(i).byteValue(); + } + return out; + } + + public static int[] toIntArray(ArrayList in) { + if (in == null) { + return null; + } + int[] out = new int[in.size()]; + for (int i = 0; i < in.size(); i++) { + out[i] = in.get(i).intValue(); + } + return out; + } +} \ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/face/sense/TestHal.java b/services/core/java/com/android/server/biometrics/sensors/face/sense/TestHal.java new file mode 100644 index 000000000000..0499bc51ede6 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/sense/TestHal.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.face.sense; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.face.V1_0.FaceError; +import android.hardware.biometrics.face.V1_0.OptionalBool; +import android.hardware.biometrics.face.V1_0.OptionalUint64; +import android.hardware.biometrics.face.V1_0.Status; +import android.hardware.face.Face; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.sensors.face.FaceUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import vendor.aospa.biometrics.face.ISenseService; +import vendor.aospa.biometrics.face.ISenseServiceReceiver; + +public class TestHal extends ISenseService.Stub { + private static final String TAG = "face.hidl.TestHal"; + + @NonNull + private final Context mContext; + private final int mSensorId; + + @Nullable + private ISenseServiceReceiver mCallback; + private int mUserId; + + TestHal(@NonNull Context context, int sensorId) { + mContext = context; + mSensorId = sensorId; + } + + @Override + public void setCallback(ISenseServiceReceiver clientCallback) { + mCallback = clientCallback; + } + + @Override + public long generateChallenge(int challengeTimeoutSec) { + Slog.w(TAG, "generateChallenge"); + return 0L; + } + + @Override + public void enroll(byte[] hat, int timeoutSec, int[] disabledFeatures) { + Slog.w(TAG, "enroll"); + } + + @Override + public int revokeChallenge() { + return 0; + } + + @Override + public void setFeature(int feature, boolean enabled, byte[] token, int faceId) { } + + @Override + public boolean getFeature(int feature, int faceId) { + return false; + } + + @Override + public int getFeatureCount() throws RemoteException { + return 0; + } + + @Override + public int getAuthenticatorId() { + return 0; + } + + @Override + public void cancel() throws RemoteException { + if (mCallback != null) { + mCallback.onError(0 /* deviceId */, 0 /* vendorCode */); + } + } + + @Override + public int enumerate() throws RemoteException { + Slog.w(TAG, "enumerate"); + if (mCallback != null) { + mCallback.onEnumerate(new int[0], 0 /* userId */); + } + return 0; + } + + @Override + public void remove(int faceId) throws RemoteException { + Slog.w(TAG, "remove"); + if (mCallback != null) { + if (faceId == 0) { + List faces = FaceUtils.getInstance(mSensorId).getBiometricsForUser(mContext, mUserId); + if (faces.size() <= 0) { + mCallback.onError(6, 0); + return; + } + int[] faceIds = new int[faces.size()]; + for (int i = 0; i < faces.size(); i++) { + Face face = faces.get(i); + faceIds[i] = face.getBiometricId(); + } + + mCallback.onRemoved(faceIds, mUserId); + } else { + mCallback.onRemoved(new int[]{faceId}, mUserId); + } + } + } + + @Override + public void authenticate(long operationId) { + Slog.w(TAG, "authenticate"); + } + + @Override + public void resetLockout(byte[] hat) { + Slog.w(TAG, "resetLockout"); + } + +} From 041362b239305f34785554a0edbdb84f1f660ef8 Mon Sep 17 00:00:00 2001 From: someone5678 Date: Mon, 8 Jan 2024 18:42:13 +0900 Subject: [PATCH 0578/1315] FaceService: Conditionally add ParanoidSense * We allows devices to use their own FaceService as ParanoidSense has weak modality * To make above impl complete, conditionally add ParanoidSense and un-comment AIDL provider support Change-Id: I7321580b87499f4020d3b080779e2f9cb1a6fce6 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../biometrics/sensors/face/FaceService.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index ab9b00af8b79..0fac4c57d39f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -665,19 +665,17 @@ public void getFeature(final IBinder token, int userId, int feature, private List getSenseProviders() { final List providers = new ArrayList<>(); - if (SenseUtils.canUseProvider()) { - FaceSensorPropertiesInternal props = new FaceSensorPropertiesInternal( - SenseProvider.DEVICE_ID, - SensorProperties.STRENGTH_WEAK, - 1, /** maxEnrollmentsPerUser **/ - new ArrayList(), - FaceSensorProperties.TYPE_RGB, - false, /** supportsFaceDetection **/ - false, /** supportsSelfIllumination **/ - false); /** resetLockoutRequiresChallenge **/ - SenseProvider provider = new SenseProvider(getContext(), mBiometricStateCallback, props, mLockoutResetDispatcher); - providers.add(provider); - } + FaceSensorPropertiesInternal props = new FaceSensorPropertiesInternal( + SenseProvider.DEVICE_ID, + SensorProperties.STRENGTH_WEAK, + 1, /** maxEnrollmentsPerUser **/ + new ArrayList(), + FaceSensorProperties.TYPE_RGB, + false, /** supportsFaceDetection **/ + false, /** supportsSelfIllumination **/ + false); /** resetLockoutRequiresChallenge **/ + SenseProvider provider = new SenseProvider(getContext(), mBiometricStateCallback, props, mLockoutResetDispatcher); + providers.add(provider); return providers; } @@ -696,13 +694,14 @@ public void registerAuthenticators( private List getProviders( FaceSensorConfigurations faceSensorConfigurations) { final List providers = new ArrayList<>(); - /* + if (SenseUtils.canUseProvider()) { + providers.addAll(getSenseProviders()); + return providers; + } final Pair filteredSensorProps = filterAvailableHalInstances( faceSensorConfigurations); providers.add(mFaceProviderFunction.getFaceProvider(filteredSensorProps, faceSensorConfigurations.getResetLockoutRequiresChallenge())); - */ - providers.addAll(getSenseProviders()); return providers; } From f685cab9e3aebbcc25f28e18de87164dcb59954e Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Sun, 28 Apr 2024 08:22:52 +0800 Subject: [PATCH 0579/1315] SystemUI: Disable FaceUnlock Lockouts Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../repository/DeviceEntryFaceAuthRepository.kt | 4 ++-- .../SystemUIDeviceEntryFaceAuthInteractor.kt | 14 +++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 48a062c8c228..477916dffa08 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -237,7 +237,7 @@ constructor( ) override fun setLockedOut(isLockedOut: Boolean) { - _isLockedOut.value = isLockedOut + _isLockedOut.value = false } private val faceLockoutResetCallback = @@ -507,7 +507,7 @@ constructor( override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { val errorStatus = ErrorFaceAuthenticationStatus(errorCode, errString.toString()) if (errorStatus.isLockoutError()) { - _isLockedOut.value = true + _isLockedOut.value = false } _isAuthenticated.value = false _authenticationStatus.value = errorStatus diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index c87b3aaab6f8..92d144d9a73d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -445,17 +445,9 @@ constructor( return } - if (repository.isLockedOut.value && !isBypassEnabled.value) { - faceAuthenticationStatusOverride.value = - ErrorFaceAuthenticationStatus( - BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT, - context.resources.getString(R.string.keyguard_face_unlock_unavailable), - ) - } else { - faceAuthenticationStatusOverride.value = null - faceAuthenticationLogger.authRequested(uiEvent) - repository.requestAuthenticate(uiEvent, fallbackToDetection = fallbackToDetect) - } + faceAuthenticationStatusOverride.value = null + faceAuthenticationLogger.authRequested(uiEvent) + repository.requestAuthenticate(uiEvent, fallbackToDetection = fallbackToDetect) } override fun isFaceAuthEnabledAndEnrolled(): Boolean = From 930952d599b7eb6eee8feb2ca2b283c5665ab6eb Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 30 Nov 2023 10:39:01 +0800 Subject: [PATCH 0580/1315] SystemUI: Implement face unlock recognition animation and text Co-authored-by: jhenrique09 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/face_not_verified.xml | 16 +++ .../SystemUI/res/drawable/face_scanning.xml | 10 ++ .../SystemUI/res/drawable/face_success.xml | 16 +++ .../res/layout/keyguard_bottom_area.xml | 11 ++ .../SystemUI/res/values/matrixx_strings.xml | 3 + .../KeyguardIndicationController.java | 60 ++++++++++- .../statusbar/phone/FaceUnlockImageView.kt | 102 ++++++++++++++++++ 7 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res/drawable/face_not_verified.xml create mode 100644 packages/SystemUI/res/drawable/face_scanning.xml create mode 100644 packages/SystemUI/res/drawable/face_success.xml create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/FaceUnlockImageView.kt diff --git a/packages/SystemUI/res/drawable/face_not_verified.xml b/packages/SystemUI/res/drawable/face_not_verified.xml new file mode 100644 index 000000000000..03b0566d6f04 --- /dev/null +++ b/packages/SystemUI/res/drawable/face_not_verified.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/face_scanning.xml b/packages/SystemUI/res/drawable/face_scanning.xml new file mode 100644 index 000000000000..27de6c80ef20 --- /dev/null +++ b/packages/SystemUI/res/drawable/face_scanning.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/face_success.xml b/packages/SystemUI/res/drawable/face_success.xml new file mode 100644 index 000000000000..d63dacb1512e --- /dev/null +++ b/packages/SystemUI/res/drawable/face_success.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index e602d6c8848d..26d7f803fb1d 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -30,6 +30,17 @@ android:layout_gravity="bottom|center_horizontal" android:orientation="vertical"> + + 4G / LTE 5G / NR Unsupported + + + Recognizing face... diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index bd4aa27e5ebb..3636757c2ece 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -117,6 +117,7 @@ import com.android.systemui.res.R; import com.android.systemui.securelockdevice.domain.interactor.SecureLockDeviceInteractor; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.FaceUnlockImageView; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -156,6 +157,8 @@ public class KeyguardIndicationController { private static final int MSG_SHOW_ACTION_TO_UNLOCK = 1; private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 2; + private static final int MSG_SHOW_RECOGNIZING_FACE = 3; + private static final int MSG_HIDE_RECOGNIZING_FACE = 4; private static final long TRANSIENT_BIOMETRIC_ERROR_TIMEOUT = 1300; public static final long DEFAULT_MESSAGE_TIME = 3500; public static final long DEFAULT_HIDE_DELAY_MS = @@ -173,6 +176,7 @@ public class KeyguardIndicationController { private final Lazy mSecureLockDeviceInteractor; private ViewGroup mIndicationArea; + private FaceUnlockImageView mFaceIconView; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; private final IBatteryStats mBatteryInfo; @@ -238,6 +242,8 @@ public class KeyguardIndicationController { private final FaceHelpMessageDeferral mFaceAcquiredMessageDeferral; private boolean mInited; + private boolean mFaceDetectionRunning; + private int mCurrentDivider; private boolean mHasDashCharger; @@ -287,6 +293,15 @@ public void onScreenTurnedOn() { mBiometricErrorMessageToShowOnScreenOn = null; } } + + @Override + public void onScreenTurnedOff() { + if (mFaceDetectionRunning) { + mFaceDetectionRunning = false; + mBiometricErrorMessageToShowOnScreenOn = null; + hideFaceUnlockRecognizingMessage(); + } + } }; private boolean mFaceLockedOutThisAuthSession; @@ -380,6 +395,11 @@ public void handleMessage(Message msg) { showActionToUnlock(); } else if (msg.what == MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON) { mBiometricErrorMessageToShowOnScreenOn = null; + } else if (msg.what == MSG_SHOW_RECOGNIZING_FACE) { + mBiometricErrorMessageToShowOnScreenOn = null; + showFaceUnlockRecognizingMessage(); + } else if (msg.what == MSG_HIDE_RECOGNIZING_FACE) { + hideFaceUnlockRecognizingMessage(); } } }; @@ -440,6 +460,7 @@ public void onConfigurationChanged() { public void setIndicationArea(ViewGroup indicationArea) { mIndicationArea = indicationArea; + mFaceIconView = indicationArea.findViewById(R.id.face_unlock_icon); mTopIndicationView = indicationArea.findViewById(R.id.keyguard_indication_text); mLockScreenIndicationView = indicationArea.findViewById( R.id.keyguard_indication_text_bottom); @@ -1064,6 +1085,12 @@ private void showBiometricMessage( return; } + if (TextUtils.equals(biometricMessage, mContext.getString(R.string.keyguard_face_successful_unlock))) { + mFaceIconView.setState(FaceUnlockImageView.State.SUCCESS); + } else if (TextUtils.equals(biometricMessage, mContext.getString(R.string.keyguard_face_failed))) { + mFaceIconView.setState(FaceUnlockImageView.State.NOT_VERIFIED); + } + if (mBiometricMessageSource != null && biometricSourceType == null) { // If there's a current biometric message showing and a non-biometric message // arrives, update the followup message with the non-biometric message. @@ -1096,6 +1123,25 @@ private void hideBiometricMessage() { } } + private void showFaceUnlockRecognizingMessage() { + mFaceIconView.setVisibility(View.VISIBLE); + mFaceIconView.setState(FaceUnlockImageView.State.SCANNING); + showBiometricMessage(mContext.getResources().getString( + R.string.face_unlock_recognizing), FACE); + } + + private void hideFaceUnlockRecognizingMessage() { + if (mFaceIconView != null) { + mFaceIconView.setVisibility(View.GONE); + } + String faceUnlockMessage = mContext.getResources().getString( + R.string.face_unlock_recognizing); + if (mBiometricMessage != null && mBiometricMessage == faceUnlockMessage) { + mBiometricMessage = null; + hideBiometricMessage(); + } + } + /** * Hides transient indication. */ @@ -1660,8 +1706,17 @@ public void onTrustAgentErrorMessage(CharSequence message) { @Override public void onBiometricRunningStateChanged(boolean running, BiometricSourceType biometricSourceType) { - if (!running && biometricSourceType == FACE) { - showTrustAgentErrorMessage(mTrustAgentErrorMessage); + if (biometricSourceType == BiometricSourceType.FACE) { + mFaceDetectionRunning = running; + if (running) { + mHandler.removeMessages(MSG_HIDE_RECOGNIZING_FACE); + mHandler.removeMessages(MSG_SHOW_RECOGNIZING_FACE); + mHandler.sendEmptyMessageDelayed(MSG_SHOW_RECOGNIZING_FACE, 100); + } else { + mHandler.removeMessages(MSG_SHOW_RECOGNIZING_FACE); + mHandler.removeMessages(MSG_HIDE_RECOGNIZING_FACE); + mHandler.sendEmptyMessageDelayed(MSG_HIDE_RECOGNIZING_FACE, 100); + } } } @@ -1839,6 +1894,7 @@ public void onDozingChanged(boolean dozing) { if (mDozing) { hideBiometricMessage(); + hideFaceUnlockRecognizingMessage(); } updateDeviceEntryIndication(false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FaceUnlockImageView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FaceUnlockImageView.kt new file mode 100644 index 000000000000..5b6c7beff8dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FaceUnlockImageView.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.phone + +import android.animation.ObjectAnimator +import android.animation.AnimatorSet +import android.animation.PropertyValuesHolder +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import android.view.animation.LinearInterpolator +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.DecelerateInterpolator + +import com.android.systemui.R + +class FaceUnlockImageView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ImageView(context, attrs, defStyleAttr) { + + enum class State { + SCANNING, NOT_VERIFIED, SUCCESS + } + + private var currentState: State = State.SCANNING + private val scanningAnimation: ObjectAnimator = createScanningAnimation() + private val successShakeAnimation: AnimatorSet = createShakeAnimation(5f) + private val failureShakeAnimation: AnimatorSet = createShakeAnimation(10f) + + init { + updateDrawable(animate = false) + } + + fun setState(state: State) { + if (currentState != state) { + currentState = state + updateDrawable(animate = true) + handleAnimationForState(state) + } + } + + private fun updateDrawable(animate: Boolean) { + setImageResource(when (currentState) { + State.SCANNING -> R.drawable.face_scanning + State.NOT_VERIFIED -> R.drawable.face_not_verified + State.SUCCESS -> R.drawable.face_success + }) + } + + private fun createScanningAnimation(): ObjectAnimator { + val scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 1.2f, 1f) + val scaleY = PropertyValuesHolder.ofFloat("scaleY", 1f, 1.2f, 1f) + val scanningAnimator = ObjectAnimator.ofPropertyValuesHolder(this, scaleX, scaleY) + scanningAnimator.duration = 1000 + scanningAnimator.repeatCount = ObjectAnimator.INFINITE + scanningAnimator.interpolator = LinearInterpolator() + return scanningAnimator + } + + private fun createShakeAnimation(amplitude: Float): AnimatorSet { + val animatorSet = AnimatorSet() + val translationX = PropertyValuesHolder.ofFloat("translationX", 0f, amplitude, -amplitude, amplitude, -amplitude, 0f) + val shakeAnimator = ObjectAnimator.ofPropertyValuesHolder(this, translationX) + shakeAnimator.duration = 500 + shakeAnimator.interpolator = AccelerateDecelerateInterpolator() + animatorSet.play(shakeAnimator) + return animatorSet + } + + private fun handleAnimationForState(state: State) { + when (state) { + State.SCANNING -> { + failureShakeAnimation.cancel() + successShakeAnimation.cancel() + scanningAnimation.start() + } + State.NOT_VERIFIED -> { + scanningAnimation.cancel() + successShakeAnimation.cancel() + failureShakeAnimation.start() + } + State.SUCCESS -> { + scanningAnimation.cancel() + failureShakeAnimation.cancel() + successShakeAnimation.start() + } + } + } +} From 4285345e6eaaa3de2b2acba84abb9b065394e2c0 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 4 Dec 2023 01:45:18 +0530 Subject: [PATCH 0581/1315] SystemUI: Use proper tint for face unlock icon Signed-off-by: Pranav Vashi Signed-off-by: AnierinB Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/layout/keyguard_bottom_area.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 26d7f803fb1d..4a8447a27aeb 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -38,7 +38,7 @@ android:padding="@dimen/keyguard_affordance_fixed_padding" android:layout_gravity="center" android:scaleType="fitCenter" - android:tint="#FFFFFF" + android:tint="?android:attr/textColorPrimary" android:visibility="gone" /> Date: Sun, 17 Dec 2023 08:36:37 +0800 Subject: [PATCH 0582/1315] SystemUI: Implement bouncer face unlock animation mnri: move face indicator to top, rewrite code and fix inconsistencies niv: Update for U QPR3 Co-authored-by: jhenrique09 Co-authored-by: someone5678 Co-authored-by: Alvin Francis Signed-off-by: Pranav Vashi Signed-off-by: minaripenguin Signed-off-by: Alvin Francis Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/drawable/face_success.xml | 8 +- .../res/layout/keyguard_bottom_area.xml | 27 ++- .../android/systemui/FaceScanningOverlay.kt | 5 + .../ui/binder/KeyguardIndicationAreaBinder.kt | 1 + .../KeyguardIndicationController.java | 30 ++- .../statusbar/phone/FaceUnlockImageView.kt | 199 ++++++++++++++---- .../phone/StatusBarKeyguardViewManager.java | 65 +++++- 7 files changed, 274 insertions(+), 61 deletions(-) diff --git a/packages/SystemUI/res/drawable/face_success.xml b/packages/SystemUI/res/drawable/face_success.xml index d63dacb1512e..ad63e1b1f1b7 100644 --- a/packages/SystemUI/res/drawable/face_success.xml +++ b/packages/SystemUI/res/drawable/face_success.xml @@ -6,11 +6,5 @@ android:tint="?attr/colorControlNormal"> - - + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z"/> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 4a8447a27aeb..49e0c984ee69 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -23,24 +23,35 @@ android:outlineProvider="none" > + + + + From 36b81a4bdd4f0b3d65820b70bf65b41fceba5907 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 4 Jan 2025 19:53:36 +0530 Subject: [PATCH 0589/1315] SystemUI: Add face unlock icon to keyguard blueprint Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/layout/keyguard_bottom_area.xml | 2 +- .../SystemUI/res/values/matrixx_dimens.xml | 3 + .../keyguard/KeyguardViewConfigurator.kt | 4 + .../ui/binder/KeyguardIndicationAreaBinder.kt | 1 - .../ui/view/KeyguardIndicationAreaTop.kt | 64 ++++++++++++++++ .../blueprints/DefaultKeyguardBlueprint.kt | 3 + .../DefaultIndicationAreaTopSection.kt | 73 +++++++++++++++++++ 7 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationAreaTop.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaTopSection.kt diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 95726a1f63d5..3f86b3b873a1 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -34,7 +34,7 @@ android:id="@+id/face_unlock_icon" android:layout_height="24dp" android:layout_width="24dp" - android:layout_marginTop="6dp" + android:layout_marginTop="@dimen/face_unlock_icon_margin_top" android:layout_gravity="center" android:scaleType="fitCenter" android:tint="@android:color/white" diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index 53d3ce90a532..a1644be49dca 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -100,4 +100,7 @@ 4dp + + + 6dp diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index b5e86ec14433..2072ec1b7fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -31,6 +31,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardJankBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea +import com.android.systemui.keyguard.ui.view.KeyguardIndicationAreaTop import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -127,6 +128,9 @@ constructor( private fun initializeViews() { val indicationArea = KeyguardIndicationArea(context, null) keyguardIndicationController.setIndicationArea(indicationArea) + + val indicationAreaTop = KeyguardIndicationAreaTop(context, null) + keyguardIndicationController.setIndicationAreaTop(indicationAreaTop) } private fun bindKeyguardRootView() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index b60dccd64eab..1a8bf001e0c1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -60,7 +60,6 @@ object KeyguardIndicationAreaBinder { disposables += DisposableHandle { previous?.let { indicationController.indicationArea = it } } - indicationController.setIndicationAreaTop(view) val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text) val indicationTextBottom: TextView = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationAreaTop.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationAreaTop.kt new file mode 100644 index 000000000000..99e9c796ddc7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationAreaTop.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.FaceUnlockImageView + +class KeyguardIndicationAreaTop( + context: Context, + private val attrs: AttributeSet?, +) : + LinearLayout( + context, + attrs, + ) { + + init { + setId(R.id.keyguard_indication_area_top) + orientation = LinearLayout.VERTICAL + + addView( + faceUnlockIcon(), + LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + gravity = Gravity.CENTER_HORIZONTAL + topMargin = R.dimen.face_unlock_icon_margin_top.dp() + } + ) + } + + private fun faceUnlockIcon(): FaceUnlockImageView { + return FaceUnlockImageView(context, attrs).apply { + id = R.id.face_unlock_icon + gravity = Gravity.CENTER + setAlpha(0.8f) + setVisibility(View.GONE) + } + } + + private fun Int.dp(): Int { + return context.resources.getDimensionPixelSize(this) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 8197bb8757e0..8e2fe3d7ba59 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotifica import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection +import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaTopSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection @@ -53,6 +54,7 @@ class DefaultKeyguardBlueprint constructor( accessibilityActionsSection: AccessibilityActionsSection, defaultIndicationAreaSection: DefaultIndicationAreaSection, + defaultIndicationAreaTopSection: DefaultIndicationAreaTopSection, defaultDeviceEntrySection: DefaultDeviceEntrySection, defaultShortcutsSection: DefaultShortcutsSection, @Named(KEYGUARD_AMBIENT_INDICATION_AREA_SECTION) @@ -75,6 +77,7 @@ constructor( listOfNotNull( accessibilityActionsSection, defaultIndicationAreaSection, + defaultIndicationAreaTopSection, defaultShortcutsSection, defaultAmbientIndicationAreaSection.getOrNull(), defaultSettingsPopupMenuSection, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaTopSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaTopSection.kt new file mode 100644 index 000000000000..d67edcd054e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaTopSection.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 crDroid Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.keyguard.ui.view.KeyguardIndicationAreaTop +import com.android.systemui.res.R +import javax.inject.Inject + +class DefaultIndicationAreaTopSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + private val indicationAreaViewId = R.id.keyguard_indication_area_top + + override fun addViews(constraintLayout: ConstraintLayout) { + val view = KeyguardIndicationAreaTop(context, null) + constraintLayout.addView(view) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + constrainWidth(indicationAreaViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + constrainHeight(indicationAreaViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + connect( + indicationAreaViewId, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + ) + connect( + indicationAreaViewId, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + indicationAreaViewId, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + constraintLayout.removeView(indicationAreaViewId) + } +} From e13f3d29f1afb8d47e9ffc85b42262088cd75937 Mon Sep 17 00:00:00 2001 From: Fabian Leutenegger Date: Tue, 14 Jan 2025 09:20:06 +0100 Subject: [PATCH 0590/1315] SystemUI: Implement pocket lock check for faceunlock Change-Id: I36007db4e07ffb927192871edcf5bd6a3f99ed5a Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUIDeviceEntryFaceAuthInteractor.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 92d144d9a73d..9e959472f328 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -20,6 +20,8 @@ import android.app.trust.TrustManager import android.content.Context import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType +import android.pocket.IPocketCallback +import android.pocket.PocketManager import android.security.Flags.secureLockDevice import android.service.dreams.Flags.dreamsV2 import com.android.keyguard.KeyguardUpdateMonitor @@ -119,7 +121,22 @@ constructor( private val listeners: MutableList = mutableListOf() + private var isDeviceInPocket: Boolean = false + private var pocketManager: PocketManager? = context.getSystemService(Context.POCKET_SERVICE) as? PocketManager + private val pocketCallback = object : IPocketCallback.Stub() { + override fun onStateChanged(state: Boolean, reason: Int) { + if (reason == PocketManager.REASON_SENSOR) { + isDeviceInPocket = state + } else { + isDeviceInPocket = false + } + } + } + override fun start() { + // Register pocket callback to detect pocket state changes + pocketManager?.addCallback(pocketCallback) + // Todo(b/310594096): there is a dependency cycle introduced by the repository depending on // KeyguardBypassController, which in turn depends on KeyguardUpdateMonitor through // its other dependencies. Once bypassEnabled state is available through a repository, we @@ -378,7 +395,7 @@ constructor( fun isAuthOrDetectRunning(): Boolean = isAuthRunning() || isDetectRunning() - override fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value + override fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value && !isDeviceInPocket override fun isFaceAuthStrong(): Boolean = facePropertyRepository.sensorInfo.value?.strength == SensorStrength.STRONG @@ -437,6 +454,7 @@ constructor( authenticationStatus.filterIsInstance() private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) { + if (isDeviceInPocket) return if ( secureLockDevice() && (_pendingFaceAuthConfirmationInSecureLockDevice.value || From a3d53d60ea606c8bed92f33d63f6d7c268cf301a Mon Sep 17 00:00:00 2001 From: jhenrique09 Date: Mon, 14 Feb 2022 14:14:38 -0300 Subject: [PATCH 0591/1315] SenseProvider: Allow our face unlock to be used on third-party apps neobuddy89: Added changes to make it work for all cases - legacy, senseprovider, biometricsOnboardingEducation Ref: https://github.com/crdroidandroid/android_frameworks_base/pull/1246/commits/553fb7b4efd1a852611b0868684c5e4505ab9e79 Suggested-by: Jenna Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/biometrics/BiometricService.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 9f58551d91b6..44351d0b3630 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -110,6 +110,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; +import com.android.server.biometrics.sensors.face.sense.SenseUtils; + /** * System service that arbitrates the modality for BiometricPrompt to use. */ @@ -349,7 +351,7 @@ public SettingObserver(Context context, Handler handler, public void updateContentObserver() { mContentResolver.unregisterContentObserver(this); - if (mUseLegacyFaceOnlySettings) { + if (mUseLegacyFaceOnlySettings || SenseUtils.canUseProvider()) { mContentResolver.registerContentObserver(FACE_UNLOCK_KEYGUARD_ENABLED, false /* notifyForDescendants */, this /* observer */, @@ -358,7 +360,9 @@ public void updateContentObserver() { false /* notifyForDescendants */, this /* observer */, UserHandle.USER_ALL); - } else if (com.android.settings.flags.Flags.biometricsOnboardingEducation()) { + } + if (!mUseLegacyFaceOnlySettings && + com.android.settings.flags.Flags.biometricsOnboardingEducation()) { mContentResolver.registerContentObserver(FINGERPRINT_KEYGUARD_ENABLED, false /* notifyForDescendants */, this /* observer */, @@ -375,7 +379,7 @@ public void updateContentObserver() { false /* notifyForDescendants */, this /* observer */, UserHandle.USER_ALL); - } else { + } else if (!mUseLegacyFaceOnlySettings) { mContentResolver.registerContentObserver(BIOMETRIC_KEYGUARD_ENABLED, false /* notifyForDescendants */, this /* observer */, @@ -533,7 +537,8 @@ public boolean getEnabledOnKeyguard(int userId, int modality) { } } else { if (!mBiometricEnabledOnKeyguard.containsKey(userId)) { - if (mUseLegacyFaceOnlySettings) { + if (mUseLegacyFaceOnlySettings || + (SenseUtils.canUseProvider() && modality == TYPE_FACE)) { onChange(true /* selfChange */, FACE_UNLOCK_KEYGUARD_ENABLED, userId); } else { onChange(true /* selfChange */, BIOMETRIC_KEYGUARD_ENABLED, userId); @@ -561,7 +566,8 @@ public boolean getEnabledForApps(int userId, int modality) { } } else { if (!mBiometricEnabledForApps.containsKey(userId)) { - if (mUseLegacyFaceOnlySettings) { + if (mUseLegacyFaceOnlySettings || + (SenseUtils.canUseProvider() && modality == TYPE_FACE)) { onChange(true /* selfChange */, FACE_UNLOCK_APP_ENABLED, userId); } else { onChange(true /* selfChange */, BIOMETRIC_APP_ENABLED, userId); From 218a88873d17397033f352640bd6930981c6a1a6 Mon Sep 17 00:00:00 2001 From: Abhay Singh Gill Date: Wed, 11 Dec 2024 11:15:15 +0530 Subject: [PATCH 0592/1315] Face: Do not throw exception if client does not support invalidation Signed-off-by: Abhay Singh Gill Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/biometrics/sensors/face/ServiceProvider.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java index 6f76cdae4539..e047a7cf5b84 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java @@ -67,8 +67,6 @@ public interface ServiceProvider extends BiometricServiceProvider Date: Sun, 8 Sep 2024 19:42:16 +0800 Subject: [PATCH 0593/1315] NSSLC: Prevent possible memory leak Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- ...tificationStackScrollLayoutController.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index ff5ad1d32abe..072a3cab5c13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -238,6 +238,7 @@ public void onViewDetachedFromWindow(View v) { mColorUpdateLogger.logTriggerEvent("NSSLC.onViewDetachedFromWindow()"); mConfigurationController.removeCallback(mConfigurationListener); mStatusBarStateController.removeCallback(mStateListener); + mTunerService.removeTunable(mTunable); } }; @@ -996,16 +997,7 @@ public void onEntryUpdated(NotificationEntry entry) { mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation); mTunerService.addTunable( - (key, newValue) -> { - switch (key) { - case Settings.Secure.NOTIFICATION_HISTORY_ENABLED: - mHistoryEnabled = null; // invalidate - break; - case HIGH_PRIORITY: - mView.setHighPriorityBeforeSpeedBump("1".equals(newValue)); - break; - } - }, + mTunable, HIGH_PRIORITY, Settings.Secure.NOTIFICATION_HISTORY_ENABLED); @@ -1037,6 +1029,17 @@ public void onEntryUpdated(NotificationEntry entry) { mViewBinder.bindWhileAttached(mView, this); } + private TunerService.Tunable mTunable = (key, newValue) -> { + switch (key) { + case Settings.Secure.NOTIFICATION_HISTORY_ENABLED: + mHistoryEnabled = null; // Invalidate + break; + case HIGH_PRIORITY: + mView.setHighPriorityBeforeSpeedBump("1".equals(newValue)); + break; + } + }; + public void setApplyHunTranslation(boolean apply) { mView.setApplyHunTranslation(apply); } From a5e236b998017a5a7ec96b879f43171d6eb006a5 Mon Sep 17 00:00:00 2001 From: Vala Zadeh Date: Tue, 17 Jan 2023 12:40:57 -0800 Subject: [PATCH 0594/1315] Fix incorrect text shown at PUK lock screen This fixes the incorrect text shown on the PUK screen when user entered an incorrect PIN 3 times, entered the PUK and the new PIN, removed and reinserted the SIM and entered an incorrect PIN 3 times. At this point, the UE should show "Enter PUK code to continue", but it instead showed "Unlocking SIM card...". Change-Id: Ic9c6b185301d3215ad39cb8e40931d0365b47faa CRs-Fixed: 3382789 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/keyguard/KeyguardSimPukViewController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index a676e5df14b8..2ff018b421af 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -251,7 +251,7 @@ void onSimLockChangedResponse(final PinResult result) { else { Log.d(TAG, "onSimCheckResponse " + " empty One result " + result.toString()); - if (result.getAttemptsRemaining() >= 0) { + if (result.getAttemptsRemaining() > 0) { mRemainingAttempts = result.getAttemptsRemaining(); mMessageAreaController.setMessage( mView.getPukPasswordErrorMessage( From 06afd2bde76e605ef24ce4bf60ee50c2e36bb50f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 31 Dec 2025 07:20:49 +0530 Subject: [PATCH 0595/1315] base: Allow tuning app switch key regardless hw key Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/policy/PhoneWindowManager.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f672511908b5..206453d628e9 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3318,7 +3318,6 @@ private void updateKeyAssignments() { final boolean hasMenu = (activeHardwareKeys & KEY_MASK_MENU) != 0; final boolean hasAssist = (activeHardwareKeys & KEY_MASK_ASSIST) != 0; - final boolean hasAppSwitch = (activeHardwareKeys & KEY_MASK_APP_SWITCH) != 0; final ContentResolver resolver = mContext.getContentResolver(); final Resources res = mContext.getResources(); @@ -3371,11 +3370,9 @@ private void updateKeyAssignments() { LineageSettings.System.KEY_ASSIST_LONG_PRESS_ACTION, mAssistLongPressAction); } - if (hasAppSwitch) { - mAppSwitchPressAction = Action.fromSettings(resolver, - LineageSettings.System.KEY_APP_SWITCH_ACTION, - mAppSwitchPressAction); - } + mAppSwitchPressAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_APP_SWITCH_ACTION, + mAppSwitchPressAction); mAppSwitchLongPressAction = Action.fromSettings(resolver, LineageSettings.System.KEY_APP_SWITCH_LONG_PRESS_ACTION, mAppSwitchLongPressAction); From 7b8a4a31bee033b8792777d4a8d7f2547c7b4d20 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 30 Nov 2024 16:28:38 +0530 Subject: [PATCH 0596/1315] base: Add customization for double tap recents key [2/3] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/policy/PhoneWindowManager.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 206453d628e9..894e747789c6 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -713,6 +713,7 @@ public void onDrawn() { private Action mAssistLongPressAction; private Action mAppSwitchPressAction; private Action mAppSwitchLongPressAction; + private Action mAppSwitchDoubleTapAction; private Action mCornerLongSwipeAction; private Action mEdgeLongSwipeAction; private Action mThreeFingersSwipeAction; @@ -1093,6 +1094,9 @@ void observe() { resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.KEY_APP_SWITCH_LONG_PRESS_ACTION), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(LineageSettings.System.getUriFor( + LineageSettings.System.KEY_APP_SWITCH_DOUBLE_TAP_ACTION), false, this, + UserHandle.USER_ALL); resolver.registerContentObserver(LineageSettings.System.getUriFor( LineageSettings.System.KEY_CORNER_LONG_SWIPE_ACTION), false, this, UserHandle.USER_ALL); @@ -1809,8 +1813,8 @@ private void backLongPress() { } } - private void appSwitchPress() { - if (!keyguardOn() && mAppSwitchPressAction != Action.NOTHING) { + private void appSwitchPress(int count) { + if (count == 1 && !keyguardOn() && mAppSwitchPressAction != Action.NOTHING) { if (mAppSwitchPressAction != Action.APP_SWITCH) { cancelPreloadRecentApps(); } @@ -1820,6 +1824,16 @@ private void appSwitchPress() { KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD); performKeyAction(mAppSwitchPressAction, event); + } else if (count == 2 && !keyguardOn() && mAppSwitchDoubleTapAction != Action.NOTHING) { + if (mAppSwitchDoubleTapAction != Action.APP_SWITCH) { + cancelPreloadRecentApps(); + } + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_APP_SWITCH, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD); + + performKeyAction(mAppSwitchDoubleTapAction, event); } } @@ -3072,7 +3086,7 @@ boolean supportLongPress() { @Override int getMaxMultiPressCount() { - return 1; + return mAppSwitchDoubleTapAction != Action.NOTHING ? 2 : 1; } @Override @@ -3082,7 +3096,7 @@ void onKeyGesture(@NonNull SingleKeyGestureEvent event) { } switch (event.getType()) { case SINGLE_KEY_GESTURE_TYPE_PRESS: - appSwitchPress(); + appSwitchPress(event.getPressCount()); break; case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS: appSwitchLongPress(); @@ -3340,6 +3354,7 @@ private void updateKeyAssignments() { mAppSwitchPressAction = Action.APP_SWITCH; mAppSwitchLongPressAction = Action.fromIntSafe(res.getInteger( org.lineageos.platform.internal.R.integer.config_longPressOnAppSwitchBehavior)); + mAppSwitchDoubleTapAction = Action.LAST_APP; mCornerLongSwipeAction = Action.SEARCH; mEdgeLongSwipeAction = Action.NOTHING; @@ -3376,6 +3391,9 @@ private void updateKeyAssignments() { mAppSwitchLongPressAction = Action.fromSettings(resolver, LineageSettings.System.KEY_APP_SWITCH_LONG_PRESS_ACTION, mAppSwitchLongPressAction); + mAppSwitchDoubleTapAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_APP_SWITCH_DOUBLE_TAP_ACTION, + mAppSwitchDoubleTapAction); mCornerLongSwipeAction = Action.fromSettings(resolver, LineageSettings.System.KEY_CORNER_LONG_SWIPE_ACTION, @@ -5312,7 +5330,8 @@ && isWakeKeyWhenScreenOff(keyCode)) { if (!keyguardOn()) { if (down) { if (mAppSwitchPressAction == Action.APP_SWITCH - || mAppSwitchLongPressAction == Action.APP_SWITCH) { + || mAppSwitchLongPressAction == Action.APP_SWITCH + || mAppSwitchDoubleTapAction == Action.APP_SWITCH) { preloadRecentApps(); } } From dacb9588237b24ad53406cf92b04edae473e6e71 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Sat, 6 Apr 2024 01:36:47 +0300 Subject: [PATCH 0597/1315] SystemUI: MediaHierarchyManager: Initiate allowMediaPlayerOnLockScreen making it stay true until the setting changes is a bad idea. it breaks keeping the big clock when no media is showing until the setting is toggled again. and probably more (see usages of this class) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../media/controls/ui/controller/MediaHierarchyManager.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index 79bfcf5f31cc..872bdea85f61 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -700,6 +700,12 @@ constructor( R.dimen.lockscreen_shade_media_transition_distance ) inSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources) + allowMediaPlayerOnLockScreen = + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT + ) } /** From 1b078d2dea82c4c869cfdca4732fd696a5bde9ae Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Sun, 1 Dec 2024 15:15:35 +0900 Subject: [PATCH 0598/1315] Settings: Expose clipboard auto clear setting [1/3] [someone5678] * Adapt to current project * Use Settings instead of DeviceConfig as GMS don't likes it Ref: https://gitlab.com/CalyxOS/platform_packages_apps_Settings/-/commit/72db57c966dd996cb7a9106e52a6416c589df92d https://github.com/crdroidandroid/android_packages_apps_Settings/commit/48e00e2b812db5ea85253edaced9b1fc48db0efd https://github.com/crdroidandroid/android_packages_apps_crDroidSettings/commit/33c49aa70c9250d64b284af6ee46d5dad618497c https://github.com/crdroidandroid/android_packages_apps_crDroidSettings/commit/bc81eea9cce7d964b2a3f64ae318cd8d0c840adb Issue: calyxos#2208 Change-Id: Ib6b62b5bc97473e192ed3dbdb4feadd868e3bdf7 Signed-off-by: someone5678 <59456192+someone5678@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 12 +++++++++ .../server/clipboard/ClipboardService.java | 26 ++++++------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0b2660c35f62..4cdc7d0b5f25 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14236,6 +14236,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String SHOW_FPS_OVERLAY = "show_fps_overlay"; + /** + * Whether to enable clipboard auto clear + * @hide + */ + public static final String CLIPBOARD_AUTO_CLEAR_ENABLED = "clipboard_auto_clear_enabled"; + + /** + * Timeout length for clipboard auto clear + * @hide + */ + public static final String CLIPBOARD_AUTO_CLEAR_TIMEOUT = "clipboard_auto_clear_timeout"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index bbae6a01786e..f10e166dc3d8 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -138,19 +138,6 @@ public class ClipboardService extends SystemService { @VisibleForTesting public static final long DEFAULT_CLIPBOARD_TIMEOUT_MILLIS = 3600000; - /** - * Device config property for whether clipboard auto clear is enabled on the device - **/ - public static final String PROPERTY_AUTO_CLEAR_ENABLED = - "auto_clear_enabled"; - - /** - * Device config property for time period in milliseconds after which clipboard is auto - * cleared - **/ - public static final String PROPERTY_AUTO_CLEAR_TIMEOUT = - "auto_clear_timeout"; - // DeviceConfig properties private static final String PROPERTY_MAX_CLASSIFICATION_LENGTH = "max_classification_length"; private static final int DEFAULT_MAX_CLASSIFICATION_LENGTH = 400; @@ -612,12 +599,16 @@ private void checkAndSetPrimaryClip( } } + private boolean isAutoClearEnabled() { + return Settings.Secure.getInt(getContext().getContentResolver(), + Settings.Secure.CLIPBOARD_AUTO_CLEAR_ENABLED, 1) == 1; + } + private void scheduleAutoClear( @UserIdInt int userId, int intendingUid, int intendingDeviceId) { final long oldIdentity = Binder.clearCallingIdentity(); try { - if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CLIPBOARD, - PROPERTY_AUTO_CLEAR_ENABLED, true)) { + if (isAutoClearEnabled()) { Pair userIdDeviceId = new Pair<>(userId, intendingDeviceId); mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR, userIdDeviceId); @@ -637,9 +628,8 @@ private void scheduleAutoClear( } private long getTimeoutForAutoClear() { - return DeviceConfig.getLong(DeviceConfig.NAMESPACE_CLIPBOARD, - PROPERTY_AUTO_CLEAR_TIMEOUT, - DEFAULT_CLIPBOARD_TIMEOUT_MILLIS); + return Settings.Secure.getLong(getContext().getContentResolver(), + Settings.Secure.CLIPBOARD_AUTO_CLEAR_TIMEOUT, DEFAULT_CLIPBOARD_TIMEOUT_MILLIS); } @Override From c161b6518d08b1321a413852683fb688191bce4b Mon Sep 17 00:00:00 2001 From: Sourajit Karmakar Date: Tue, 28 Jan 2025 22:54:05 -0500 Subject: [PATCH 0599/1315] screenrecord: Set entire screenrecord as the default option I don't wanna record a single app and it's so annoying to open the dropdown menu Every. Single. Time. Change-Id: Idef4ec7a7ea76d3fb648dbc4089a9141439bfa2e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../screenrecord/ScreenRecordPermissionDialogDelegate.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index 8610ebf39010..3ae268f081da 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -24,6 +24,7 @@ import androidx.annotation.StyleRes import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.permission.BaseMediaProjectionPermissionContentManager import com.android.systemui.mediaprojection.permission.BaseMediaProjectionPermissionDialogDelegate +import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN import com.android.systemui.mediaprojection.permission.SINGLE_APP import com.android.systemui.mediaprojection.permission.ScreenShareMode import com.android.systemui.plugins.ActivityStarter @@ -84,7 +85,7 @@ class ScreenRecordPermissionDialogDelegate( onStartRecordingClicked, mediaProjectionMetricsLogger, systemUIDialogFactory, - defaultSelectedMode = SINGLE_APP, + defaultSelectedMode = ENTIRE_SCREEN, theme = SystemUIDialog.DEFAULT_THEME, context, displayManager, From 007c42a0ac5b2759e234783100a708c01f6e8e03 Mon Sep 17 00:00:00 2001 From: junklu Date: Wed, 7 May 2025 15:29:11 +0800 Subject: [PATCH 0600/1315] SystemUI: Reset when sleeping state change Reset to show/hide bouncer when sleeping state change. As dozing signal may be skipped on power button double press. Fixes: 413156280 Test: manual - hit power button twice when on SIM unlock Google: 3618014 Change-Id: I86ef82d5eea537fbd999da6b8759ea73de313806 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/statusbar/phone/StatusBarKeyguardViewManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 7029561b5cf0..c65fb730906f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -1151,6 +1151,7 @@ private void setRootViewAnimationDisabled(boolean disabled) { public void onStartedWakingUp() { mIsSleeping = false; setRootViewAnimationDisabled(false); + reset(false); NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView(); if (navBarView != null) { navBarView.forEachView(view -> @@ -1170,6 +1171,7 @@ public void onStartedWakingUp() { public void onStartedGoingToSleep() { mIsSleeping = true; setRootViewAnimationDisabled(true); + reset(true); NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView(); if (navBarView != null) { navBarView.forEachView(view -> From 3ff159035a205aa0d794a4520372a1e1a22105d9 Mon Sep 17 00:00:00 2001 From: Hecheng Yu Date: Sat, 2 Aug 2025 16:57:41 +0800 Subject: [PATCH 0601/1315] InputEventReceiver: Catch error in finishInputEvent() Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/view/InputEventReceiver.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 746139ce695e..4d14f841c47f 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -227,7 +227,13 @@ public final void finishInputEvent(InputEvent event, boolean handled) { } else { int seq = mSeqMap.valueAt(index); mSeqMap.removeAt(index); - nativeFinishInputEvent(mReceiverPtr, seq, handled); + try { + nativeFinishInputEvent(mReceiverPtr, seq, handled); + } catch (RuntimeException e) { + // Just log the exception instead of crashing + Log.w(TAG, "Exception in nativeFinishInputEvent: " + e.getMessage()); + return; + } } } event.recycleIfNeededAfterDispatch(); From bfd3d38ee5ab1053551d71d13bdeceb0a986a315 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 10 Aug 2025 18:20:49 +0530 Subject: [PATCH 0602/1315] services: Suppress double upgrade notification for cloned profile Ref: https://github.com/crdroidandroid/android_frameworks_base/commit/0329145d1d9e24e6a39d26a411eb0c7cec479a82 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/am/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 811fa9ec7a7f..35a1daf1b7c0 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -998,7 +998,7 @@ private void finishUserUnlocked(final UserState uss) { // Suppress double notifications for managed profiles that // were unlocked automatically as part of their parent user being // unlocked. TODO(b/217442918): this code doesn't work correctly. - final boolean quiet = info.isManagedProfile(); + final boolean quiet = info.isManagedProfile() || info.isCloneProfile(); mInjector.sendPreBootBroadcast(userId, quiet, () -> finishUserUnlockedCompleted(uss)); } else { From 7ddfeef969870c49b90bf3130f7e1c79d3e54caa Mon Sep 17 00:00:00 2001 From: Yoel Gluschnaider Date: Thu, 14 Sep 2023 17:12:11 +0100 Subject: [PATCH 0603/1315] ignore virtual and overlay displays when turning displays off Test: 1. Create a virtual display from your launcher app. 2. Turn display off by pressing the power button. 3. Run: `adb shell dumpsys power | grep -i mHalAutoSuspendModeEnabled` 4. You should see mHalAutoSuspendModeEnabled=true Change-Id: I773ba8db10973bc3ea3442f5922ba5672a74fbad Signed-off-by: mukesh22584 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/display/DisplayManagerService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index fc30a6a7374f..c897c8a3c4c4 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -423,7 +423,16 @@ public synchronized void requestDisplayState(int displayId, int state, float bri for (int i = 0; i < size; i++) { final int displayState = i == index ? state : mDisplayStates.valueAt(i); if (displayState != Display.STATE_OFF) { - allOff = false; + final LogicalDisplay display = + mLogicalDisplayMapper + .getDisplayLocked(mDisplayStates.keyAt(i)); + if (display.getDisplayInfoLocked() != null) { + int displayType = display.getDisplayInfoLocked().type; + if (displayType != Display.TYPE_VIRTUAL + && displayType != Display.TYPE_OVERLAY) { + allOff = false; + } + } } if (Display.isActiveState(displayState)) { allInactive = false; From 2414b59b0498dc0fc70e4e54aaab4f8a7621c479 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Wed, 14 Apr 2021 23:14:16 +0530 Subject: [PATCH 0604/1315] display: Don't spam log when display state changes * dynamic refresh rate devices keep switching display configuration for changing refresh rate and ends up spamming log way too much [ghostrider-reborn] - DisplayManagerService -> DisplayDeviceRepository in android 12 Signed-off-by: Adithya R Change-Id: I57c19a0042083a86e86f2c210288904f6dc87e2c Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/display/DisplayDeviceRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java index 5f7bc4effa1b..40f66e35f459 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java +++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java @@ -202,6 +202,14 @@ private void handleDisplayDeviceChanged(DisplayDevice device) { } else if (diff != DisplayDeviceInfo.DIFF_HDR_SDR_RATIO) { Slog.i(TAG, "Display device changed: " + info); } + if (DEBUG) { + if (diff == DisplayDeviceInfo.DIFF_STATE) { + Slog.i(TAG, "Display device changed state: \"" + info.name + + "\", " + Display.stateToString(info.state)); + } else if (diff != DisplayDeviceInfo.DIFF_HDR_SDR_RATIO) { + Slog.i(TAG, "Display device changed: " + info); + } + } if ((diff & DisplayDeviceInfo.DIFF_COLOR_MODE) != 0) { try { From ac9d6837c2b66a21d80c4b51a0aa0ddc29624695 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 29 May 2025 23:05:34 +0530 Subject: [PATCH 0605/1315] SystemUI: InternetDialog: Fallback to first available subid If DDS is currently not set or invalid, fallback to the first active sub ID if available. This is inline with mobile data QS tile behaviour and can avoid some scenarios where mobile data toggle disappears from the internet dialog. Change-Id: If402feebe2ce01d2f6296acc4bd69594287c0c8e Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../dialog/InternetDetailsContentController.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java index 6f2d64dd67c0..6f0c3a559a03 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java @@ -68,6 +68,7 @@ import androidx.annotation.WorkerThread; import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.ArrayUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.DeviceInfoUtils; @@ -433,7 +434,16 @@ void setAirplaneModeDisabled() { } protected int getDefaultDataSubscriptionId() { - return mSubscriptionManager.getDefaultDataSubscriptionId(); + int dds = mSubscriptionManager.getDefaultDataSubscriptionId(); + if (dds == SubscriptionManager.INVALID_SUBSCRIPTION_ID + || !mSubscriptionManager.isActiveSubscriptionId(dds)) { + Log.d(TAG, "DDS " + dds + "is invalid or inactive, fallback to first active subId"); + final int[] activeSubIds = mSubscriptionManager.getActiveSubscriptionIdList(); + if (!ArrayUtils.isEmpty(activeSubIds)) { + dds = activeSubIds[0]; + } + } + return dds; } @VisibleForTesting @@ -1061,7 +1071,7 @@ private void refreshHasActiveSubIdOnDds() { int dds = getDefaultDataSubscriptionId(); if (dds == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { mHasActiveSubIdOnDds = false; - Log.d(TAG, "DDS is INVALID_SUBSCRIPTION_ID"); + Log.d(TAG, "DDS is still INVALID_SUBSCRIPTION_ID"); return; } SubscriptionInfo ddsSubInfo = mSubscriptionManager.getActiveSubscriptionInfo(dds); From bda57c097e86358f903e4cf503fa78ddce71442d Mon Sep 17 00:00:00 2001 From: Adithya R Date: Fri, 25 Jul 2025 00:53:17 +0530 Subject: [PATCH 0606/1315] SystemUI: Alter constraints for privacy chip on large screen header too Large screen header is used on landscape mode and without this in landscape the status icons don't reappear even after the privacy chip goes away. Change-Id: I206a5d829584f872fbef1739244912d9b5b37813 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../shade/CombinedShadeHeadersConstraintManagerImpl.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt index b2e473685daa..eabdc472a9b8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt @@ -32,6 +32,9 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain return ConstraintsChanges( qqsConstraintsChanges = { setAlpha(R.id.shade_header_system_icons, constraintAlpha) + }, + largeScreenConstraintsChanges = { + setAlpha(R.id.shade_header_system_icons, constraintAlpha) } ) } From 4e29416f7e9f7a810c18e625dadab8b93b7b26cf Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 26 Nov 2023 01:19:14 +0530 Subject: [PATCH 0607/1315] AppOpsService: Do not error out user app that was system app earlier Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/appop/AppOpsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 93349d708daf..1aa02681f1f6 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -5091,7 +5091,7 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState Binder.restoreCallingIdentity(ident); } - if (pkgUid != uid) { + if (pkgUid != Process.INVALID_UID && pkgUid != uid) { if (!suppressErrorLogs) { Slog.e(TAG, "Bad call made by uid " + callingUid + ". " + "Package \"" + packageName + "\" does not belong to uid " + uid + "."); From 51a8779116aa84f28b0d64c79d903f8eb8054f90 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Wed, 16 Oct 2024 14:59:00 +0800 Subject: [PATCH 0608/1315] AutoAODService: Add null checks to mSharedPreferences crash: 10-16 14:49:40.890 1663 1663 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: main 10-16 14:49:40.890 1663 1663 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'android.content.SharedPreferences$Editor android.content.SharedPreferences.edit()' on a null object reference 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at com.android.server.display.AutoAODService$SettingsObserver.onChange(AutoAODService.java:220) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.onChange(ContentObserver.java:184) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.onChange(ContentObserver.java:202) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.onChange(ContentObserver.java:224) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.onChange(ContentObserver.java:236) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.lambda$dispatchChange$1(ContentObserver.java:332) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver.$r8$lambda$_30FqRqKC3pUku8T3BsVPQAFotM(Unknown Source:0) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.database.ContentObserver$$ExternalSyntheticLambda1.run(D8$$SyntheticClass:0) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:959) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:100) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at com.android.server.SystemServer.run(SystemServer.java:981) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at com.android.server.SystemServer.main(SystemServer.java:665) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580) 10-16 14:49:40.890 1663 1663 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:864) Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/display/AutoAODService.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/display/AutoAODService.java b/services/core/java/com/android/server/display/AutoAODService.java index 89d8557dd195..f30ddc1b7d05 100644 --- a/services/core/java/com/android/server/display/AutoAODService.java +++ b/services/core/java/com/android/server/display/AutoAODService.java @@ -212,12 +212,16 @@ public void onChange(boolean selfChange, Uri uri) { // we have a future alarm set and user left current state // save the next alarm Slog.v(TAG, "user abandoned state. active: " + mActive); - mSharedPreferences.edit() - .putLong(PREF_TIME_KEY, mLastSetTime).apply(); + if (mSharedPreferences != null) { + mSharedPreferences.edit() + .putLong(PREF_TIME_KEY, mLastSetTime).apply(); + } return; } Slog.v(TAG, "removing PREF_TIME_KEY. active: " + mActive); - mSharedPreferences.edit().remove(PREF_TIME_KEY).apply(); + if (mSharedPreferences != null) { + mSharedPreferences.edit().remove(PREF_TIME_KEY).apply(); + } return; } mHandler.post(() -> initState()); From b7fadf98d8c17bcf40328d596e497adcb834ba1c Mon Sep 17 00:00:00 2001 From: Adithya R Date: Sat, 23 Nov 2024 16:49:54 +0800 Subject: [PATCH 0609/1315] core: Broadcast intent when display power state changes Other (system) apps can use this to perform specific tasks, such as for DOZE state. Change-Id: I2c62767a60db58e3999de65fbf506e5cc137e28e Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/content/Intent.java | 7 +++++++ core/res/AndroidManifest.xml | 1 + .../com/android/server/display/DisplayManagerService.java | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 9b50fafe007a..d9b5962e62db 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -4549,6 +4549,13 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend */ public static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; + /** + * Broadcast Action: Display power state has changed. + * @hide + */ + public static final String ACTION_DISPLAY_STATE_CHANGED = + "android.intent.action.DISPLAY_STATE_CHANGED"; + /** * Activity Action: Allow the user to select and return one or more existing * documents. When invoked, the system will display the various diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index bea6387ea053..542c7aec25e3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -831,6 +831,7 @@ + diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index c897c8a3c4c4..17bb23332ea8 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -459,6 +459,13 @@ public synchronized void requestDisplayState(int displayId, int state, float bri if (state != Display.STATE_OFF) { requestDisplayStateInternal(displayId, state, brightness, sdrBrightness); } + + if (stateChanged) { + Intent intent = new Intent(Intent.ACTION_DISPLAY_STATE_CHANGED) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } } }; From c0c1556eb037f70444e25a7f2dc277cb6801fe97 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 16 Dec 2022 13:35:42 +0800 Subject: [PATCH 0610/1315] BootReceiver: Return early if trace_pipe doesn't exists * instead of trigerring errnoexception, check if the file or directory exists first , otherwise perform a early return so we can skip the following exception below 12-16 13:05:46.592 1438 1438 E BootReceiver: Could not open /sys/kernel/tracing/instances/bootreceiver/trace_pipe 12-16 13:05:46.592 1438 1438 E BootReceiver: android.system.ErrnoException: open failed: ENOENT (No such file or directory) 12-16 13:05:46.592 1438 1438 E BootReceiver: at libcore.io.Linux.open(Native Method) 12-16 13:05:46.592 1438 1438 E BootReceiver: at libcore.io.ForwardingOs.open(ForwardingOs.java:563) 12-16 13:05:46.592 1438 1438 E BootReceiver: at libcore.io.BlockGuardOs.open(BlockGuardOs.java:274) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.system.Os.open(Os.java:494) 12-16 13:05:46.592 1438 1438 E BootReceiver: at com.android.server.BootReceiver.onReceive(BootReceiver.java:162) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.app.ActivityThread.handleReceiver(ActivityThread.java:4336) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2154) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.os.Handler.dispatchMessage(Handler.java:106) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.os.Looper.loopOnce(Looper.java:201) 12-16 13:05:46.592 1438 1438 E BootReceiver: at android.os.Looper.loop(Looper.java:288) 12-16 13:05:46.592 1438 1438 E BootReceiver: at com.android.server.SystemServer.run(SystemServer.java:982) 12-16 13:05:46.592 1438 1438 E BootReceiver: at com.android.server.SystemServer.main(SystemServer.java:667) 12-16 13:05:46.592 1438 1438 E BootReceiver: at java.lang.reflect.Method.invoke(Native Method) 12-16 13:05:46.592 1438 1438 E BootReceiver: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:553) 12-16 13:05:46.592 1438 1438 E BootReceiver: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924) Signed-off-by: minaripenguin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/BootReceiver.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 4c329eb99541..cc3119520b4b 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -150,6 +150,11 @@ public class BootReceiver extends BroadcastReceiver { private static final int MAX_ERROR_REPORTS = 8; private static int sSentReports = 0; + public boolean fileExists(String fileName) { + final File file = new File(fileName); + return file.exists(); + } + // Max tombstone file size to add to dropbox. private static final long MAX_TOMBSTONE_SIZE_BYTES = DropBoxManagerService.DEFAULT_QUOTA_KB * 1024; @@ -180,6 +185,7 @@ public void run() { FileDescriptor tracefd = null; try { + if (!fileExists(ERROR_REPORT_TRACE_PIPE)) return; tracefd = Os.open(ERROR_REPORT_TRACE_PIPE, O_RDONLY, 0600); } catch (ErrnoException e) { Slog.wtf(TAG, "Could not open " + ERROR_REPORT_TRACE_PIPE, e); From 93bcbc3e2a6448b34c1a65e3d69277781daf64ec Mon Sep 17 00:00:00 2001 From: Pulkit077 Date: Fri, 16 Sep 2022 14:46:37 +0530 Subject: [PATCH 0611/1315] base: Follow Dark/Light theme for Safe Mode dialog Change-Id: Ia9864a45551e969abaccd351e8b6d65e21d99165 Signed-off-by: Pulkit077 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/power/ShutdownThread.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index ec9e276c3190..025a431ee9ce 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManagerInternal; +import android.content.res.Configuration; import android.os.Bundle; import android.os.FileUtils; import android.os.Handler; @@ -196,6 +197,11 @@ private static void shutdownInner(final Context context, boolean confirm) { ? com.android.internal.R.string.shutdown_confirm_question : com.android.internal.R.string.shutdown_confirm); + boolean isNightMode = (context.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + int themeResId = isNightMode ? android.R.style.Theme_DeviceDefault_Dialog_Alert : + android.R.style.Theme_DeviceDefault_Light_Dialog_Alert; + if (DEBUG) { Log.d(TAG, "Notifying thread to start shutdown longPressBehavior=" + longPressBehavior); } @@ -205,7 +211,7 @@ private static void shutdownInner(final Context context, boolean confirm) { if (sConfirmDialog != null) { sConfirmDialog.dismiss(); } - sConfirmDialog = new AlertDialog.Builder(context) + sConfirmDialog = new AlertDialog.Builder(context, themeResId) .setTitle(mRebootSafeMode ? com.android.internal.R.string.reboot_safemode_title : com.android.internal.R.string.power_off) From cb9b8f81b75fc3d29ae1e9d8218b1fbf2caeb30e Mon Sep 17 00:00:00 2001 From: katao Date: Tue, 12 Mar 2019 18:57:20 +0800 Subject: [PATCH 0612/1315] Fixes crash (race cond) when destroyActivity. some UI stress (or sleep on UI thread) causes a crash from DESTROY_TIMEOUT_MSG. The steps that crash the app are: 1. build two activities ,such as A and B; 2. A start B; 3. B(oncreate) start A use FLAG_ACTIVITY_CLEAR_TOP; 4. B(oncreate) stall UI thread (Thread.sleep, heavy task) 5. crash tring to handleLaunchActivity or handleResumeActivity Bug: 128469605 Test: manual - write an app to jump between two activities, then Thread.sleep(); Change-Id: I4027dfc0cf3878c12964cabee54ad82bc108d79c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 811ff1a97369..2b39353eae09 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4725,7 +4725,7 @@ private void reportSizeConfigurations(ActivityClientRecord r) { return; } Configuration[] configurations = r.activity.getResources().getSizeConfigurations(); - if (configurations == null) { + if (configurations == null || r.activity.mFinished) { return; } r.mSizeConfigurations = new SizeConfigurationBuckets(configurations); From 55d5bc0232ca95bd9c0c631baaccbe239f84bf20 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:51:35 +0800 Subject: [PATCH 0613/1315] ComputerEngine: Fix signature spoofing for microG revanced Change-Id: Ic19fe142be51fad4bbf7e29168c6e33c9ab1809b Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/pm/ComputerEngine.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 9332a9c35abb..59a9e5004b44 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -1493,13 +1493,18 @@ public static boolean isMicrogSigned(AndroidPackage p) { // Allowlist the following apps: // * com.android.vending - microG Companion // * com.google.android.gms - microG Services - if (!p.getPackageName().equals("com.android.vending") && - !p.getPackageName().equals("com.google.android.gms")) { + // * revanced - revanced microG Services + Set allowlistedPackages = Set.of( + "com.android.vending", // microG Companion + "com.google.android.gms" // microG Services + ); + + if (!allowlistedPackages.contains(p.getPackageName()) && + !p.getPackageName().toLowerCase().contains("revanced")) { return false; } - return Signature.areExactMatch( - p.getSigningDetails(), new Signature[]{MICROG_REAL_SIGNATURE}); + return true; } private static Optional generateFakeSignature(AndroidPackage p) { From 3957096ba2bd78e66aa70fa0b7e9d975a95e397b Mon Sep 17 00:00:00 2001 From: Adithya R Date: Thu, 4 Mar 2021 18:12:20 +0530 Subject: [PATCH 0614/1315] SystemUI: Redraw display cutout on overlay changes Fixes notch hide overlay on some devices commit 5481d59996b34cda1cb6b680af7510fee7b53b42 Author: daniml3 Date: Tue Mar 9 08:11:13 2021 +0100 SystemUI: check if the cutout views array is null before using it Signed-off-by: daniml3 Change-Id: I1316c61280dadc30a86f2ae72559437a61dd4616 Co-authored-by: daniml3 Change-Id: I5a049099ab375833f1e5ebbda49dc36c3c0b0a68 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/ScreenDecorations.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index e1aa0fc51875..93b2de61d8af 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -1122,6 +1122,12 @@ public void onConfigChanged(Configuration newConfig) { mLogger.logRotationChanged(oldRotation, mRotation); } setupDecorations(); + for (int id: DISPLAY_CUTOUT_IDS) { + final View view = getOverlayView(id); + if (view instanceof DisplayCutoutView) { + ((DisplayCutoutView) view).updateCutout(); + } + } if (mOverlays != null) { // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(), // which ensures that the forced seamless rotation will end, even if we updated From 75febe83280257bb1d856f765428af29768505a6 Mon Sep 17 00:00:00 2001 From: huyuxin Date: Sat, 7 Oct 2023 15:53:37 +0800 Subject: [PATCH 0615/1315] CUR_MAX_CACHED_PROCESSES is not greater than the maximum value allowed Bug: 303823820 Test:adb shell device_config put activity_manager max_cached_processes 2147483647;adb reboot Change-Id: I3f18b56386cd9cf597a574d61d28be4049bbf646 Signed-off-by: huyuxin Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/am/ActivityManagerConstants.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index e639fd49a136..f1da8b80f098 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -1479,14 +1479,12 @@ public void onPropertiesChanged(Properties properties) { .map(ComponentName::unflattenFromString).collect(Collectors.toSet())); mCustomizedMaxCachedProcesses = context.getResources().getInteger( com.android.internal.R.integer.config_customizedMaxCachedProcesses); - CUR_MAX_CACHED_PROCESSES = mCustomizedMaxCachedProcesses; + CUR_MAX_CACHED_PROCESSES = Integer.min(mCustomizedMaxCachedProcesses, MAX_CACHED_PROCESSES); CUR_MAX_EMPTY_PROCESSES = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES); - final int rawMaxEmptyProcesses = computeEmptyProcessLimit( - Integer.min(CUR_MAX_CACHED_PROCESSES, MAX_CACHED_PROCESSES)); + final int rawMaxEmptyProcesses = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES); CUR_TRIM_EMPTY_PROCESSES = rawMaxEmptyProcesses / 2; - CUR_TRIM_CACHED_PROCESSES = (Integer.min(CUR_MAX_CACHED_PROCESSES, MAX_CACHED_PROCESSES) - - rawMaxEmptyProcesses) / 3; + CUR_TRIM_CACHED_PROCESSES = (CUR_MAX_CACHED_PROCESSES - rawMaxEmptyProcesses) / 3; loadNativeBootDeviceConfigConstants(); mDefaultDisableAppProfilerPssProfiling = context.getResources().getBoolean( R.bool.config_am_disablePssProfiling); @@ -2073,13 +2071,13 @@ private void updateMaxCachedProcesses() { "Unable to parse flag for max_cached_processes: " + maxCachedProcessesFlag, e); CUR_MAX_CACHED_PROCESSES = mCustomizedMaxCachedProcesses; } + CUR_MAX_CACHED_PROCESSES = Integer.min(CUR_MAX_CACHED_PROCESSES, MAX_CACHED_PROCESSES); + CUR_MAX_EMPTY_PROCESSES = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES); - final int rawMaxEmptyProcesses = computeEmptyProcessLimit( - Integer.min(CUR_MAX_CACHED_PROCESSES, MAX_CACHED_PROCESSES)); + final int rawMaxEmptyProcesses = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES); CUR_TRIM_EMPTY_PROCESSES = rawMaxEmptyProcesses / 2; - CUR_TRIM_CACHED_PROCESSES = (Integer.min(CUR_MAX_CACHED_PROCESSES, MAX_CACHED_PROCESSES) - - rawMaxEmptyProcesses) / 3; + CUR_TRIM_CACHED_PROCESSES = (CUR_MAX_CACHED_PROCESSES - rawMaxEmptyProcesses) / 3; } private void updateProactiveKillsEnabled() { From 00e2209079829e4538b94f761630c6f5a6cfc07e Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 6 Sep 2024 13:16:04 +0530 Subject: [PATCH 0616/1315] services: Disallow max cached processes above 128 * No QCOM devices has value above 128. Let's prevent devices misusing device config or overlay to modify no. of cached processes. * Shorter time to kill empty app processes. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/am/ActivityManagerConstants.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index f1da8b80f098..ccc5ce282c3f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -185,7 +185,7 @@ final class ActivityManagerConstants extends ContentObserver { */ static final String KEY_FREEZER_CUTOFF_ADJ = "freezer_cutoff_adj"; - private static final int DEFAULT_MAX_CACHED_PROCESSES = 1024; + private static final int DEFAULT_MAX_CACHED_PROCESSES = 128; private static final boolean DEFAULT_PRIORITIZE_ALARM_BROADCASTS = true; private static final long DEFAULT_FGSERVICE_MIN_SHOWN_TIME = 2*1000; private static final long DEFAULT_FGSERVICE_MIN_REPORT_TIME = 3*1000; @@ -947,7 +947,7 @@ final class ActivityManagerConstants extends ContentObserver { private static final String KEY_MAX_EMPTY_TIME_MILLIS = "max_empty_time_millis"; - private static final long DEFAULT_MAX_EMPTY_TIME_MILLIS = 1000L * 60L * 60L * 1000L; + private static final long DEFAULT_MAX_EMPTY_TIME_MILLIS = 30L * 60L * 1000L; volatile long mMaxEmptyTimeMillis = DEFAULT_MAX_EMPTY_TIME_MILLIS; From 19cb63db9d7f8f4686605967c483c8c3fd152b9c Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 21 Nov 2023 21:38:58 +0530 Subject: [PATCH 0617/1315] CachedAppOptimizer: Set thread group to background * Pulled from CAF Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/am/ActivityManagerService.java | 3 ++- .../java/com/android/server/am/CachedAppOptimizer.java | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 41502a4d4ebe..3a95c1b0f18e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2568,7 +2568,7 @@ public ActivityManagerService(Context systemContext, ActivityTaskManagerService Process.THREAD_GROUP_SYSTEM); Process.setThreadGroupAndCpuset( mCachedAppOptimizer.mCachedAppOptimizerThread.getThreadId(), - Process.THREAD_GROUP_SYSTEM); + Process.THREAD_GROUP_BACKGROUND); } catch (Exception e) { Slog.w(TAG, "Setting background thread cpuset failed"); } @@ -5372,6 +5372,7 @@ public void performReceive(Intent intent, int resultCode, // Defer the full Pss collection as the system is really busy now. mHandler.postDelayed(() -> { synchronized (mProcLock) { + mCachedAppOptimizer.compactAllSystem(); mAppProfiler.requestPssAllProcsLPr( SystemClock.uptimeMillis(), true, false); } diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 0ec502f9cba1..bc60bdccdd55 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -556,7 +556,7 @@ public CachedAppOptimizer(ActivityManagerService am) { mAm = am; mProcLock = am.mProcLock; mCachedAppOptimizerThread = new ServiceThread("CachedAppOptimizerThread", - Process.THREAD_GROUP_SYSTEM, true); + Process.THREAD_GROUP_BACKGROUND, true); mProcStateThrottle = new HashSet<>(); mProcessDependencies = processDependencies; mTestCallback = callback; @@ -797,10 +797,9 @@ private void updateUseCompaction() { mCompactionHandler = new MemCompactionHandler(); mCompactStatsManager = CompactionStatsManager.getInstance(); - - Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(), - Process.THREAD_GROUP_SYSTEM); } + Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(), + Process.THREAD_GROUP_BACKGROUND); } /** @@ -900,7 +899,7 @@ private void updateUseFreezer() { } Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(), - Process.THREAD_GROUP_SYSTEM); + Process.THREAD_GROUP_BACKGROUND); } else { Slog.d(TAG_AM, "Freezer disabled"); enableFreezer(false); From 8bddc63f0d80c6d6bf14ed2eaa6889f64d9ca7dc Mon Sep 17 00:00:00 2001 From: Zhuo Fu Date: Mon, 3 Apr 2023 14:39:26 +0800 Subject: [PATCH 0618/1315] CachedAppOptimizer: Fix persistent compact skipped Persistent app compaction broken, fix by only do oom adj throttle check for APP compact type CRs-Fixed: 3453069 Change-Id: I2dc0ea5aca0b36fd09e71942fdd0f3eafdac2eec Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/am/CachedAppOptimizer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index bc60bdccdd55..8a86b6673959 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1558,10 +1558,13 @@ private MemCompactionHandler() { private boolean shouldOomAdjThrottleCompaction(ProcessRecord proc) { final String name = proc.processName; + final ProcessCachedOptimizerRecord opt = proc.mOptRecord; + CompactSource compactSource = opt.getReqCompactSource(); // don't compact if the process has returned to perceptible // and this is only a cached/home/prev compaction - if (proc.getSetAdj() <= ProcessList.PERCEPTIBLE_APP_ADJ) { + if (compactSource == CompactSource.APP + && proc.getSetAdj() <= ProcessList.PERCEPTIBLE_APP_ADJ) { if (DEBUG_COMPACTION) { Slog.d(TAG_AM, "Skipping compaction as process " + name + " is " From 0ca909d1f107e2648d6c3c4b35f184be492b0555 Mon Sep 17 00:00:00 2001 From: ayushigupta Date: Tue, 2 Jul 2024 14:56:58 +0530 Subject: [PATCH 0619/1315] CachedAppOptimizer: Initialize compactProfile and compactTime CRs-Fixed: 3858822 Change-Id: I6f06176d8e5e40a5fd2afb53b9417005f2e26b2a Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/am/CachedAppOptimizer.java | 2 +- .../com/android/server/am/ProcessCachedOptimizerRecord.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 8a86b6673959..75ca8260df9e 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -278,7 +278,7 @@ public class CachedAppOptimizer { @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_1 = 5_000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_2 = 10_000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_3 = 500; - @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 10_000; + @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 5*60*1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_5 = 10 * 60 * 1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_6 = 10 * 60 * 1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_MIN_OOM_ADJ = diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index 2152551418d5..7b2e73108331 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -214,7 +214,7 @@ int getLastOomAdjChangeReason() { CachedAppOptimizer.CompactProfile getLastCompactProfile() { if (mLastCompactProfile == null) { // The first compaction won't have a previous one, so assign one to avoid crashing. - mLastCompactProfile = CachedAppOptimizer.CompactProfile.SOME; + mLastCompactProfile = CachedAppOptimizer.CompactProfile.FULL; } return mLastCompactProfile; @@ -403,6 +403,7 @@ void dispatchUnfrozenEvent() { void init(long nowUptime) { mFreezeUnfreezeTime = nowUptime; + mLastCompactTime = nowUptime; } @GuardedBy("mProcLock") From 4962bbad4ee4922deb037aef8a7532e4100cfa2f Mon Sep 17 00:00:00 2001 From: Divyanand Rangu Date: Wed, 28 Dec 2022 14:53:58 +0530 Subject: [PATCH 0620/1315] CachedAppOptimizer: Pageout File pages during system compaction During system compaction, mark File pages as MADVISE_PAGEOUT instead of MADVISE_COLD. This will drop clean pages and swap-out file pages mapped as private. This improves the bootup free memory. CRs-Fixed: 3365547 Change-Id: I1311126d8fd551c48599e160360122c05cc8ea02 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../jni/com_android_server_am_CachedAppOptimizer.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp index 98ee84a0a3dc..bea833d2321a 100644 --- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp +++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp @@ -85,6 +85,8 @@ namespace android { // before starting next VMA batch static std::atomic cancelRunningCompaction; +static bool inSystemCompaction = false; + // A VmaBatch represents a set of VMAs that can be processed // as VMAs are processed by client code it is expected that the // VMAs get consumed which means they are discarded as they are @@ -328,12 +330,16 @@ static int getAnonPageAdvice(const Vma& vma) { bool hasReadFlag = (vma.flags & PROT_READ) > 0; bool hasWriteFlag = (vma.flags & PROT_WRITE) > 0; bool hasExecuteFlag = (vma.flags & PROT_EXEC) > 0; - if ((hasReadFlag || hasWriteFlag) && !hasExecuteFlag && !vma.is_shared) { + if ((hasReadFlag || hasWriteFlag) && !hasExecuteFlag) { return MADV_PAGEOUT; } return -1; } static int getAnyPageAdvice(const Vma& vma) { + if (inSystemCompaction == true) { + return MADV_PAGEOUT; + } + if (vma.inode == 0 && !vma.is_shared) { return MADV_PAGEOUT; } @@ -461,6 +467,7 @@ static void compactMemcg(int uid, int pid, int compactionFlags) { static void com_android_server_am_CachedAppOptimizer_compactSystem(JNIEnv *, jobject) { std::unique_ptr proc(opendir("/proc"), closedir); struct dirent* current; + inSystemCompaction = true; while ((current = readdir(proc.get()))) { if (current->d_type != DT_DIR) { continue; @@ -489,6 +496,7 @@ static void com_android_server_am_CachedAppOptimizer_compactSystem(JNIEnv *, job compactMemcg(status_info.st_uid, pid, COMPACT_ACTION_ANON_FLAG | COMPACT_ACTION_FILE_FLAG); } + inSystemCompaction = false; } static void com_android_server_am_CachedAppOptimizer_cancelCompaction(JNIEnv*, jobject) { From 6f856b40b60d060594da88a6e5b5b9dc392e51f2 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:55:38 +0800 Subject: [PATCH 0621/1315] services: optimize memory on device wake [neobuddy89: Drop experimental stuff.] Change-Id: I67d917630fbbba8c72bb94d0cd96ee2de1d71165 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/IActivityManager.aidl | 2 + .../server/am/ActivityManagerService.java | 57 +++++++++++++++++++ .../server/policy/PhoneWindowManager.java | 26 +++++++++ 3 files changed, 85 insertions(+) diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 8cfdc8ad041d..8c4e3acbc6b2 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1068,4 +1068,6 @@ interface IActivityManager { * Force full screen for devices with cutout */ boolean shouldForceCutoutFullscreen(in String packageName); + + void releaseMemory(int minAdj, int maxKillCount, boolean includeUIProcesses, boolean skipCamera); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 3a95c1b0f18e..0c7013dbb71d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19969,4 +19969,61 @@ public void setThreeGestureStateActive(boolean active) { public boolean shouldForceCutoutFullscreen(String packageName) { return mActivityTaskManager.shouldForceCutoutFullscreen(packageName); } + + @Override + public void releaseMemory(int minAdj, int maxKillCount, boolean includeUIProcesses, boolean skipCamera) { + if (minAdj == 0) return; + + try { + ArrayList processList = + (ArrayList) mProcessList.getLruProcessesLOSP().clone(); + + ArrayList toKill = new ArrayList<>(); + + for (ProcessRecord record : processList) { + if (record != null && record.getSetAdj() >= minAdj) { + boolean hasUI = record.hasActivities(); + if (!hasUI || includeUIProcesses) { + toKill.add(new ProcessToKill( + record.getPid(), + record.getSetAdj(), + record.processName + )); + } + } + } + + Collections.sort(toKill, new ProcessComparator()); + + int killedCount = 0; + for (ProcessToKill info : toKill) { + Process.killProcess(info.pid); + killedCount++; + + if (killedCount >= maxKillCount) { + return; + } + } + + } catch (Exception e) {} + } + + public class ProcessComparator implements Comparator { + @Override + public int compare(ProcessToKill p1, ProcessToKill p2) { + return Integer.compare(p2.adj, p1.adj); + } + } + + public static final class ProcessToKill { + public int adj; + public String name; + public int pid; + + public ProcessToKill(int pid, int adj, String name) { + this.pid = pid; + this.adj = adj; + this.name = name; + } + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 894e747789c6..c8f3d3945f00 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -430,6 +430,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final String ACTION_TORCH_OFF = "com.android.server.policy.PhoneWindowManager.ACTION_TORCH_OFF"; + private static final long MEMORY_RELEASE_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes + private long lastMemoryReleaseTime = 0L; + /** * Keyguard stuff */ @@ -6370,6 +6373,8 @@ public void startedGoingToSleep(int displayGroupId, if (mPocketManager != null) { mPocketManager.onInteractiveChanged(false); } + + mHandler.removeCallbacks(mMemoryOpt); } // Called on the PowerManager's Notifier thread. @@ -6430,6 +6435,9 @@ public void startedWakingUp(int displayGroupId, @WakeReason int pmWakeReason) { EventLogTags.writeScreenToggled(1); + mHandler.removeCallbacks(mMemoryOpt); + mHandler.postDelayed(mMemoryOpt, 1250 /* allowance time */); + mIsGoingToSleep = false; setPendingWakingUpGroup(displayGroupId); mDefaultDisplayPolicy.setAwake(true); @@ -6478,6 +6486,13 @@ public void finishedWakingUp(int displayGroupId, @WakeReason int pmWakeReason) { } } + private final Runnable mMemoryOpt = new Runnable() { + @Override + public void run() { + releaseMemoryAtScreenOn(); + } + }; + private boolean shouldWakeUpWithHomeIntent() { if (mWakeUpToLastStateTimeout <= 0) { return false; @@ -8158,4 +8173,15 @@ private void toggleRingerModes() { break; } } + + private void releaseMemoryAtScreenOn() { + long currentTime = System.currentTimeMillis(); + if (lastMemoryReleaseTime == 0L || currentTime - lastMemoryReleaseTime > MEMORY_RELEASE_INTERVAL_MS) { + try { + ActivityManager.getService().releaseMemory(900, 20, false, false); + lastMemoryReleaseTime = currentTime; + } catch (RemoteException e) { + } + } + } } From aa11b70c9f5861464ad544b3b491706d22e94238 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:09:17 +0800 Subject: [PATCH 0622/1315] services: clean system_server heap memory on screen off Change-Id: I27277ae924f3563ee56a323804ba41e64e317731 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/policy/PhoneWindowManager.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index c8f3d3945f00..d81749988143 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -433,6 +433,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final long MEMORY_RELEASE_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes private long lastMemoryReleaseTime = 0L; + private static final long GC_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes + private long lastGcTime = 0L; + /** * Keyguard stuff */ @@ -6417,6 +6420,9 @@ public void finishedGoingToSleep(int displayGroupId, } mPowerButtonLaunchGestureTriggeredDuringGoingToSleep = false; mPowerButtonLaunchGestureTriggered = false; + + // make sure we do garbage collection at screen off but delay it to avoid black wallpaper + mHandler.postDelayed(mSystemServerGcOpt, 5000); } // Called on the PowerManager's Notifier thread. @@ -6438,6 +6444,9 @@ public void startedWakingUp(int displayGroupId, @WakeReason int pmWakeReason) { mHandler.removeCallbacks(mMemoryOpt); mHandler.postDelayed(mMemoryOpt, 1250 /* allowance time */); + // remove pending system server gc for frequent screen state changes + mHandler.removeCallbacks(mSystemServerGcOpt); + mIsGoingToSleep = false; setPendingWakingUpGroup(displayGroupId); mDefaultDisplayPolicy.setAwake(true); @@ -6493,6 +6502,20 @@ public void run() { } }; + private final Runnable mSystemServerGcOpt = new Runnable() { + @Override + public void run() { + long currentTime = System.currentTimeMillis(); + if (lastGcTime == 0L || currentTime - lastGcTime > GC_INTERVAL_MS) { + System.gc(); + System.runFinalization(); + System.gc(); + lastGcTime = currentTime; + Log.v("GcOpt", "performing garbage collection for system_server"); + } + } + }; + private boolean shouldWakeUpWithHomeIntent() { if (mWakeUpToLastStateTimeout <= 0) { return false; From f11efd32ddb67221c7b8b0ef546af469b3be047b Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:16:32 +0800 Subject: [PATCH 0623/1315] SystemUI: clean heap memory on screen off Change-Id: If1ed8a9a02c190ba9fb056c093c21b8b91c85161 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/phone/CentralSurfacesImpl.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 2629d3936025..46a8a90f5c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -58,6 +58,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; @@ -449,6 +450,8 @@ public QSPanelController getQSPanelController() { private final StatusBarSignalPolicy mStatusBarSignalPolicy; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + /** Controller for the Shade. */ private final ShadeSurface mShadeSurface; private final ShadeLogger mShadeLogger; @@ -481,6 +484,9 @@ public QSPanelController getQSPanelController() { private final DisplayMetrics mDisplayMetrics; + private static final long GC_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes + private long lastGcTime = 0L; + // XXX: gesture research private final GestureRecorder mGestureRec = DEBUG_GESTURES ? new GestureRecorder("/sdcard/statusbar_gestures.dat") @@ -2625,6 +2631,8 @@ && launchWalletViaSysuiCallbacks()) { () -> mCommandQueueCallbacks.onEmergencyActionLaunchGestureDetected()); } updateIsKeyguard(); + // make sure we do garbage collection at screen off but delay it to avoid black wallpaper + mHandler.postDelayed(mSystemUiGcOpt, 5000); } @Override @@ -2673,6 +2681,7 @@ public void onStartedWakingUp() { } }); DejankUtils.stopDetectingBlockingIpcs(tag); + mHandler.removeCallbacks(mSystemUiGcOpt); } /** @@ -2741,6 +2750,20 @@ && launchWalletViaSysuiCallbacks()) { } }; + private final Runnable mSystemUiGcOpt = new Runnable() { + @Override + public void run() { + long currentTime = System.currentTimeMillis(); + if (lastGcTime == 0L || currentTime - lastGcTime > GC_INTERVAL_MS) { + Log.v("GcOpt", "performing garbage collection for SystemUI"); + System.gc(); + System.runFinalization(); + System.gc(); + lastGcTime = currentTime; + } + } + }; + /** * We need to disable touch events because these might * collapse the panel after we expanded it, and thus we would end up with a blank From c5499b53613fe65e44ed06a6c4e79047d1c72307 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 28 Sep 2025 20:50:00 +0530 Subject: [PATCH 0624/1315] ActivityManagerService: Rewrite release memory on screen wake * Make it safer with proc lock block. * Exclude important foreground states. * Compact all non-zygote processes while at it. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/am/ActivityManagerService.java | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 0c7013dbb71d..41a63bd127d7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19971,41 +19971,50 @@ public boolean shouldForceCutoutFullscreen(String packageName) { } @Override - public void releaseMemory(int minAdj, int maxKillCount, boolean includeUIProcesses, boolean skipCamera) { - if (minAdj == 0) return; + public void releaseMemory(int minAdj, int maxKillCount, + boolean includeUIProcesses, boolean skipCamera) { + if (minAdj <= 0) return; - try { - ArrayList processList = - (ArrayList) mProcessList.getLruProcessesLOSP().clone(); - - ArrayList toKill = new ArrayList<>(); - - for (ProcessRecord record : processList) { - if (record != null && record.getSetAdj() >= minAdj) { - boolean hasUI = record.hasActivities(); - if (!hasUI || includeUIProcesses) { - toKill.add(new ProcessToKill( - record.getPid(), - record.getSetAdj(), - record.processName - )); - } - } - } + final int currentUser = mUserController.getCurrentUserId(); + final ArrayList victims = new ArrayList<>(); + + synchronized (this) { + synchronized (mProcLock) { + mCachedAppOptimizer.compactAllSystem(); - Collections.sort(toKill, new ProcessComparator()); + mProcessList.forEachLruProcessesLOSP(false, proc -> { + if (proc == null || proc.getThread() == null) return; - int killedCount = 0; - for (ProcessToKill info : toKill) { - Process.killProcess(info.pid); - killedCount++; + final int setAdj = proc.getSetAdj(); + final int state = proc.getSetProcState(); - if (killedCount >= maxKillCount) { - return; - } + // Exclusions + if (proc.isPersistent()) return; + if (proc.userId != currentUser) return; + if (state <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) return; + if (state == ActivityManager.PROCESS_STATE_HOME) return; + if (!includeUIProcesses && proc.hasActivities()) return; + + if (setAdj >= minAdj) victims.add(proc); + }); } + } - } catch (Exception e) {} + victims.sort((a, b) -> Integer.compare(b.getSetAdj(), a.getSetAdj())); + + int killed = 0; + for (ProcessRecord proc : victims) { + if (killed >= maxKillCount) break; + final String reason = "screen-on memory reclaim"; + mHandler.post(() -> { + synchronized (ActivityManagerService.this) { + proc.killLocked(reason, + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_MEMORY_PRESSURE, true); + } + }); + killed++; + } } public class ProcessComparator implements Comparator { From fb5a8204fea51f0bbce60258d3637498de2a010a Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 28 Sep 2025 21:16:53 +0530 Subject: [PATCH 0625/1315] services: Reduce memory release interval guard * Reduced from 10 min to 5 min. This functionality provides better UX. * Add log. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/policy/PhoneWindowManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index d81749988143..6e2f0acd4795 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -430,7 +430,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final String ACTION_TORCH_OFF = "com.android.server.policy.PhoneWindowManager.ACTION_TORCH_OFF"; - private static final long MEMORY_RELEASE_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes + private static final long MEMORY_RELEASE_INTERVAL_MS = 5 * 60 * 1000L; // 5 minutes private long lastMemoryReleaseTime = 0L; private static final long GC_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes @@ -8201,8 +8201,9 @@ private void releaseMemoryAtScreenOn() { long currentTime = System.currentTimeMillis(); if (lastMemoryReleaseTime == 0L || currentTime - lastMemoryReleaseTime > MEMORY_RELEASE_INTERVAL_MS) { try { - ActivityManager.getService().releaseMemory(900, 20, false, false); + mActivityManagerService.releaseMemory(900, 25, false, false); lastMemoryReleaseTime = currentTime; + Slog.d(TAG, "Performing screen-on memory reclaim."); } catch (RemoteException e) { } } From 9abb9610fbabb424f51864da764f52ec490fd891 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 15 Sep 2025 22:35:38 +0530 Subject: [PATCH 0626/1315] services: Reduce grace window to kill cache after user unlock * Reduced from 10 minutes to 2 minutes. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/am/ActivityManagerConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index ccc5ce282c3f..332dbbeccd95 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -916,7 +916,7 @@ final class ActivityManagerConstants extends ContentObserver { /** @see #mNoKillCachedProcessesPostBootCompletedDurationMillis */ private static final long - DEFAULT_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS = 600_000; + DEFAULT_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS = 120_000; /** * If true, do not kill excessive cached processes proactively, until user-0 is unlocked. From 18a2f235736f182666e589d79b0309fbfd8be748 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 29 May 2025 21:55:40 +0800 Subject: [PATCH 0627/1315] ActivityManagerService: Adjust delay on boot system compaction Change-Id: I4f77325c14d1bc78ed12c965537b8a1e1c9aae6d Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/am/ActivityManagerService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 41a63bd127d7..e5a13519649c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5369,10 +5369,14 @@ public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { mBootCompletedTimestamp = SystemClock.uptimeMillis(); - // Defer the full Pss collection as the system is really busy now. mHandler.postDelayed(() -> { synchronized (mProcLock) { mCachedAppOptimizer.compactAllSystem(); + } + }, 300000); + // Defer the full Pss collection as the system is really busy now. + mHandler.postDelayed(() -> { + synchronized (mProcLock) { mAppProfiler.requestPssAllProcsLPr( SystemClock.uptimeMillis(), true, false); } From 2e323a6c5777402c1661141e2a96920acc81d154 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 29 Sep 2025 02:19:17 +0530 Subject: [PATCH 0628/1315] ActivityManagerService: Perform system compaction with GC * Do not run this on screen-on path - as it is bit CPU intensive. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/IActivityManager.aidl | 2 ++ .../com/android/server/am/ActivityManagerService.java | 11 +++++++++-- .../com/android/server/policy/PhoneWindowManager.java | 8 ++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 8c4e3acbc6b2..6f8f48b7b073 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1070,4 +1070,6 @@ interface IActivityManager { boolean shouldForceCutoutFullscreen(in String packageName); void releaseMemory(int minAdj, int maxKillCount, boolean includeUIProcesses, boolean skipCamera); + + void compactAllSystem(); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e5a13519649c..c2e9354a2d2b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19984,8 +19984,6 @@ public void releaseMemory(int minAdj, int maxKillCount, synchronized (this) { synchronized (mProcLock) { - mCachedAppOptimizer.compactAllSystem(); - mProcessList.forEachLruProcessesLOSP(false, proc -> { if (proc == null || proc.getThread() == null) return; @@ -20021,6 +20019,15 @@ public void releaseMemory(int minAdj, int maxKillCount, } } + @Override + public void compactAllSystem() { + mHandler.post(() -> { + synchronized (mProcLock) { + mCachedAppOptimizer.compactAllSystem(); + } + }); + } + public class ProcessComparator implements Comparator { @Override public int compare(ProcessToKill p1, ProcessToKill p2) { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 6e2f0acd4795..32971ef58553 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -433,7 +433,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final long MEMORY_RELEASE_INTERVAL_MS = 5 * 60 * 1000L; // 5 minutes private long lastMemoryReleaseTime = 0L; - private static final long GC_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes + private static final long GC_INTERVAL_MS = 5 * 60 * 1000L; // 5 minutes private long lastGcTime = 0L; /** @@ -6510,8 +6510,12 @@ public void run() { System.gc(); System.runFinalization(); System.gc(); + try { + mActivityManagerService.compactAllSystem(); + } catch (RemoteException e) { + } lastGcTime = currentTime; - Log.v("GcOpt", "performing garbage collection for system_server"); + Slog.d(TAG, "Performing garbage collection for system_server"); } } }; From ff2b9a63bb615c9de801bb89d4d7edab458aafe9 Mon Sep 17 00:00:00 2001 From: Tobias Merkel Date: Thu, 22 Jun 2023 18:52:41 +0200 Subject: [PATCH 0629/1315] SystemUI: Refresh system icons on theme change System icon pack changes were previously managed by reconstructing some components which used them, such as KeyguardStatusBarView. This introduced a memory leak where KeyguardStatusBarViewController never unregistered a callback, thus it was not being garbage collected. Furthermore, some usages of the system icons were not updated on a pack change. Change that by recreating the icons on theme changes. Change-Id: Ic2b774d243546a88ac2e17f8441e073629caa1c8 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/phone/ui/StatusBarIconControllerImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java index d8dbdc7a3064..ffb9e91a0a8b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java @@ -515,6 +515,11 @@ public void onDensityOrFontScaleChanged() { refreshIconGroups(); } + @Override + public void onThemeChanged() { + refreshIconGroups(); + } + private String createExternalSlotName(String slot) { if (slot.endsWith(EXTERNAL_SLOT_SUFFIX)) { return slot; From db44b31326d999a2d27e5dd2135b6e7ee161c8a1 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Thu, 21 Dec 2023 16:02:57 +0200 Subject: [PATCH 0630/1315] base: Allow disabling private DNS for VPN [1/2] Allows using the VPN's DNS instead of the set private DNS Automatically restores the previous set mode Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 19 +++++++++++ .../com/android/server/connectivity/Vpn.java | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4cdc7d0b5f25..31ac9a650f0a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14248,6 +14248,25 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String CLIPBOARD_AUTO_CLEAR_TIMEOUT = "clipboard_auto_clear_timeout"; + /** + * Whether to turn off Private DNS {@link #PRIVATE_DNS_MODE} + * when a VPN is connected + *

+ * Set to 1 for true and 0 for false. Default 0. + * + * @hide + */ + public static final String VPN_ENFORCE_DNS = "vpn_enforce_dns"; + + /** + * A setting used to store the last mode of {@link #PRIVATE_DNS_MODE} + * used for {@link #VPN_ENFORCE_DNS} + * Not for backup! + * + * @hide + */ + public static final String VPN_ENFORCE_DNS_STORE = "vpn_enforce_dns_store"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 7a2ca702a695..5320490cb473 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -19,6 +19,7 @@ import static android.Manifest.permission.BIND_VPN_SERVICE; import static android.Manifest.permission.CONTROL_VPN; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; @@ -57,6 +58,7 @@ import android.content.pm.UserInfo; import android.net.ConnectivityDiagnosticsManager; import android.net.ConnectivityManager; +import android.net.ConnectivitySettingsManager; import android.net.INetd; import android.net.INetworkManagementEventObserver; import android.net.Ikev2VpnProfile; @@ -757,12 +759,14 @@ protected void updateState(DetailedState detailedState, String reason) { mVpnConnectivityMetrics.resetMetrics(); } } + maybeRestoreDNS(); break; case CONNECTING: if (null != mNetworkAgent) { throw new IllegalStateException("VPN can only go to CONNECTING state when" + " the agent is null."); } + maybeRestrictDNS(); break; default: throw new IllegalArgumentException("Illegal state argument " + detailedState); @@ -1738,6 +1742,34 @@ private void agentDisconnect() { updateState(DetailedState.DISCONNECTED, "agentDisconnect"); } + @GuardedBy("this") + private void maybeRestrictDNS() { + BinderUtils.withCleanCallingIdentity(() -> { + final boolean isEnforceDns = mSystemServices.settingsSecureGetIntForUser( + Settings.Secure.VPN_ENFORCE_DNS, 0, mUserId) == 1; + final int mode = ConnectivitySettingsManager.getPrivateDnsMode(mUserIdContext); + if (!isEnforceDns || mode == PRIVATE_DNS_MODE_OFF) return; + // Store current private DNS mode + mSystemServices.settingsSecurePutIntForUser( + Settings.Secure.VPN_ENFORCE_DNS_STORE, mode, mUserId); + // Disable private DNS + ConnectivitySettingsManager.setPrivateDnsMode(mUserIdContext, PRIVATE_DNS_MODE_OFF); + }); + } + + @GuardedBy("this") + private void maybeRestoreDNS() { + BinderUtils.withCleanCallingIdentity(() -> { + final int mode = mSystemServices.settingsSecureGetIntForUser( + Settings.Secure.VPN_ENFORCE_DNS_STORE, -1, mUserId); + if (mode == -1) return; + // Restore previous private DNS mode + ConnectivitySettingsManager.setPrivateDnsMode(mUserIdContext, mode); + mSystemServices.settingsSecurePutIntForUser( + Settings.Secure.VPN_ENFORCE_DNS_STORE, -1, mUserId); + }); + } + @GuardedBy("this") private void startNewNetworkAgent(NetworkAgent oldNetworkAgent, String reason) { // Initialize the state for a new agent, while keeping the old one connected From 09190afc478d3b892e4d94db98b2473ba78ad6bf Mon Sep 17 00:00:00 2001 From: rituj Date: Sat, 3 Sep 2022 02:51:42 -0300 Subject: [PATCH 0631/1315] base: Add option to cycle through ringer modes [1/3] * Settings->System->Gestures->Prevent ringing core: Remove `@SystemApi` annotation for VOLUME_HUSH_CYCLE. This addition should not be annotated with `@SystemApi` because it creates an addition that fails the API test in user builds Signed-off-by: rituj Co-authored-by: Evan Anderson Change-Id: I334bd165e970665ac5300d387a0ea6745748d3ad Signed-off-by: aswin7469 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 4 +++- core/res/res/values/matrixx_strings.xml | 3 +++ core/res/res/values/matrixx_symbols.xml | 3 +++ .../android/server/audio/AudioService.java | 20 +++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 31ac9a650f0a..7dd80572f1fc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13413,7 +13413,7 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val /** * What behavior should be invoked when the volume hush gesture is triggered - * One of VOLUME_HUSH_OFF, VOLUME_HUSH_VIBRATE, VOLUME_HUSH_MUTE. + * One of VOLUME_HUSH_OFF, VOLUME_HUSH_VIBRATE, VOLUME_HUSH_MUTE, VOLUME_HUSH_CYCLE. * * @hide */ @@ -13430,6 +13430,8 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val /** @hide */ @SystemApi public static final int VOLUME_HUSH_MUTE = 2; + /** @hide */ + public static final int VOLUME_HUSH_CYCLE = 3; /** * The number of times (integer) the user has manually enabled battery saver. diff --git a/core/res/res/values/matrixx_strings.xml b/core/res/res/values/matrixx_strings.xml index a9f3bc5bc4f6..7d411455d324 100644 --- a/core/res/res/values/matrixx_strings.xml +++ b/core/res/res/values/matrixx_strings.xml @@ -34,4 +34,7 @@ 1. Ensure the blue area (proximity sensor) shown above is unobstructed and free from dirt or dust. 2. Press and hold the power button to exit pocket mode. + + Calls and notifications will ring + diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index c8266f590a64..b20e0a982b71 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -87,4 +87,7 @@ + + + diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 358f17028764..9cc71f37d0fd 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -65,6 +65,7 @@ import static android.media.audiopolicy.Flags.volumeGroupManagementUpdate; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.INVALID_UID; +import static android.provider.Settings.Secure.VOLUME_HUSH_CYCLE; import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; @@ -6657,6 +6658,25 @@ public void silenceRingerModeInternal(String reason) { ringerMode = AudioManager.RINGER_MODE_VIBRATE; toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate; break; + case VOLUME_HUSH_CYCLE: + switch (mRingerMode) { + case AudioManager.RINGER_MODE_NORMAL: + effect = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); + ringerMode = AudioManager.RINGER_MODE_VIBRATE; + toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate; + break; + case AudioManager.RINGER_MODE_VIBRATE: + effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + ringerMode = AudioManager.RINGER_MODE_SILENT; + toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_silent; + break; + case AudioManager.RINGER_MODE_SILENT: + effect = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); + ringerMode = AudioManager.RINGER_MODE_NORMAL; + toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_normal; + break; + } + break; } maybeVibrate(effect, reason); setRingerModeInternal(ringerMode, reason); From 783304913f93c9c3e9453332883a38db53810fa5 Mon Sep 17 00:00:00 2001 From: LibXZR Date: Thu, 17 Mar 2022 22:58:19 +0800 Subject: [PATCH 0632/1315] base: Add support for application downgrade [1/2] Can be useful in some cases. Change-Id: I999337cc55e9675cedde35191a6438d90744e5aa Signed-off-by: LibXZR Adapt to Android 13 Signed-off-by: someone5678 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++++++ .../java/com/android/server/pm/InstallPackageHelper.java | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7dd80572f1fc..0badc4573871 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -21283,6 +21283,12 @@ public static boolean putFloat(ContentResolver cr, String name, float value) { */ public static final String RESTRICTED_NETWORKING_MODE = "restricted_networking_mode"; + /** + * Control whether application downgrade is allowed. + * @hide + */ + public static final String PM_DOWNGRADE_ALLOWED = "pm_downgrade_allowed"; + /** * Setting indicating whether Low Power Standby is enabled, if supported. * diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 67db48aaf5e4..ec9d7dd30734 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2908,8 +2908,9 @@ Pair verifyReplacingVersionCode(PackageInfoLite pkgLite, // system apps case like below. } else if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) { if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags, - dataOwnerPkg.isDebuggable())) { - // Downgrade is not permitted; a lower version of the app will not be allowed + dataOwnerPkg.isDebuggable()) + && android.provider.Settings.Global.getInt(mContext.getContentResolver(), + android.provider.Settings.Global.PM_DOWNGRADE_ALLOWED, 0) == 0) { try { PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite); } catch (PackageManagerException e) { From 8f1f99ef4aa49f0ae7292dea0db5e98d1513e850 Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:22:42 +0900 Subject: [PATCH 0633/1315] SystemUI: Use privacy_chip_background for charger indicator bg Change-Id: Iac9f0958676c02c49252e5a0e1a88383a7294d00 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/drawable/statusbar_chip_bg.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/drawable/statusbar_chip_bg.xml b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml index d7de16d7c5bb..551a0125eb12 100644 --- a/packages/SystemUI/res/drawable/statusbar_chip_bg.xml +++ b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml @@ -18,6 +18,6 @@ - + \ No newline at end of file From 229c21787d363956f776bfe1d4e678311bf23fc8 Mon Sep 17 00:00:00 2001 From: maxwen Date: Wed, 14 Dec 2022 22:57:20 +0100 Subject: [PATCH 0634/1315] SystemUI: write initial value of SHOW_QR_CODE_SCANNER_SETTING on first call and reset only in unregisterDefaultQRCodeScannerObserver so it is in sync with isActivityCallable Change-Id: If4fc2c541d0cee90b49b5769d34adc9d7bffbb02 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../controller/QRCodeScannerController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index a65e7a295620..e1ba1a8d4dfb 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -374,8 +374,6 @@ private void unregisterQRCodePreferenceObserver() { // Reset cached values to default as we are no longer listening mQRCodeScannerPreferenceObserver = new HashMap<>(); - mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, null, - mUserTracker.getUserId()); } private void unregisterDefaultQRCodeScannerObserver() { @@ -384,6 +382,8 @@ private void unregisterDefaultQRCodeScannerObserver() { // Reset cached values to default as we are no longer listening mOnDefaultQRCodeScannerChangedListener = null; + mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, null, + mUserTracker.getUserId()); } private void notifyQRCodeScannerActivityChanged() { @@ -411,7 +411,10 @@ private void registerDefaultQRCodeScannerObserver() { // While registering the observers for the first time update the default values in the // background - mExecutor.execute(() -> updateQRCodeScannerActivityDetails()); + mExecutor.execute(() -> { + updateQRCodeScannerActivityDetails(); + updateQRCodeScannerPreferenceDetails(/* updateSettings = */true); + }); mOnDefaultQRCodeScannerChangedListener = properties -> { if (DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace()) From fcfd2bd34db788dfa35ecd3f38e3e324ea5ff275 Mon Sep 17 00:00:00 2001 From: johnmart19 Date: Tue, 30 May 2023 21:47:32 +0300 Subject: [PATCH 0635/1315] frameworks/base: Import Xiaomi Image Tags defenitions Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android_hardware_camera2_DngCreator.cpp | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/jni/android_hardware_camera2_DngCreator.cpp b/core/jni/android_hardware_camera2_DngCreator.cpp index 828f2eb76c60..f7d4e4cc2b8c 100644 --- a/core/jni/android_hardware_camera2_DngCreator.cpp +++ b/core/jni/android_hardware_camera2_DngCreator.cpp @@ -1514,6 +1514,19 @@ static sp DngCreator_setup(JNIEnv* env, jobject thiz, uint32_t image writer); } + // MIUI ADD START + { + // market name + // Use "" to represent unknown productname as suggested in XiaoMi spec. + std::string productname = GetProperty("ro.product.marketname", ""); + uint32_t count = static_cast(productname.size()) + 1; + + BAIL_IF_INVALID_RET_NULL_SP(writer->addEntry(TAG_XIAOMI_PRODUCT, count, + reinterpret_cast(productname.c_str()), TIFF_IFD_0), env, TAG_XIAOMI_PRODUCT, + writer); + } + // END + { // x resolution uint32_t xres[] = { 72, 1 }; // default 72 ppi @@ -1660,6 +1673,38 @@ static sp DngCreator_setup(JNIEnv* env, jobject thiz, uint32_t image TIFF_IFD_0), env, TAG_FOCALLENGTH, writer); } + { + // FocalLengthIn35mmFilm + uint16_t focalLengthIn35mmFilm = 0; + uint32_t tag = 0; + sp vTags; + sp cache = VendorTagDescriptorCache::getGlobalVendorTagCache(); + if (cache) { + auto vendorId = results.getVendorId(); + cache->getVendorTagDescriptor(vendorId, &vTags); + } + + if (vTags != NULL) { + const char *section = "com.xiaomi.sensor.info"; + const char *TagName = "focalLength35mm"; + const String8 sectionName(section); + const String8 tagName(TagName); + status_t ret = vTags->lookupTag(tagName, sectionName, &tag); + if (ret == 0) { + camera_metadata_entry entry = results.find(tag); + if (entry.count != 0) { + focalLengthIn35mmFilm =static_cast (entry.data.f[0] + 0.5f); + BAIL_IF_INVALID_RET_NULL_SP(writer->addEntry(TAG_FOCALLLENGTHIN35MMFILM, 1, &focalLengthIn35mmFilm, + TIFF_IFD_0), env, TAG_FOCALLLENGTHIN35MMFILM, writer); + } else { + ALOGW("%s: get focalLength35mm failed.", __FUNCTION__); + } + } + } else { + ALOGW("%s:com.xiaomi.sensor.info.focalLength35mm vTags is null.", __FUNCTION__); + } + } + { // f number camera_metadata_entry entry = From 5281b34137efe6c7bafc015150c2123ebee3653f Mon Sep 17 00:00:00 2001 From: Tommy Webb Date: Tue, 2 Jul 2024 15:03:49 +0000 Subject: [PATCH 0636/1315] SystemUI: Fix Internet Tile showing no service If the cellular network is in service, and the data network name is not available for some reason, rather than falling back to the default network name of "No service", use the carrier name. Issue: calyxos#2256 Change-Id: Ibf02f67b70cfc5d4804e9d6a3f53a9d112811d1d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../prod/MobileConnectionRepositoryImpl.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 471536005357..fd6a7686e035 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -78,6 +78,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -386,7 +387,7 @@ class MobileConnectionRepositoryImpl( * See b/322432056 for context. */ @SuppressLint("RegisterReceiverViaContext") - override val networkName: StateFlow = + private val networkNameOrDefault: StateFlow = conflatedCallbackFlow { val receiver = object : BroadcastReceiver() { @@ -416,6 +417,21 @@ class MobileConnectionRepositoryImpl( .flowOn(bgDispatcher) .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName) + /** + * Filtered version of networkNameOrDefault that, when in service, uses the carrier name + * rather than default network name ("No service"). + */ + override val networkName: StateFlow = + combine(isInService, carrierName, networkNameOrDefault) { + isInServiceVal, carrierNameVal, networkNameOrDefaultVal -> + if (isInServiceVal && (networkNameOrDefaultVal === defaultNetworkName)) { + carrierNameVal + } else { + networkNameOrDefaultVal + } + } + .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName) + override val dataEnabled = run { val initial = telephonyManager.isDataConnectionAllowed callbackEvents From d6c34d5e509daa85e69f27b57d3c82d48778ff86 Mon Sep 17 00:00:00 2001 From: wumin3 Date: Tue, 9 Jul 2024 15:00:43 +0800 Subject: [PATCH 0637/1315] AudioService: do not block focus request from applications compiled with lower version sdks The applications compiled with lower version sdks can not request audio focus properly in some scenes, which will cause many anomalies. Change-Id: I8875922efdfbcd08a7872e090c3c4b51446b2637 Signed-off-by: wumin3 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/audio/AudioService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 9cc71f37d0fd..76b0f2ed41e0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -11980,6 +11980,8 @@ public int requestAudioFocus(AudioAttributes aa, int focusReqType, IBinder cb, permissionOverridesCheck = true; } else if (uid < UserHandle.AID_APP_START) { permissionOverridesCheck = true; + } else if (sdk <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + permissionOverridesCheck = true; } final long token = Binder.clearCallingIdentity(); From dacbe25e6cadb797e75daeddf3e31dc8bf6360c3 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Fri, 20 Nov 2020 18:43:26 +0200 Subject: [PATCH 0638/1315] AudioService: Cancel old toasts when switching ringer mode When the user cycles fast between modes the last toast will show so late it'll be irrelevant Change-Id: I0f8f442ea10d670663fdd298f2198f6e8baa7a36 Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/audio/AudioService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 76b0f2ed41e0..e28304e89570 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6634,6 +6634,8 @@ public void setRingerModeInternal(int ringerMode, String caller) { setRingerMode(ringerMode, caller, false /*external*/); } + private static Toast mSilenceToast; + public void silenceRingerModeInternal(String reason) { VibrationEffect effect = null; int ringerMode = AudioManager.RINGER_MODE_SILENT; @@ -6680,7 +6682,9 @@ public void silenceRingerModeInternal(String reason) { } maybeVibrate(effect, reason); setRingerModeInternal(ringerMode, reason); - Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT).show(); + if (mSilenceToast != null) mSilenceToast.cancel(); + mSilenceToast = Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT); + mSilenceToast.show(); } private boolean maybeVibrate(VibrationEffect effect, String reason) { From 2e4159c6e6d444ae4d704ce75fc033418a1e6db7 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Wed, 15 Jan 2025 18:29:04 +0100 Subject: [PATCH 0639/1315] SystemUI: Constrain keyguard indication area burn-in offset Avoid it getting cut off at bottom on devices with low udfps, where the bottom margin is lesser. Change-Id: I2ce00abeba353fff4cb4aa8a58257ef54bf4cca9 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../keyguard/ui/binder/KeyguardIndicationAreaBinder.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 1a8bf001e0c1..a6e470eab41b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -94,7 +94,12 @@ object KeyguardIndicationAreaBinder { launch("$TAG#viewModel.indicationAreaTranslationY") { configurationBasedDimensions - .map { it.defaultBurnInPreventionYOffsetPx } + .map { + minOf( + it.indicationAreaBottomMarginPx, + it.defaultBurnInPreventionYOffsetPx + ) + } .flatMapLatest { defaultBurnInOffsetY -> viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) } @@ -143,12 +148,15 @@ object KeyguardIndicationAreaBinder { view.resources.getDimensionPixelSize( com.android.internal.R.dimen.text_size_small_material ), + indicationAreaBottomMarginPx = + view.resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom), ) } private data class ConfigurationBasedDimensions( val defaultBurnInPreventionYOffsetPx: Int, val indicationAreaPaddingPx: Int, + val indicationAreaBottomMarginPx: Int, val indicationTextSizePx: Int, ) From 991f1dd2cbc38149aa5dfcfd765212641d5dad6d Mon Sep 17 00:00:00 2001 From: LuK1337 Date: Sun, 5 Jan 2025 21:45:24 +0100 Subject: [PATCH 0640/1315] SystemUI: Remove split navigation bar layout for sw900dp Saw some users complaining about that and I personally don't see much point of it. Change-Id: I5b9fe7e4a4f14b231533cd9e295a03bfb852a9b3 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/values-sw900dp/config.xml | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 packages/SystemUI/res/values-sw900dp/config.xml diff --git a/packages/SystemUI/res/values-sw900dp/config.xml b/packages/SystemUI/res/values-sw900dp/config.xml deleted file mode 100644 index 221b0139e713..000000000000 --- a/packages/SystemUI/res/values-sw900dp/config.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - back,home,left;space;right,recent - - From 6fc8c2754a6107a62750f9dbc5a7d67dcabda6de Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 22 Oct 2024 20:25:38 +0300 Subject: [PATCH 0641/1315] don't remove app widgets from user's home screen when the user stops onUserStopped() method name was misleading, it's called from onUserStopping(), while the user is still running. Initial research was done by maade93791 <70593890+maade69@users.noreply.github.com> Co-authored-by: maade93791 <70593890+maade69@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/appwidget/AppWidgetService.java | 2 +- .../com/android/server/appwidget/AppWidgetServiceImpl.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java index 3562205834b4..4bc7e6565977 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java @@ -52,7 +52,7 @@ public void onBootPhase(int phase) { @Override public void onUserStopping(@NonNull TargetUser user) { - mImpl.onUserStopped(user.getUserIdentifier()); + mImpl.onUserStopping(user.getUserIdentifier()); } @Override diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 3b197d30e59a..b8fda0504b9a 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -4703,7 +4703,7 @@ private static AtomicFile getSavedStateFile(int userId) { return new AtomicFile(new File(Environment.getUserSystemDirectory(userId), STATE_FILENAME)); } - void onUserStopped(int userId) { + void onUserStopping(int userId) { if (DEBUG) { Slog.i(TAG, "onUserStopped() " + userId); } @@ -4723,6 +4723,10 @@ void onUserStopped(int userId) { // as we do not want to make host callbacks and provider broadcasts // as the host and the provider will be killed. if (hostInUser && (!hasProvider || providerInUser)) { + // The user's app widget host (i.e. the launcher) is usually still running at + // this point. Don't notify it that the widget is removed to avoid affecting + // homescreen configuration + widget.host.callbacks = null; removeWidgetLocked(widget); widget.host.widgets.remove(widget); widget.host = null; From 69ecd0f28bedd0f80e08fe445483d066cd459d62 Mon Sep 17 00:00:00 2001 From: Shuangxi Xiang Date: Tue, 29 Jul 2025 06:48:55 -0700 Subject: [PATCH 0642/1315] fix NullPointerException in Animator:callOnList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit summary:(Animator$AnimatorCaller$$ExternalSyntheticLambda1.call)java.lang.NullPointerException--Attempt to invoke interface method 'void android.animation.Animator$AnimatorListener.onAnimationEnd(android.animation.Animator, boolean)' on a null object reference det:versionName:20240724.0, versionCode:202407240 java.lang.NullPointerException: Attempt to invoke interface method 'void android.animation.Animator$AnimatorListener.onAnimationEnd(android.animation.Animator, boolean)' on a null object reference at android.animation.Animator$AnimatorCaller$$ExternalSyntheticLambda1.call(D8$$SyntheticClass:0) at android.animation.Animator.callOnList(Animator.java:666) at android.animation.Animator.notifyListeners(Animator.java:609) at android.animation.Animator.notifyEndListeners(Animator.java:634) at android.animation.ValueAnimator.endAnimation(ValueAnimator.java:1306) at android.animation.ValueAnimator.doAnimationFrame(ValueAnimator.java:1566) at android.animation.AnimationHandler.doAnimationFrame(AnimationHandler.java:344) at android.animation.AnimationHandler.-$$Nest$mdoAnimationFrame(Unknown Source:0) at android.animation.AnimationHandler$1.doFrame(AnimationHandler.java:87) at android.view.Choreographer$CallbackRecord.ru Test:UAT Change-Id: I135eac97787a6d3b820a2336908bf1412d2ed218 Signed-off-by: xiangshuangxi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/animation/Animator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index fef97d382da1..ef709c094dd6 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -739,7 +739,7 @@ void callOnList( for (int i = 0; i < size; i++) { //noinspection unchecked T item = (T) array[i]; - call.call(item, animator, isReverse); + if (item != null) call.call(item, animator, isReverse); array[i] = null; } // Store it for the next call so we can reuse this array, if needed. From 5b4e620ab4c43adaa23e18b934caf7a45fbe50dd Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 23 Aug 2025 03:12:06 +0530 Subject: [PATCH 0643/1315] SystemUI: Add margin above small clock in lockscreen * Space is too small. It often overlaps with face unlock image. * Tweak space above clock and above notifications. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/dimens.xml | 4 ++-- .../systemui/keyguard/ui/view/layout/sections/ClockSection.kt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8a5f75f03543..9888031a14bd 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -908,9 +908,9 @@ split shade on keyguard--> 68dp - 20dp + 16dp - 18dp + 16dp 14dp diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 7baddb8700a6..6b278212c50a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -264,6 +264,7 @@ constructor( ), ) val smallClockTopMargin = keyguardClockViewModel.getSmallClockTopMargin() + + context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) create(R.id.small_clock_guideline_top, ConstraintSet.HORIZONTAL_GUIDELINE) setGuidelineBegin(R.id.small_clock_guideline_top, smallClockTopMargin) connect( @@ -279,6 +280,7 @@ constructor( val smallClockBottom = keyguardClockViewModel.getSmallClockTopMargin() + context.resources.getDimensionPixelSize(clocksR.dimen.small_clock_height) + - context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) val marginBetweenSmartspaceAndNotification = context.resources.getDimensionPixelSize( R.dimen.keyguard_status_view_bottom_margin From a0a6c3784b65acedc462d56f151761e936b8cba4 Mon Sep 17 00:00:00 2001 From: Shuangxi Xiang Date: Fri, 5 Sep 2025 03:06:57 -0700 Subject: [PATCH 0644/1315] Optimize the DateTimeView logic time consumption when updating the Ui main thread time During the notification bar case fluency test in the system core UI scene, the SystemUIUi main thread experienced severe frame drops due to frequent execution of the following DateTimeView logic while updating the status bar time once a minute: android.view.ViewRootImpl$ViewRootHandler: android.widget.DateTimeView$ReceiverInfo$$ExternalSyntheticLambda0 Signed-off-by: Shuangxi Xiang Change-Id: I888218897555d596b46b78f23a63d1df1750b3ca Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/widget/DateTimeView.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index ca9cd66188aa..8eaebde4a40e 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -165,10 +165,6 @@ void update() { if (mLocalTime == null || getVisibility() == GONE) { return; } - if (mShowRelativeTime) { - updateRelativeTime(); - return; - } int display; ZoneId zoneId = ZoneId.systemDefault(); @@ -476,6 +472,13 @@ void updateAll() { final int count = mAttachedViews.size(); for (int i = 0; i < count; i++) { DateTimeView view = mAttachedViews.get(i); + if (view.mShowRelativeTime) { + // Every minute, the status bar only needs to execute this function once + // to update the display + view.post(() -> view.updateRelativeTime()); + return; + } + view.post(() -> view.clearFormatAndUpdate()); } } From 11a5bd6d426679c6ef220fcf6be52508b5a17442 Mon Sep 17 00:00:00 2001 From: Sourajit Karmakar Date: Tue, 29 Dec 2020 21:50:09 +0100 Subject: [PATCH 0645/1315] SystemUI: Screenrecord: Add delete action to the notification This reverts commit 6fcdb6b. Change-Id: I125d2aff9b406341a533cbfe80686ee473dd3e88 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/values/matrixx_strings.xml | 5 +++ .../screenrecord/RecordingService.java | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 070ee7ee877a..6b62d5a17734 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -175,4 +175,9 @@ Recognizing face... + + + Delete + + Screen recording deleted diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 1c8c50082146..89216b2e8ae5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -100,6 +100,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList "com.android.systemui.screenrecord.STOP_FROM_NOTIF"; private static final String ACTION_SHOW_DIALOG = "com.android.systemui.screenrecord.SHOW_DIALOG"; protected static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; + protected static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE"; private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; protected static final String EXTRA_NOTIFICATION_ID = "notification_id"; @@ -310,6 +311,22 @@ public int onStartCommand(Intent intent, int flags, int startId) { mController.createScreenRecordDialog(null).show(); } break; + case ACTION_DELETE: + // Close quick shade + closeSystemDialogs(); + + Uri uri = intent.getParcelableExtra(EXTRA_PATH, Uri.class); + getContentResolver().delete(uri, null, null); + + Toast.makeText( + this, + R.string.screenrecord_delete_description, + Toast.LENGTH_LONG).show(); + + // Remove notification + mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); + Log.d(TAG, "Deleted recording " + uri); + break; } return Service.START_STICKY; } @@ -483,6 +500,16 @@ protected Notification createSaveNotification(@Nullable SavedRecording recording PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); + Notification.Action deleteAction = new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_screenrecord), + getResources().getString(R.string.screenrecord_delete_label), + PendingIntent.getService( + this, + REQUEST_CODE, + getDeleteIntent(this, uri), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle()); @@ -496,6 +523,7 @@ protected Notification createSaveNotification(@Nullable SavedRecording recording viewIntent, PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) + .addAction(deleteAction) .setAutoCancel(true) .setGroup(GROUP_KEY_SAVED) .addExtras(extras); @@ -680,6 +708,11 @@ private Intent getShareIntent(Context context, Uri path) { .putExtra(EXTRA_NOTIFICATION_ID, mNotificationId); } + private Intent getDeleteIntent(Context context, Uri path) { + return new Intent(context, this.getClass()).setAction(ACTION_DELETE) + .putExtra(EXTRA_PATH, path); + } + @Override public void onInfo(MediaRecorder mr, int what, int extra) { Log.d(getTag(), "Media recorder info: " + what); From 05505076533f6102fed00553b568f79056655573 Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Mon, 18 Dec 2023 17:05:03 +0200 Subject: [PATCH 0646/1315] Screenrecord: Fix notifications not being dismissed ...after using actions such as "delete" or "share". Post / Recieve the notification ID with the intent for that. Also count grouped notifications to decide when to dismiss the group summary and only close QS after all notifications are dismissed. Change-Id: I1023367412b2b737b1cf3e9935b1d268059a2a3d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../screenrecord/RecordingService.java | 90 ++++++++++++++----- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 89216b2e8ae5..509b64f9f2ec 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -40,6 +40,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.Display; import android.widget.Toast; @@ -76,6 +77,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList protected static final int NOTIF_GROUP_ID_SAVED = NOTIF_BASE_ID + 1; protected static final int NOTIF_GROUP_ID_ERROR_SAVING = NOTIF_BASE_ID + 2; protected static final int NOTIF_GROUP_ID_ERROR_STARTING = NOTIF_BASE_ID + 3; + protected static final int PROGRESS_NOTIF_ID = NOTIF_BASE_ID + 4; // YAAP + protected static final int ERROR_NOTIF_ID = NOTIF_BASE_ID + 5; // YAAP private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; @VisibleForTesting static final String GROUP_KEY_SAVED = "screen_record_saved"; @@ -299,7 +302,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { startActivity(Intent.createChooser(shareIntent, shareLabel) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); // Remove notification - mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); + final int id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId); + mNotificationManager.cancelAsUser(null, id, currentUser); + maybeDismissGroup(currentUser); return false; }, false, false); @@ -312,20 +317,20 @@ public int onStartCommand(Intent intent, int flags, int startId) { } break; case ACTION_DELETE: - // Close quick shade - closeSystemDialogs(); - - Uri uri = intent.getParcelableExtra(EXTRA_PATH, Uri.class); + Uri uri = Uri.parse(intent.getStringExtra(EXTRA_PATH)); getContentResolver().delete(uri, null, null); - Toast.makeText( this, R.string.screenrecord_delete_description, Toast.LENGTH_LONG).show(); - // Remove notification - mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); + final int id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId); + mNotificationManager.cancelAsUser(null, id, currentUser); + maybeDismissGroup(currentUser); Log.d(TAG, "Deleted recording " + uri); + + // Close quick shade + maybeCloseSystemDialogs(); break; } return Service.START_STICKY; @@ -425,7 +430,7 @@ private void createErrorNotification( .setContentTitle(notificationContentTitle) .setGroup(groupKey) .addExtras(extras); - startForeground(mNotificationId, builder.build()); + startForeground(ERROR_NOTIF_ID, builder.build()); } @VisibleForTesting @@ -461,7 +466,7 @@ protected void createRecordingNotification() { .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) .addAction(stopAction) .addExtras(extras); - startForeground(mNotificationId, builder.build()); + startForeground(PROGRESS_NOTIF_ID, builder.build()); } @VisibleForTesting @@ -495,7 +500,7 @@ protected Notification createSaveNotification(@Nullable SavedRecording recording strings().getShareLabel(), PendingIntent.getService( this, - REQUEST_CODE, + mNotificationId, /* unique request code */ getShareIntent(this, uri), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); @@ -505,8 +510,8 @@ protected Notification createSaveNotification(@Nullable SavedRecording recording getResources().getString(R.string.screenrecord_delete_label), PendingIntent.getService( this, - REQUEST_CODE, - getDeleteIntent(this, uri), + mNotificationId, /* unique request code */ + getDeleteIntent(this, uri.toString()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); @@ -519,7 +524,7 @@ protected Notification createSaveNotification(@Nullable SavedRecording recording .setContentText(strings().getSaveText()) .setContentIntent(PendingIntent.getActivity( this, - REQUEST_CODE, + mNotificationId, /* unique request code */ viewIntent, PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) @@ -554,6 +559,8 @@ private void postGroupSummaryNotification( String notificationContentTitle, String groupKey, int notificationIdForGroup) { + if (countGroupNotifications() < 1) + return; // only post after we show the 2nd notification Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle()); @@ -562,12 +569,37 @@ private void postGroupSummaryNotification( .setContentTitle(notificationContentTitle) .setGroup(groupKey) .setGroupSummary(true) + .setAutoCancel(true) .setExtras(extras) .build(); mNotificationManager.notifyAsUser( getTag(), notificationIdForGroup, groupNotif, currentUser); } + private void maybeDismissGroup(UserHandle currentUser) { + if (countGroupNotifications() >= 1) + return; // dismiss only when we have one notification left + mNotificationManager.cancelAsUser(TAG, NOTIF_GROUP_ID_SAVED, currentUser); + } + + private void maybeCloseSystemDialogs() { + if (countGroupNotifications() > 0) + return; // only dismiss when we cancel the last group notification + closeSystemDialogs(); + } + + private int countGroupNotifications() { + StatusBarNotification[] notifications = mNotificationManager.getActiveNotifications(); + int count = 0; + for (StatusBarNotification notification : notifications) { + final String tag = notification.getTag(); + if (tag == null || !tag.equals(TAG)) continue; + final int id = notification.getId(); + if (id != NOTIF_GROUP_ID_SAVED) count++; + } + return count; + } + private void stopService(int userId, @StopReason int stopReason) { if (userId == USER_ID_NOT_SPECIFIED) { userId = mUserContextTracker.getUserContext().getUserId(); @@ -611,13 +643,14 @@ private void stopService(int userId, @StopReason int stopReason) { private void saveRecording(int userId) { UserHandle currentUser = new UserHandle(userId); - mNotificationManager.notifyAsUser(null, mNotificationId, + mNotificationManager.notifyAsUser(null, PROGRESS_NOTIF_ID, createProcessingNotification(), currentUser); mLongExecutor.execute(() -> { try { Log.d(getTag(), "saving recording"); SavedRecording savedRecording = getRecorder() != null ? getRecorder().save() : null; + mNotificationManager.cancelAsUser(null, PROGRESS_NOTIF_ID, currentUser); postGroupSummaryNotification( currentUser, strings().getSaveTitle(), @@ -628,7 +661,8 @@ private void saveRecording(int userId) { Log.e(getTag(), "Error saving screen recording: " + e.getMessage()); e.printStackTrace(); showErrorToast(R.string.screenrecord_save_error); - mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); + mNotificationManager.cancelAsUser(null, PROGRESS_NOTIF_ID, currentUser); + maybeDismissGroup(currentUser); } }); } @@ -662,6 +696,11 @@ private void setHEVC(boolean hevc) { } } + private PendingIntent getStopPendingIntent() { + return PendingIntent.getService(this, REQUEST_CODE, getStopIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + protected String getTag() { return TAG; } @@ -703,14 +742,23 @@ protected Intent getNotificationIntent(Context context) { } private Intent getShareIntent(Context context, Uri path) { - return new Intent(context, this.getClass()).setAction(ACTION_SHARE) + return getShareIntent(context, path, mNotificationId); + } + + private static Intent getShareIntent(Context context, Uri path, int id) { + return new Intent(context, RecordingService.class).setAction(ACTION_SHARE) .putExtra(EXTRA_PATH, path) - .putExtra(EXTRA_NOTIFICATION_ID, mNotificationId); + .putExtra(EXTRA_NOTIFICATION_ID, id); } - private Intent getDeleteIntent(Context context, Uri path) { - return new Intent(context, this.getClass()).setAction(ACTION_DELETE) - .putExtra(EXTRA_PATH, path); + private Intent getDeleteIntent(Context context, String path) { + return getDeleteIntent(context, path, mNotificationId); + } + + private static Intent getDeleteIntent(Context context, String path, int id) { + return new Intent(context, RecordingService.class).setAction(ACTION_DELETE) + .putExtra(EXTRA_PATH, path) + .putExtra(EXTRA_NOTIFICATION_ID, id); } @Override From 94e87ae29db48852169476e881150a1dfdc244d5 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 5 Oct 2025 09:45:58 +0530 Subject: [PATCH 0647/1315] ScreenshotController: Clean up resources when dismissed * Also remove some unused components. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../screenshot/ScreenshotController.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index 57368610a8e4..b07e3817b9ff 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -111,7 +111,6 @@ internal constructor( private val currentRequestCallbacks: MutableList = mutableListOf() - private var screenBitmap: Bitmap? = null private var screenshotTakenInPortrait = false private var screenshotAnimation: Animator? = null private var packageName = "" @@ -201,7 +200,6 @@ internal constructor( return } - screenBitmap = currentBitmap val oldPackageName = packageName packageName = screenshot.packageNameString @@ -327,8 +325,11 @@ internal constructor( // Any cleanup needed when the service is being destroyed. override fun onDestroy() { + screenshotAnimation?.cancel() + screenshotAnimation = null removeWindow() screenshotSoundController.releaseScreenshotSoundAsync() + scrollCaptureExecutor.close() releaseContext() bgExecutor.shutdown() screenshotHandler.cancelTimeout() @@ -338,7 +339,9 @@ internal constructor( /** Release the constructed window context. */ private fun releaseContext() { - broadcastDispatcher.unregisterReceiver(copyBroadcastReceiver) + try { + broadcastDispatcher.unregisterReceiver(copyBroadcastReceiver) + } catch (t: Throwable) { } context.release() } @@ -551,7 +554,10 @@ internal constructor( screenshotAnimation = viewProxy.createScreenshotDropInAnimation(screenRect, showFlash).apply { - doOnEnd { onAnimationComplete?.run() } + doOnEnd { + onAnimationComplete?.run() + screenshotAnimation = null + } // Play the shutter sound to notify that we've taken a screenshot playScreenshotSound() if (LogConfig.DEBUG_ANIM) { @@ -564,6 +570,8 @@ internal constructor( /** Reset screenshot view and then call onCompleteRunnable */ private fun finishDismiss() { Log.d(TAG, "finishDismiss") + screenshotAnimation?.cancel() + screenshotAnimation = null actionsController.endScreenshotSession() scrollCaptureExecutor.close() currentRequestCallbacks.forEach { it.onFinish() } @@ -601,9 +609,7 @@ internal constructor( finisher.accept(result.uri) } catch (e: Exception) { Log.d(TAG, "Failed to store screenshot", e) - if (LogConfig.DEBUG_CALLBACK) { - Log.d(TAG, "calling back with uri: null") - } + logScreenshotResultStatus(null, screenshot.userHandle) finisher.accept(null) } }, @@ -632,13 +638,6 @@ internal constructor( ) == 1 } - private val fullScreenRect: Rect - get() { - val displayMetrics = DisplayMetrics() - display.getRealMetrics(displayMetrics) - return Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels) - } - /** Injectable factory to create screenshot controller instances for a specific display. */ @AssistedFactory interface Factory : InteractiveScreenshotHandler.Factory { From 3b75079a9cd57683d103112ba4b4e67bc0167b19 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 5 Oct 2025 09:51:20 +0530 Subject: [PATCH 0648/1315] ScreenshotController: Play haptic feedback even on normal ringer mode * Previously we played only when ringer mode was set to vibrate. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/screenshot/ScreenshotController.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index b07e3817b9ff..075e622ff41a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -493,21 +493,18 @@ internal constructor( private fun playScreenshotSound() { var playSound = false + var playHaptic = false when (audioManager.ringerMode) { AudioManager.RINGER_MODE_SILENT -> { // do nothing } AudioManager.RINGER_MODE_VIBRATE -> { - vibrator?.takeIf { it.hasVibrator() }?.vibrate( - VibrationEffect.createOneShot( - 50, - VibrationEffect.DEFAULT_AMPLITUDE - ) - ) + playHaptic = true } AudioManager.RINGER_MODE_NORMAL -> { // in this case we want to play sound even if not forced on playSound = true + playHaptic = true } } if (playSound && Settings.System.getIntForUser( @@ -519,6 +516,11 @@ internal constructor( ) { screenshotSoundController.playScreenshotSoundAsync() } + if (playHaptic) { + vibrator?.takeIf { it.hasVibrator() }?.vibrate( + VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE) + ) + } } /** From 43529ad22f6c11140f417029b1d71e626ab9d6d8 Mon Sep 17 00:00:00 2001 From: "xingyun.wang" Date: Wed, 21 May 2025 21:06:00 +0800 Subject: [PATCH 0649/1315] system: Postpone NetworkWatchlistService scan during boot Avoids ANRs and excessive I/O by delaying the system APK summary scan, which was previously triggered inappropriately during the boot phase. The scan will now initiate 1 minute after boot if triggered within the initial 30 seconds. Test: test steps are: 1.Install 60 APKs; All Logs, Data filled to 95%; 2.Close watchdog, open Sysdump; 3.Repeated restart for a long time (120h) Bug: 414507692 Change-Id: I1d329484ac12d4763248bd9c650d15e8741aea5d Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../net/watchlist/WatchlistLoggingHandler.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java index c863cbf327b8..54ce837ef2af 100644 --- a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java +++ b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java @@ -35,6 +35,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Slog; +import android.os.SystemClock; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -69,6 +70,9 @@ class WatchlistLoggingHandler extends Handler { private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); private static final String DROPBOX_TAG = "network_watchlist_report"; + private static final long BOOT_TIME_THRESHOLD_MILLIS = TimeUnit.SECONDS.toMillis(30); + private static final long INITIAL_CHECK_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1); + private final Context mContext; private final @Nullable DropBoxManager mDropBoxManager; private final ContentResolver mResolver; @@ -178,8 +182,14 @@ private boolean isPackageTestOnly(int uid) { * Report network watchlist records if we collected enough data. */ public void reportWatchlistIfNecessary() { + long uptimeMillis = SystemClock.elapsedRealtime(); final Message msg = obtainMessage(REPORT_RECORDS_IF_NECESSARY_MSG); - sendMessage(msg); + + if(uptimeMillis < BOOT_TIME_THRESHOLD_MILLIS) { + sendMessageDelayed(msg, INITIAL_CHECK_DELAY_MILLIS); + } else { + sendMessage(msg); + } } public void forceReportWatchlistForTest(long lastReportTime) { From 31e3388165a109fa16c3130a19174f5e8049c118 Mon Sep 17 00:00:00 2001 From: Abdulla Shoukathali Date: Mon, 7 Jul 2025 01:44:27 -0700 Subject: [PATCH 0650/1315] Fix ConcurrentModificationException in LocaleStore Language Settings crashing due to ConcurrentModificationException. The structure of the map used for supportedLocaleInfos is being changed while it's being read. One way to fix this is by creating a copy of map for iteration. bug: 420412904 Change-Id: Ideb02ac913f474edec5bb437574fb6da94344d0f Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/app/LocaleStore.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index ceecf787901e..a76d4ad7cbc3 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -31,6 +31,7 @@ import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -603,7 +604,8 @@ private static Set getTierLocales( boolean hasTargetParent = parent != null; String parentId = hasTargetParent ? parent.getId() : null; HashSet result = new HashSet<>(); - for (LocaleStore.LocaleInfo li : supportedLocaleInfos.values()) { + Collection currentLocaleInfos = new ArrayList<>(supportedLocaleInfos.values()); + for (LocaleStore.LocaleInfo li : currentLocaleInfos) { if (isShallIgnore(ignorables, li, translatedOnly)) { continue; } From 556d73109aacc0858f79693b8109511d0407cf62 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 30 Oct 2025 22:29:36 +0530 Subject: [PATCH 0651/1315] SystemUI: Prevent NotifCollection illegal state on notification dump * Not sure where this is failing for main thread check. * Let's remove assert check for dump function. Log: time: 1761829105393 msg: java.lang.IllegalStateException: should be called from the main thread. sMainLooper.threadName=main Thread.currentThread()=dump-thread stacktrace: java.lang.IllegalStateException: should be called from the main thread. sMainLooper.threadName=main Thread.currentThread()=dump-thread at com.android.systemui.util.Assert.isMainThread(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:60) at com.android.systemui.statusbar.notification.collection.NotifCollection.dump(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:3) at com.android.systemui.dump.DumpHandler$Companion.dumpDumpable(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:21) at com.android.systemui.dump.DumpHandler.dumpCritical(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:31) at com.android.systemui.dump.DumpHandler.dump(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:8) at com.android.systemui.SystemUIService.dump(go/retraceme a0ed5d08cbafd6ceda3025e7580143a234574b99d67372ec0a205b5910abeaab:19) at android.app.ActivityThread.handleDumpService(ActivityThread.java:5389) at android.app.ActivityThread.-$$Nest$mhandleDumpService(Unknown Source:0) at android.app.ActivityThread$ApplicationThread.lambda$dumpService$0(ActivityThread.java:1521) at android.app.ActivityThread$ApplicationThread.$r8$lambda$OeHpPo9a5q6Dn6gB6isOwWv_X-E(Unknown Source:0) at android.app.ActivityThread$ApplicationThread$$ExternalSyntheticLambda8.run(D8$$SyntheticClass:0) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651) at java.lang.Thread.run(Thread.java:1119) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/notification/collection/NotifCollection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index fb2e6413c11c..a6b608d1c559 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -976,7 +976,7 @@ private static boolean userIdMatches(NotificationEntry entry, int userId) { @Override public void dump(PrintWriter pw, @NonNull String[] args) { - final List entries = new ArrayList<>(getAllNotifs()); + final List entries = new ArrayList<>(mReadOnlyNotificationSet); entries.sort(Comparator.comparing(NotificationEntry::getKey)); pw.println("\t" + TAG + " unsorted/unfiltered notifications: " + entries.size()); From 8f123852e53b0e4b9c7d2369156ff1b73600565f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 10 Nov 2025 19:49:54 +0530 Subject: [PATCH 0652/1315] SystemUI: Prevent ShadeListBuilder illegal state on notification dump Fixes: https://github.com/crdroidandroid/issue_tracker/issues/822 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/notification/collection/ShadeListBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 6050a2a5d824..0d0ba304b6c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -345,7 +345,6 @@ void setComparators(List comparators) { } List getShadeList() { - Assert.isMainThread(); // NOTE: Accessing this method when the pipeline is running is generally going to provide // incorrect results, and indicates a poorly behaved component of the pipeline. mPipelineState.requireState(STATE_IDLE); From dbdd4353f5b1c84c68d41a9317612881df5187d0 Mon Sep 17 00:00:00 2001 From: lijilou Date: Tue, 11 Nov 2025 10:01:02 +0800 Subject: [PATCH 0653/1315] Fix index out of bounds exception in HandwritingModeController. The possible execution order of the code is on the UI thread: 1 Firstly,we post the delay mDelegationIdleTimeoutRunnable into the UiHandler. 2 Secondly,the onInputEvent method is caaled,so the mRecordingGesture or mRecordingGestureAfterStylusUp is true. 3 mDelegationIdleTimeoutRunnable is Executed due to timeout,so the mHandwritingBuffer is empty. 4 Finally,startHandwritingSession method is called,we call the get method of mHandwritingBuffer which is empty,so IndexOutOfBoundsException happen. we should reset the the mRecordingGesture and mRecordingGestureAfterStylusUp value to false when timeout happen. Bug: 459595388 Flag: bugfix Change-Id: I28002bb90322db8d40a4c55d9102521def9cf52a Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/inputmethod/HandwritingModeController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index bc2121f0edfd..bb47d3659193 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -245,6 +245,8 @@ private void scheduleHandwritingDelegationTimeout() { mDelegationIdleTimeoutRunnable = () -> { Slog.d(TAG, "Stylus handwriting delegation idle timed-out."); clearPendingHandwritingDelegation(); + mRecordingGesture = false; + mRecordingGestureAfterStylusUp = false; if (mHandwritingBuffer != null) { mHandwritingBuffer.forEach(MotionEvent::recycle); mHandwritingBuffer.clear(); From 38143a7289c189b90e3ec537fdcc377077a325ed Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 15 Nov 2025 13:46:26 +0530 Subject: [PATCH 0654/1315] SystemUI: Hide data switch tile on non-voice capable devices Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/qs/tiles/DataSwitchTile.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java index 6b52a6e2bf5e..32a0bc7aafda 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSwitchTile.java @@ -106,8 +106,15 @@ public DataSwitchTile( mPanelInteractor = panelInteractor; } + public static boolean isVoiceCapable(Context context) { + TelephonyManager telephony = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return telephony != null && telephony.isVoiceCapable(); + } + @Override public boolean isAvailable() { + if (!isVoiceCapable(mContext)) return false; int count = TelephonyManager.getDefault().getPhoneCount(); Log.d(TAG, "phoneCount: " + count); return count >= 2; From 8fdace7a76d9692eae0e10b84aa2e8681bd59e61 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 3 Jan 2026 22:08:02 +0530 Subject: [PATCH 0655/1315] SystemUI: Fix keyguard back button background Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../keyguard/KeyguardPinViewController.java | 18 ++++++++++-------- .../src/com/android/keyguard/NumPadButton.java | 7 ------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index e6c1e535ad3e..c134bfbe6437 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -102,7 +102,9 @@ protected void onViewAttached() { mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); mPasswordEntry.setUsePinShapes(true); - updateAutoConfirmationState(); + if (isAutoPinConfirmEnabledInSettings()) { + updateAutoConfirmationState(); + } mView.updatePinScrambling( LineageSettings.System.getIntForUser(getContext().getContentResolver(), LineageSettings.System.LOCKSCREEN_PIN_SCRAMBLE_LAYOUT, 0, @@ -145,7 +147,9 @@ public boolean startDisappearAnimation(Runnable finishRunnable) { @Override protected void handleAttemptLockout(long elapsedRealtimeDeadline) { super.handleAttemptLockout(elapsedRealtimeDeadline); - updateAutoConfirmationState(); + if (isAutoPinConfirmEnabledInSettings()) { + updateAutoConfirmationState(); + } } private void updateAutoConfirmationState() { @@ -172,12 +176,10 @@ private void updateOKButtonVisibility() { * Visibility changes are only for auto confirmation configuration. */ private void updateBackSpaceVisibility() { - boolean isAutoConfirmation = isAutoPinConfirmEnabledInSettings(); - mBackspaceKey.setTransparentMode(/* isTransparentMode= */ - isAutoConfirmation && !mDisabledAutoConfirmation); - if (isAutoConfirmation) { - if (mPasswordEntry.getText().length() > 0 - || mDisabledAutoConfirmation) { + boolean hasPass = mPasswordEntry.getText().length() > 0; + mBackspaceKey.setTransparentMode(hasPass); + if (isAutoPinConfirmEnabledInSettings()) { + if (hasPass || mDisabledAutoConfirmation) { mBackspaceKey.setVisibility(View.VISIBLE); } else { mBackspaceKey.setVisibility(View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 717519472bbb..810c866c83ae 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -134,17 +134,10 @@ public void setTransparentMode(boolean isTransparentMode) { if (mDrawableForTransparentMode != 0) { setImageResource(mDrawableForTransparentMode); } - setBackgroundColor(getResources().getColor(android.R.color.transparent)); } else { if (mDefaultDrawable != 0) { setImageResource(mDefaultDrawable); } - Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background); - if (Flags.bouncerUiRevamp2() && bgDrawable != null) { - int bgColor = BouncerColors.pinActionBg(getContext()); - bgDrawable.setTint(bgColor); - } - setBackground(bgDrawable); } setupAnimator(); reloadColors(); From 491956e3f082ee6a9518e0964586eb301cba89af Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:15:03 +0800 Subject: [PATCH 0656/1315] fixing contacts apps crash 12-18 21:12:28.236 4196 4196 E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.os.Bundle.getInt(java.lang.String, int)' on a null object reference 12-18 21:12:28.236 4196 4196 E AndroidRuntime: at android.provider.ContactsContract$RawContacts$DefaultAccount.getDefaultAccountForNewContacts(ContactsContract.java:3302) 12-18 21:12:28.236 4196 4196 E AndroidRuntime: at com.android.contacts.preference.ContactsPreferences$SystemDefaultAccountReader.getDefaultAccountAndState(ContactsPreferences.java:416) 12-18 21:12:28.236 4196 4196 E AndroidRuntime: at com.android.contacts.preference.ContactsPreferences.canInsertIntoLocalAccounts(ContactsPreferences.java:214) 12-18 21:12:28.236 4196 4196 E AndroidRuntime: at com.android.contacts.list.DefaultContactBrowseListFragment.onResume(DefaultContactBrowseListFragment.java:810) Change-Id: I2e040c987357b2f3578c48c2ba4af09a73346a7a Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/ContactsContract.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 687a448d780a..648433afcfaf 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -3298,6 +3298,9 @@ public boolean equals(Object obj) { Bundle response = nullSafeCall(resolver, ContactsContract.AUTHORITY_URI, QUERY_DEFAULT_ACCOUNT_FOR_NEW_CONTACTS_METHOD, null, null); + if (response == null) { + return DefaultAccountAndState.ofNotSet(); + } int defaultAccountState = response.getInt(KEY_DEFAULT_ACCOUNT_STATE, -1); if (DefaultAccountAndState.isCloudOrSimAccount(defaultAccountState)) { String accountName = response.getString(Settings.ACCOUNT_NAME); From 0fa14bdc4065a657bff69ef508c6553597781074 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:06:46 +0800 Subject: [PATCH 0657/1315] fixing configuration controller crash 12-24 16:23:32.690 17585 17585 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.Configuration android.content.res.Resources.getConfiguration()' on a null object reference 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.app.ConfigurationController.updateLocaleListFromAppContext(ConfigurationController.java:285) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7775) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2560) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:110) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.os.Looper.dispatchMessage(Looper.java:315) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:251) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.os.Looper.loop(Looper.java:349) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:9048) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 12-24 16:23:32.690 17585 17585 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) Change-Id: I96c139f477958fc29399e81b4892245606bcfe67 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ConfigurationController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index c6a4d2ad321b..3ed1263a7112 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -282,6 +282,10 @@ int getCurDefaultDisplayDpi() { * original LocaleList. */ void updateLocaleListFromAppContext(@NonNull Context context) { + final Resources resources = context.getResources(); + if (resources == null) { + return; + } final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0); final LocaleList newLocaleList = mResourcesManager.getConfiguration().getLocales(); final int newLocaleListSize = newLocaleList.size(); From f1740c0143aede26f00e47a3f45cd4dd38fa5337 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 6 Jan 2023 17:20:46 +0200 Subject: [PATCH 0658/1315] Revert "Null safe package name in AppOps writeState" This reverts commit 0b925d4f46ef9d0f25fa5fd56e996280e9a98c71. Reverted commit introduced a bug: it skipped the "pkg" tag for ops with null package name. This meant that ops with null package name were serialized differently than ops with non-null package name. Tag hierarchy became the following: for non-null package name ops: "pkg" -> "uid" -> "op" -> "st" for null package name ops: "uid" -> "op" -> "st" Uid ops have the same first two tags as null package name ops started to have: "uid" -> "op". (refer to the loop over uidStatesClone elements above). This led to type confusion during deserialization that happens in readState(): null package name ops were deserialized as uid ops, through readUidOps() instead of through readPackage(). Uid ops are serialized differently than uid element inside package ops, specifically the latter skips the op mode ("m") attribute when the op mode is at its default value. Op mode attribute is read unconditionally in readUidOps(), which led to XmlPullParserException: Missing attribute "m" exception. This exception is caught in readState(), and is handled by discarding all deserialized state, which meant that all appops got reset to their default values. Subsequent commit adds skipping of ops with null package name during serialization: they are invalid, package name is defined and treated as @NonNull in multiple places. Such ops are being constructed due to another bug. Change-Id: I8b13b8f0979a6daff2db33a1b8e8544dd9e8e531 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/appop/AppOpsService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 1aa02681f1f6..d68a78da918d 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -5571,15 +5571,13 @@ void writeRecentAccesses() { String lastPkg = null; for (int i=0; i Date: Fri, 6 Jan 2023 17:22:29 +0200 Subject: [PATCH 0659/1315] appops: skip ops for invalid null package during state serialization There's a bug that leads to construction of ops for invalid null package name. Package name should always be non-null, it's defined and treated as such in AppOpsService. It being null leads to crashes in system_server when appops state is serialized. Previous commit reverted a buggy workaround for this bug, add a new workaround to prevent these crashes. Change-Id: I5262a347adebf73167e63a5314ca0941d8e39e2c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/appop/AppOpsService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index d68a78da918d..45c0f9ba393e 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -5571,6 +5571,9 @@ void writeRecentAccesses() { String lastPkg = null; for (int i=0; i Date: Thu, 27 Nov 2025 16:39:18 +0800 Subject: [PATCH 0660/1315] preventing media album bitmaps from impacting performance targets the same approach as minari's: https://github.com/AxionAOSP/android_frameworks_base/commit/b59b6f3eef4a8b216da48a1313284795024206e3. but dont force 500px, this approach prevents media bitmaps from impacting performance on high dpi devices without increasing the max bitmap size on low pixel devices and touching unnecessary stuffs Change-Id: I3fae0e9671893566dd402a221e04f44b5f073329 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- media/java/android/media/session/MediaSession.java | 6 +++++- .../media/controls/domain/pipeline/MediaDataLoader.kt | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 8989ae8eebe4..a0bf7faa3151 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -200,8 +200,12 @@ public MediaSession(@NonNull Context context, @NonNull String tag, } mContext = context; - mMaxBitmapSize = context.getResources().getDimensionPixelSize( + + int bitmapSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); + + mMaxBitmapSize = Math.min(bitmapSize, 500); + mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context .getSystemService(Context.MEDIA_SESSION_SERVICE); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index 3d1cad0da0bb..b2e7fe0012d9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -71,6 +71,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import kotlin.math.min /** Loads media information from media style [StatusBarNotification] classes. */ @SysUISingleton @@ -89,8 +90,11 @@ constructor( private val mediaProcessingJobs = ConcurrentHashMap() private val artworkWidth: Int = - context.resources.getDimensionPixelSize( - com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + min( + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ), + 500 ) private val artworkHeight: Int = context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) From dc88f3aa232f78b92b6bedd63e64ac404c3a286c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:46:15 +0800 Subject: [PATCH 0661/1315] center-cropping media bitmaps to improve quality applying ImageView's CenterCrop style of cropping to media bitmap Change-Id: I219142f5c9addbbfc08bfda0d23c8e3f71139139 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- media/java/android/media/MediaMetadata.java | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/media/java/android/media/MediaMetadata.java b/media/java/android/media/MediaMetadata.java index eb2ea4c94b55..32526a730863 100644 --- a/media/java/android/media/MediaMetadata.java +++ b/media/java/android/media/MediaMetadata.java @@ -24,6 +24,9 @@ import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; import android.media.browse.MediaBrowser; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -1008,14 +1011,26 @@ public MediaMetadata build() { } private Bitmap scaleBitmap(Bitmap bmp, int maxDimension) { - float maxDimensionF = maxDimension; - float widthScale = maxDimensionF / bmp.getWidth(); - float heightScale = maxDimensionF / bmp.getHeight(); - float scale = Math.min(widthScale, heightScale); - int height = (int) (bmp.getHeight() * scale); - int width = (int) (bmp.getWidth() * scale); + if (bmp == null) return null; + int srcWidth = bmp.getWidth(); + int srcHeight = bmp.getHeight(); + float scale = Math.max( + (float) maxDimension / srcWidth, + (float) maxDimension / srcHeight + ); + float scaledWidth = scale * srcWidth; + float scaledHeight = scale * srcHeight; + float dx = (maxDimension - scaledWidth) / 2f; + float dy = (maxDimension - scaledHeight) / 2f; + Bitmap output = Bitmap.createBitmap(maxDimension, maxDimension, bmp.getConfig()); + Canvas canvas = new Canvas(output); + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); + matrix.postTranslate(dx, dy); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); + canvas.drawBitmap(bmp, matrix, paint); StrictMode.noteSlowCall("Downscaling oversized MediaMetadata Bitmap"); - return Bitmap.createScaledBitmap(bmp, width, height, true); + return output; } } } From 294e4f694f3d2c32982b52abe48d843fd22397fa Mon Sep 17 00:00:00 2001 From: luyunzeng Date: Thu, 25 Dec 2025 14:02:42 +0800 Subject: [PATCH 0662/1315] Fixes an issue where the lockscreen clock would display the incorrect time after a SystemUI restart or hot reload. **Phenomenon:** After a SystemUI process restart that is not a full device reboot, the lockscreen clock (Flex Clock) would display a time based on the GMT/UTC timezone, not the user's local timezone. For example, on a device in GMT+8 at 4:00 PM (16:00), the lockscreen clock would incorrectly show 8:00 AM. The status bar clock would also briefly show the incorrect time but would quickly correct itself. The lockscreen clock, however, would remain incorrect until the next full device reboot. **Root Cause:** The issue was a race condition during SystemUI initialization. 1. On process start, the default timezone is GMT. 2. An `ACTION_TIMEZONE_CHANGED` broadcast is sent shortly after to propagate the correct local timezone. 3. The status bar clock uses a direct `BroadcastReceiver`, which is lightweight and successfully catches this initial broadcast. 4. The lockscreen clock, however, receives its timezone updates via `ClockEventController` -> `KeyguardUpdateMonitor`. This path is slower to initialize and would often miss the first critical `ACTION_TIMEZONE_CHANGED` broadcast. 5. As a result, the lockscreen clock's `TimeKeeper` would remain stuck in the default GMT timezone. **Solution:** This commit makes the lockscreen clock's timezone handling more robust by mirroring the proactive approach of the status bar clock. In `ClockEventController.kt`, within the `registerListeners()` method, we now proactively fetch the system's default timezone and dispatch it to the current clock. This ensures that even if the initial broadcast is missed, the clock's timezone is synchronized as soon as its event system is ready, thus resolving the race condition. Change-Id: Ib0793850570cba7a8922b94f8c2e555939ed85f0 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/keyguard/ClockEventController.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index f579bc4f5d71..1ff18673b180 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -482,6 +482,10 @@ constructor( localeBroadcastReceiver, IntentFilter(Intent.ACTION_LOCALE_CHANGED), ) + + // Proactively update timezone on listener registration to avoid race conditions on startup. + clock?.events?.onTimeZoneChanged(IcuTimeZone.getDefault()) + configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) From f50196af2e74bb6d4417fbacfa7d393bd74f9c1e Mon Sep 17 00:00:00 2001 From: "ot_shiliang.wang@mediatek.com" Date: Thu, 23 Oct 2025 17:31:50 +0800 Subject: [PATCH 0663/1315] Add Fix Bluetooth status display issue after power cycle [Description] Bluetooth remote shows disconnected status on Dashboard after successful reconnection post power cycle. [Root Cause] Service connection listeners were not called, causing incorrect status display. [Solution] - Add calls to service connection and disconnection listeners in HidDeviceProfile and HidProfile. [Test Report] build pass CR-Id: DTV04734746 Change-Id: Ib8b02c5705864575ac4f4bdbd607b688804cfe20 (cherry picked from commit 1757f11a89364e00a0db7ca1d3aa7768998bd6ac) Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/settingslib/bluetooth/HidDeviceProfile.java | 2 ++ .../src/com/android/settingslib/bluetooth/HidProfile.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java index 5468efbdbd3e..29e1424f205d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java @@ -76,10 +76,12 @@ public void onServiceConnected(int profile, BluetoothProfile proxy) { device.refresh(); } mIsProfileReady = true; + mProfileManager.callServiceConnectedListeners(); } public void onServiceDisconnected(int profile) { mIsProfileReady = false; + mProfileManager.callServiceDisconnectedListeners(); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java index b849d44622b2..df5eca51fccb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java @@ -70,10 +70,12 @@ public void onServiceConnected(int profile, BluetoothProfile proxy) { device.refresh(); } mIsProfileReady=true; + mProfileManager.callServiceConnectedListeners(); } public void onServiceDisconnected(int profile) { mIsProfileReady=false; + mProfileManager.callServiceDisconnectedListeners(); } } From 47ee5aa54570b6a8b8470b4626db74f47f68dfed Mon Sep 17 00:00:00 2001 From: luanzhuang Date: Tue, 21 Oct 2025 19:38:08 +0800 Subject: [PATCH 0664/1315] totalScanTimeMs and totalWifiLockTimeMs may be 0, causing an ArithmeticException and triggering a reboot. Test: monkey test Flag: EXEMPT bugfix Bug: 453934442 Change-Id: I68fad8b85e19547a748563592653dc03ca9cf0a4 Signed-off-by: luanzhuang Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/power/stats/BatteryStatsImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index f303ceec39bd..bf84c3549a73 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -11965,11 +11965,11 @@ private void updateWifiBatteryStats(WifiActivityEnergyInfo info, // // This means that we may have apps that transmitted/received packets not be // blamed for this, but this is fine as scans are relatively more expensive. - if (totalScanTimeMs > rxTimeMs) { + if (totalScanTimeMs != 0 && totalScanTimeMs > rxTimeMs) { scanRxTimeSinceMarkMs = (rxTimeMs * scanRxTimeSinceMarkMs) / totalScanTimeMs; } - if (totalScanTimeMs > txTimeMs) { + if (totalScanTimeMs != 0 && totalScanTimeMs > txTimeMs) { scanTxTimeSinceMarkMs = (txTimeMs * scanTxTimeSinceMarkMs) / totalScanTimeMs; } @@ -11993,7 +11993,7 @@ private void updateWifiBatteryStats(WifiActivityEnergyInfo info, final long wifiLockTimeSinceMarkMs = uid.mFullWifiLockTimer.getTimeSinceMarkLocked( elapsedRealtimeMs * 1000) / 1000; - if (wifiLockTimeSinceMarkMs > 0) { + if (wifiLockTimeSinceMarkMs > 0 && totalWifiLockTimeMs != 0) { // Set the new mark so that next time we get new data since this point. uid.mFullWifiLockTimer.setMark(elapsedRealtimeMs); From d9e1004f8ebeab8691411f8661f82edec1bc5571 Mon Sep 17 00:00:00 2001 From: Naga Venkata Durga Ashok Mutyala Date: Tue, 4 May 2021 10:44:55 +0000 Subject: [PATCH 0665/1315] Remove Duplicate WIFI_DISPLAY permission entry Test: Manual Change-Id: Iddc3583d6c5529daedbacdb10aa01b8505286be5 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 0dfc91f08f82..d4ab12d62e5a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -48,7 +48,6 @@ - From 7ddb88dc7c838acc7ceba98a50b4ddc6c78063dd Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 11 Jan 2026 17:57:32 +0530 Subject: [PATCH 0666/1315] SystemUI: Fix default font feature on lockscreen Fixes: https://github.com/crdroidandroid/issue_tracker/issues/840 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../customization/clocks/view/DigitalClockTextView.kt | 2 +- .../systemui/shared/clocks/DefaultClockController.kt | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/view/DigitalClockTextView.kt b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/view/DigitalClockTextView.kt index 2c421da1f780..56d7e8ba403d 100644 --- a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/view/DigitalClockTextView.kt +++ b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/view/DigitalClockTextView.kt @@ -565,7 +565,7 @@ abstract class DigitalClockTextView( this.textStyle = textStyle lockScreenPaint.strokeJoin = Paint.Join.ROUND lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(fontVariations.lockscreen) - lockScreenPaint.fontFeatureSettings = if (isLargeClock) "" else "pnum" + lockScreenPaint.fontFeatureSettings = if (isLargeClock) "tnum" else "pnum" typeface = lockScreenPaint.typeface textStyle.lineHeight?.let { lineHeight = it.roundToInt() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 5597b2f19956..58873179c87f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -175,7 +175,9 @@ class DefaultClockController( override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} } - open fun recomputePadding(targetRegion: Rect?) {} + open fun recomputePadding(targetRegion: Rect?) { + view.setFontFeatureSettings("pnum") + } private fun getAodColor(): Int { return if (ambientAod()) { @@ -200,7 +202,9 @@ class DefaultClockController( animations = LargeClockAnimations(view, 0f, 0f) } - override fun recomputePadding(targetRegion: Rect?) {} + override fun recomputePadding(targetRegion: Rect?) { + view.setFontFeatureSettings("tnum") + } /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */ fun offsetGlyphsForStepClockAnimation(args: ClockPositionAnimationArgs) { From c1ace56bb04cd744a3ffcd869de2c8e324c282c0 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 28 Jan 2026 01:26:00 +0530 Subject: [PATCH 0667/1315] SystemUI: Add status bar battery disable toggle Fixes: https://github.com/crdroidandroid/issue_tracker/issues/870 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/xml/status_bar_prefs.xml | 7 ++-- .../HomeStatusBarIconBlockListInteractor.kt | 34 +++++++++++++++---- .../shared/ui/composable/StatusBarRoot.kt | 14 ++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/res/xml/status_bar_prefs.xml b/packages/SystemUI/res/xml/status_bar_prefs.xml index d04295df6e17..98ecb35385c1 100644 --- a/packages/SystemUI/res/xml/status_bar_prefs.xml +++ b/packages/SystemUI/res/xml/status_bar_prefs.xml @@ -148,11 +148,10 @@ android:key="vpn" android:title="@string/legacy_vpn_name" /> - + android:key="battery" + android:title="@string/battery" /> = secureSettingsRepository.boolSetting(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, false) + private val tunerBlockedIcons: Flow> = + secureSettingsRepository + .stringSetting(StatusBarIconController.ICON_HIDE_LIST) + .map { hideListStr -> + StatusBarIconController.getIconHideList(context, hideListStr).toSet() + } + .distinctUntilChanged() + val iconBlockList: Flow> = - shouldShowVibrateIcon.map { - val defaultSet = defaultBlockedIcons.toMutableSet() + combine(tunerBlockedIcons, shouldShowVibrateIcon) { tunerSet, showVibrate -> + val set = defaultBlockedIcons.toMutableSet() + + // Merge tuner blacklist (this is what hides “clock”, “battery”, etc) + set.addAll(tunerSet) + // It's possible that the vibrate icon was in the default blocklist, so we manually // merge the setting and list - if (it) { - defaultSet.remove(vibrateIconSlot) + if (showVibrate) { + set.remove(vibrateIconSlot) } else { - defaultSet.add(vibrateIconSlot) + set.add(vibrateIconSlot) } - defaultSet.toList() + set.toList() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 23e715dccde8..805692bb5f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -119,6 +119,9 @@ import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.collect /** Factory to simplify the dependency management for [StatusBarRoot] */ @PerDisplaySingleton @@ -548,6 +551,8 @@ fun chipsMaxWidth( return (widthInPx / density).dp } +private const val SLOT_BATTERY = "battery" + /** Create a new [UnifiedBattery] and add it to the end of the system_icons container */ private fun addBatteryComposable( phoneStatusBarView: PhoneStatusBarView, @@ -585,6 +590,15 @@ private fun addBatteryComposable( phoneStatusBarView.findViewById(R.id.system_icons).apply { addView(batteryComposeView, -1) } + + batteryComposeView.repeatWhenAttached { + statusBarViewModel.iconBlockList + .map { blocked -> blocked.contains(SLOT_BATTERY) } + .distinctUntilChanged() + .collect { isBlocked -> + batteryComposeView.visibility = if (isBlocked) View.GONE else View.VISIBLE + } + } } /** From 722192c8d42a158b4f983568c4a3fb274d72217f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 28 Jan 2026 09:22:46 +0530 Subject: [PATCH 0668/1315] SystemUI: FlexClock: Read font from config_clockFontFamily * Requires clock_reactive_variants enabled. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../shared/clocks/DefaultClockProvider.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index ae427839f7c6..1fd94069884a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -80,7 +80,7 @@ constructor( val clockSettings = settings.copy(axes = ClockAxisStyle(fontAxes)) val typefaceCache = TypefaceCache(buffers.infraMessageBuffer, NUM_CLOCK_FONT_ANIMATION_STEPS) { - FLEX_TYPEFACE + getDefaultClockFontFamily() } FlexClockController( ClockContext( @@ -135,14 +135,24 @@ constructor( } } + private fun getDefaultClockFontFamily(): Typeface { + val resId = resources.getIdentifier( + "config_clockFontFamily", + "string", + "android" + ) + val family = if (resId != 0) { + resources.getString(resId) + } else { + "google-sans-flex-clock" + } + + return Typeface.create(family, Typeface.NORMAL) + } + companion object { // 750ms @ 120hz -> 90 frames of animation // In practice, 30 looks good enough and limits our memory usage const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 - - val FLEX_TYPEFACE by lazy { - // TODO(b/364680873): Move constant to config_clockFontFamily when shipping - Typeface.create("google-sans-flex-clock", Typeface.NORMAL) - } } } From 9675f15b022cfcb3d36cd351a9243fb7a03adc93 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 28 Jan 2026 22:05:44 +0530 Subject: [PATCH 0669/1315] SystemUI: Handle secondary click for mobile data tile * Similar to how we do this wifi tile, let user toggle data without showing dialog. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/qs/tiles/MobileDataTile.kt | 8 ++++++++ .../MobileDataTileUserActionInteractor.kt | 14 ++++++++++++++ .../impl/cell/ui/mapper/MobileDataTileMapper.kt | 6 +++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt index 9126d5c50f02..c3933ea6887a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt @@ -97,6 +97,10 @@ constructor( lifecycle.coroutineScope.launch { userActionInteractor.handleClick(expandable) } } + override fun handleSecondaryClick(expandable: Expandable?) { + userActionInteractor.handleSecondaryClick(expandable) + } + override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent override fun handleUpdateState(state: QSTile.State?, arg: Any?) { @@ -117,6 +121,10 @@ constructor( label = tileState.label contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName + handlesSecondaryClick = + tileState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK) + handlesLongClick = + tileState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileUserActionInteractor.kt index 3433d091cb2d..e31999847c11 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileUserActionInteractor.kt @@ -55,6 +55,9 @@ constructor( is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(input.action.expandable, longClickIntent) } + is QSTileUserAction.ToggleClick -> { + handleSecondaryClick(input.action.expandable) + } else -> {} } } @@ -70,6 +73,17 @@ constructor( } } + fun handleSecondaryClick(expandable: Expandable?) { + val activeRepo = mobileConnectionsRepository.activeMobileDataRepository.value ?: return + // If mobile data is disabled, turn it on. + if (!activeRepo.dataEnabled.value) { + activeRepo.setDataEnabled(true) + } else { + // Otherwise, just turn it off. + activeRepo.setDataEnabled(false) + } + } + private fun showEnableConfirmationDialog(expandable: Expandable?) { val dialog: SystemUIDialog = systemUIDialogFactory.create() dialog.setTitle(context.getString(R.string.mobile_data_enable_title)) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/ui/mapper/MobileDataTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/ui/mapper/MobileDataTileMapper.kt index 2f582215d7c8..89eb3bf21405 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/ui/mapper/MobileDataTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/ui/mapper/MobileDataTileMapper.kt @@ -73,6 +73,10 @@ constructor( QSTileState.ActivationState.UNAVAILABLE } supportedActions = - setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + setOf( + QSTileState.UserAction.CLICK, + QSTileState.UserAction.LONG_CLICK, + QSTileState.UserAction.TOGGLE_CLICK, + ) } } From a5dc96e624ab3a758cd209b7ab3f8e1ddce58960 Mon Sep 17 00:00:00 2001 From: Lup Gabriel Date: Sat, 31 Jan 2026 21:24:50 +0200 Subject: [PATCH 0670/1315] SmartSpace: Mark strings as non-translatable Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/matrixx_strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 6b62d5a17734..c7e07fe96296 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -57,11 +57,11 @@ Next alarm at %s Page %1$d of %2$d %1$s is on - %1$s, %2$s + %1$s, %2$s No title At a glance QR code - EEEMMMd + EEEMMMd View From b28226b8d4d645593b2b9dd29f5f8922cd1549e2 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 1 Feb 2026 12:02:56 +0530 Subject: [PATCH 0671/1315] SystemUI: Allow ambient wallpaper on pulsing Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../wallpapers/data/repository/WallpaperRepository.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index efb63dadaaf5..028fbc868503 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -127,17 +127,13 @@ constructor( Settings.Secure.DOZE_ALWAYS_ON_WALLPAPER_ENABLED, ) .onStart { emit(Unit) }, - secureSettings - .observerFlow(UserHandle.USER_ALL, Settings.Secure.DOZE_ALWAYS_ON) - .onStart { emit(Unit) }, configurationInteractor.onAnyConfigurationChange, - ::Triple, + ::Pair, ) .map { - val aodEnabled = secureSettings.getInt(Settings.Secure.DOZE_ALWAYS_ON, 0) == 1 val wallpaperEnabled = secureSettings.getInt(Settings.Secure.DOZE_ALWAYS_ON_WALLPAPER_ENABLED, 0) == 1 - aodEnabled && wallpaperEnabled && configEnabled() && ambientAod() + wallpaperEnabled && configEnabled() && ambientAod() } .flowOn(bgDispatcher) From a3a0eb842f81d91ec40a09beaec4fd7e3474b32e Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 14 Jan 2026 21:25:04 +0000 Subject: [PATCH 0672/1315] fix an upstream infinite loop bug in ProtoFieldFilter.skipBytes() Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/util/proto/ProtoFieldFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/util/proto/ProtoFieldFilter.java b/core/java/android/util/proto/ProtoFieldFilter.java index c3ae106b68f8..3a0e674ab9b6 100644 --- a/core/java/android/util/proto/ProtoFieldFilter.java +++ b/core/java/android/util/proto/ProtoFieldFilter.java @@ -307,7 +307,7 @@ private void skipBytes(InputStream in, long n) throws IOException { while (bytesRemaining > 0) { int bytesToRead = (int) Math.min(bytesRemaining, mBuffer.length); int bytesRead = in.read(mBuffer, 0, bytesToRead); - if (bytesRemaining < 0) { + if (bytesRead < 0) { throw new IOException("EOF while skipping bytes"); } bytesRemaining -= bytesRead; From 271de10439097c15b99c6e72308cfd34544b254a Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Gandrothu Date: Tue, 13 Jan 2026 06:15:19 -0800 Subject: [PATCH 0673/1315] Home screen is stuck at "phone is starting" screen The ActivityTaskManager: activityIdleInternal call for the UserState information is added to mStartingUsers in failure case and is getting called before startUserSwitchTransition , whereas in success case it is getting called after startUserSwitchTransition of user 10 is added and in next idle timeout uc_finish_user_boot of user 10 is getting triggered for finishUserBoot sequence. Change-Id: I44e38b55cba86900d19b74ab9b8552ad5146c3ca Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/WindowManagerService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 20a67fa90160..c3bad3eaab17 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4033,6 +4033,12 @@ public void startUserSwitchTransition(@UserIdInt int oldUserId, @UserIdInt int n switchUserInternal(newUserId); moveUserToForeground(newUserId, uss, "startUserSwitchTransition"); }; + // Call ActivityIdle in case its already invoked, to ensure mStartingUsers is + //handled. + mRoot.mTaskSupervisor.activityIdleInternal(null /* idleActivity */, + false /* fromTimeout */,true /* processPausingActivities */, + null /* configuration */); + final TransitionController controller = mAtmService.getTransitionController(); if (!controller.isShellTransitionsEnabled()) { From cd484a0faaf0616241d7b3888c97b85aa1dea870 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 6 Jan 2026 10:53:28 +0000 Subject: [PATCH 0674/1315] add workaround for UsageStatsDatabase OOM system_server crash Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/usage/UsageStatsDatabase.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java index ba33eab5331a..a2c613268860 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -1105,7 +1105,12 @@ private void readLocked(AtomicFile file, IntervalStats statsOut, boolean skipEve Slog.wtf(TAG, "Reading UsageStats as XML; current database version: " + mCurrentVersion); } - readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData, skipEvents); + try { + readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData, skipEvents); + } catch (OutOfMemoryError e) { + file.delete(); + Slog.e(TAG, "unable to parse " + file, e); + } } /** From 7ed68a986c8fae25b6cb2ab1b098457c9c55cc38 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 6 Jan 2026 10:53:55 +0000 Subject: [PATCH 0675/1315] add workaround for WindowContext.finalize() system_server crash Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/window/WindowContext.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/java/android/window/WindowContext.java b/core/java/android/window/WindowContext.java index 806ad2172a5f..685270f9f018 100644 --- a/core/java/android/window/WindowContext.java +++ b/core/java/android/window/WindowContext.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; +import android.app.ActivityThread; import android.content.ComponentCallbacks; import android.content.ComponentCallbacksController; import android.content.Context; @@ -142,7 +143,7 @@ public Object getSystemService(String name) { @Override protected void finalize() throws Throwable { try { - release(); + getMainThreadHandler().post(this::release); } finally { super.finalize(); } From 36c517f937a704e4b4e1f49f1fb3b95ad16aa797 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 24 Dec 2025 16:04:32 +0200 Subject: [PATCH 0676/1315] fix system_server crash in NotificationHistoryProtoHelper Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../NotificationHistoryProtoHelper.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java index 1f5d8fcd8843..f5b77445b7c8 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java @@ -279,13 +279,15 @@ private static void writeNotification(ProtoOutputStream proto, proto.write(Notification.CHANNEL_NAME, notification.getChannelName()); } } - final int channelIdIndex = Arrays.binarySearch(stringPool, notification.getChannelId()); - if (channelIdIndex >= 0) { - proto.write(Notification.CHANNEL_ID_INDEX, channelIdIndex + 1); - } else { - Slog.w(TAG, "notification channel id (" + notification.getChannelId() - + ") not found in string cache"); - proto.write(Notification.CHANNEL_ID, notification.getChannelId()); + if (!TextUtils.isEmpty(notification.getChannelId())) { + final int channelIdIndex = Arrays.binarySearch(stringPool, notification.getChannelId()); + if (channelIdIndex >= 0) { + proto.write(Notification.CHANNEL_ID_INDEX, channelIdIndex + 1); + } else { + Slog.w(TAG, "notification channel id (" + notification.getChannelId() + + ") not found in string cache"); + proto.write(Notification.CHANNEL_ID, notification.getChannelId()); + } } if (!TextUtils.isEmpty(notification.getConversationId())) { final int conversationIdIndex = Arrays.binarySearch( From a2a4a9fd7a86d46a30855f46d4be0bb8a7f481b9 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 17 Dec 2025 11:40:10 +0200 Subject: [PATCH 0677/1315] fix system_server crash in NotificationHistory.getPooledStringsToWrite() See https://github.com/GrapheneOS/os-issue-tracker/issues/6815 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/NotificationHistory.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java index bbaef65d4ebe..ba3e13ff7e89 100644 --- a/core/java/android/app/NotificationHistory.java +++ b/core/java/android/app/NotificationHistory.java @@ -314,11 +314,15 @@ public void poolStringsFromNotifications() { mStringsToWrite.clear(); for (int i = 0; i < mNotificationsToWrite.size(); i++) { final HistoricalNotification notification = mNotificationsToWrite.get(i); - mStringsToWrite.add(notification.getPackage()); + if (!TextUtils.isEmpty(notification.getPackage())) { + mStringsToWrite.add(notification.getPackage()); + } if (!TextUtils.isEmpty(notification.getChannelName())) { mStringsToWrite.add(notification.getChannelName()); } - mStringsToWrite.add(notification.getChannelId()); + if (!TextUtils.isEmpty(notification.getChannelId())) { + mStringsToWrite.add(notification.getChannelId()); + } if (!TextUtils.isEmpty(notification.getConversationId())) { mStringsToWrite.add(notification.getConversationId()); } From 24536c3acdb13c9e075fdabf34bb3144d3724d65 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 23 Jan 2026 13:40:47 +0000 Subject: [PATCH 0678/1315] add workaround for system_server startUserInBackgroundTemporarily crash See https://github.com/GrapheneOS/os-issue-tracker/issues/6790 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- services/core/java/com/android/server/am/UserController.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 35a1daf1b7c0..5e20b40d297c 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -1978,6 +1978,11 @@ boolean startProfile(@UserIdInt int userId, boolean evenWhenDisabled, } boolean startUserInBackgroundTemporarily(@UserIdInt int userId, int durationSecs) { + synchronized (mLock) { + if (!mReady) { + return false; + } + } return startUserNoChecks(userId, Display.DEFAULT_DISPLAY, USER_START_MODE_BACKGROUND, durationSecs, /* unlockListener= */ null); } From 33236bf24dc32c2d0e6ccf9daa8b3156017aac5d Mon Sep 17 00:00:00 2001 From: lijilou Date: Mon, 1 Dec 2025 17:31:57 +0800 Subject: [PATCH 0679/1315] Minor code optimization for FileObserver. fast return when the observer is null. Bug: none Flag: EXEMPT minor optimization Change-Id: Idb81dd09fe71ac80e839af51e63fa1ef950d2d03 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/os/FileObserver.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index 9e52075938ba..9ed6228fb218 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -161,6 +161,7 @@ public void onEvent(int wfd, @NotifyEventType int mask, String path) { observer = (FileObserver) weak.get(); if (observer == null) { mRealObservers.remove(wfd); + return; } } } From 9b8c31396df721f3cddb1b9ba637887669e2a0ac Mon Sep 17 00:00:00 2001 From: sumiyawang Date: Mon, 26 Jan 2026 19:55:13 +0800 Subject: [PATCH 0680/1315] wm: Only close system dialogs for visible windows Previously, closeSystemDialogs() iterated over all windows with surfaces. This caused binder transactions to be sent to invisible or background windows. If a background process was frozen or stuck (e.g., on mmap_sem), holding mGlobalLock while calling into it would freeze the system. Invisible windows do not need to have their dialogs closed from a user perspective. This CL restricts the cleanup to w.isVisible(). This avoids interacting with stuck background processes and prevents the lock contention. Test: Manual Change-Id: I07058fb958ddd42e6ee2a28d67cd345032f3de5b Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/RootWindowContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 79f33fa94885..d4d46be9fa80 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -425,7 +425,7 @@ public boolean test(Task task) { } private final Consumer mCloseSystemDialogsConsumer = w -> { - if (w.mHasSurface) { + if (w.isVisible()) { try { w.mClient.closeSystemDialogs(mCloseSystemDialogsReason); } catch (RemoteException e) { From 9901341f753d5da948feeac89318e013057dbf93 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 2 Feb 2026 00:28:11 +0530 Subject: [PATCH 0681/1315] base: Use SingleKeyRule for assist long press Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/policy/PhoneWindowManager.java | 99 +++++++++++++------ 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 32971ef58553..7ea105a404c9 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -697,7 +697,6 @@ public void onDrawn() { private boolean mKeyguardOccludedChanged; boolean mMenuPressed; - boolean mAssistPressed; Intent mHomeIntent; Intent mCarDockIntent; Intent mDeskDockIntent; @@ -1859,6 +1858,38 @@ private void appSwitchLongPress() { } } + private void assistPress() { + if (!keyguardOn() && mAssistPressAction != Action.NOTHING) { + if (mAssistPressAction != Action.APP_SWITCH) { + cancelPreloadRecentApps(); + } + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ASSIST, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD); + + performKeyAction(mAssistPressAction, event, + AssistUtils.INVOCATION_TYPE_ASSIST_BUTTON); + } + } + + private void assistLongPress() { + if (!keyguardOn() && mAssistLongPressAction != Action.NOTHING) { + if (mAssistLongPressAction != Action.APP_SWITCH) { + cancelPreloadRecentApps(); + } + + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ASSIST, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD); + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Assist - Long Press"); + performKeyAction(mAssistLongPressAction, event, + AssistUtils.INVOCATION_TYPE_ASSIST_BUTTON); + } + } + private void sleepPress() { if (mShortPressOnSleepBehavior == SHORT_PRESS_SLEEP_GO_TO_SLEEP_AND_GO_HOME) { launchHomeFromHotKey(DEFAULT_DISPLAY, false /* awakenDreams */, @@ -3111,6 +3142,35 @@ void onKeyGesture(@NonNull SingleKeyGestureEvent event) { } } + /** + * Rule for single assist key gesture. + */ + private final class AssistKeyRule extends SingleKeyGestureDetector.SingleKeyRule { + AssistKeyRule() { + super(KeyEvent.KEYCODE_ASSIST); + } + + @Override + boolean supportLongPress() { + return mAssistLongPressAction != Action.NOTHING; + } + + @Override + void onKeyGesture(@NonNull SingleKeyGestureEvent event) { + if (event.getAction() != ACTION_COMPLETE) { + return; + } + switch (event.getType()) { + case SINGLE_KEY_GESTURE_TYPE_PRESS: + assistPress(); + break; + case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS: + assistLongPress(); + break; + } + } + } + /** * Rule for single stem primary key gesture. */ @@ -3327,6 +3387,7 @@ private void initSingleKeyGestureRules(Looper looper) { mSingleKeyGestureDetector.addRule(new BackKeyRule()); mSingleKeyGestureDetector.addRule(new StylusTailButtonRule()); mSingleKeyGestureDetector.addRule(new AppSwitchKeyRule()); + mSingleKeyGestureDetector.addRule(new AssistKeyRule()); } private void updateKeyAssignments() { @@ -5683,37 +5744,11 @@ && isWakeKeyWhenScreenOff(keyCode)) { break; } case KeyEvent.KEYCODE_ASSIST: { - if (keyguardOn()) { - break; - } - if (down) { - if (mAssistPressAction == Action.APP_SWITCH - || mAssistLongPressAction == Action.APP_SWITCH) { - preloadRecentApps(); - } - if (event.getRepeatCount() == 0) { - mAssistPressed = true; - } else if (longPress) { - if (mAssistLongPressAction != Action.NOTHING) { - if (mAssistLongPressAction != Action.APP_SWITCH) { - cancelPreloadRecentApps(); - } - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, - "Assist - Long Press"); - performKeyAction(mAssistLongPressAction, event, - AssistUtils.INVOCATION_TYPE_ASSIST_BUTTON); - mAssistPressed = false; - } - } - } else { - if (mAssistPressed) { - if (mAssistPressAction != Action.APP_SWITCH) { - cancelPreloadRecentApps(); - } - mAssistPressed = false; - if (!canceled) { - performKeyAction(mAssistPressAction, event, - AssistUtils.INVOCATION_TYPE_ASSIST_BUTTON); + if (!keyguardOn()) { + if (down) { + if (mAssistPressAction == Action.APP_SWITCH + || mAssistLongPressAction == Action.APP_SWITCH) { + preloadRecentApps(); } } } From 49520e8c01789c8f9363423591e0394ba9a82fb9 Mon Sep 17 00:00:00 2001 From: "Christopher R. Palmer" Date: Mon, 4 Jan 2016 05:18:31 -0500 Subject: [PATCH 0682/1315] aapt: Speed up the style pruning Prior to this commit it removed each style one by one which causes the vector to repeatedly be shrunk and reallocated and copied (aka either quadratic or NlogN in the number of items removed). This commit simply makes it remove them all at once to make it linear instead. Change-Id: I541d151675ab19f37d9de1e7a323104d0d3b3c63 Signed-off-by: Pranav Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- tools/aapt/StringPool.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp index b2e48bd74e8a..542f61f514c9 100644 --- a/tools/aapt/StringPool.cpp +++ b/tools/aapt/StringPool.cpp @@ -338,14 +338,18 @@ void StringPool::sortByConfig() // Now trim any entries at the end of the new style array that are // not needed. - for (ssize_t i=newEntryStyleArray.size()-1; i>=0; i--) { + ssize_t i; + for (i=newEntryStyleArray.size()-1; i>=0; i--) { const entry_style& style = newEntryStyleArray[i]; if (style.spans.size() > 0) { // That's it. break; } - // This one is not needed; remove. - newEntryStyleArray.removeAt(i); + } + + ssize_t nToRemove=newEntryStyleArray.size()-(i+1); + if (nToRemove) { + newEntryStyleArray.removeItemsAt(i+1, nToRemove); } // All done, install the new data structures and upate mValues with From 66d7f52726de58d686cb32a3ba90432e9d2de2d4 Mon Sep 17 00:00:00 2001 From: "Christopher R. Palmer" Date: Mon, 4 Jan 2016 20:29:02 -0500 Subject: [PATCH 0683/1315] aapt: Use a std::map instead of a SortedVector Android's SortedVectorImpl uses arrays that it must insert into the middle of. Each insertion is O(N) time because it must move on average half the elements of the array to make room for the new element. That is, O(N^2) time to build this sorted vector. std::map on the other hand normally uses red/black trees and has a cost of NlogN to add N elements to it. Change-Id: I5da0363ba806ab615b2aad0fb2a43ef9a9bec327 Signed-off-by: Pranav Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- tools/aapt/StringPool.cpp | 21 +++++++++++---------- tools/aapt/StringPool.h | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp index 542f61f514c9..1fa73bd36058 100644 --- a/tools/aapt/StringPool.cpp +++ b/tools/aapt/StringPool.cpp @@ -116,7 +116,7 @@ int StringPool::entry::compare(const entry& o) const { } StringPool::StringPool(bool utf8) : - mUTF8(utf8), mValues(-1) + mUTF8(utf8) { } @@ -133,8 +133,8 @@ ssize_t StringPool::add(const String16& value, const Vector& s ssize_t StringPool::add(const String16& value, bool mergeDuplicates, const String8* configTypeName, const ResTable_config* config) { - ssize_t vidx = mValues.indexOfKey(value); - ssize_t pos = vidx >= 0 ? mValues.valueAt(vidx) : -1; + auto it = mValues.find(value); + ssize_t pos = it != mValues.end() ? it->second : -1; ssize_t eidx = pos >= 0 ? mEntryArray.itemAt(pos) : -1; if (eidx < 0) { eidx = mEntries.add(entry(value)); @@ -181,21 +181,21 @@ ssize_t StringPool::add(const String16& value, } } - const bool first = vidx < 0; + const bool first = (it == mValues.end()); const bool styled = (pos >= 0 && (size_t)pos < mEntryStyleArray.size()) ? mEntryStyleArray[pos].spans.size() : 0; if (first || styled || !mergeDuplicates) { pos = mEntryArray.add(eidx); if (first) { - vidx = mValues.add(value, pos); + mValues[value] = pos; } entry& ent = mEntries.editItemAt(eidx); ent.indices.add(pos); } if (kIsDebug) { - printf("Adding string %s to pool: pos=%zd eidx=%zd vidx=%zd\n", - String8(value).c_str(), pos, eidx, vidx); + printf("Adding string %s to pool: pos=%zd eidx=%zd\n", + String8(value).c_str(), pos, eidx); } return pos; @@ -360,7 +360,7 @@ void StringPool::sortByConfig() mValues.clear(); for (size_t i=0; i* StringPool::offsetsForString(const String16& val) const { - ssize_t pos = mValues.valueFor(val); - if (pos < 0) { + auto it = mValues.find(val); + if (it == mValues.end()) { return NULL; } + ssize_t pos = it->second; return &mEntries[mEntryArray[pos]].indices; } diff --git a/tools/aapt/StringPool.h b/tools/aapt/StringPool.h index 253bcca4f507..e17c865e39a6 100644 --- a/tools/aapt/StringPool.h +++ b/tools/aapt/StringPool.h @@ -18,6 +18,7 @@ #include #include #include +#include using namespace android; @@ -172,7 +173,7 @@ class StringPool // Unique set of all the strings added to the pool, mapped to // the first index of mEntryArray where the value was added. - DefaultKeyedVector mValues; + std::map mValues; // This array maps from the original position a string was placed at // in mEntryArray to its new position after being sorted with sortByConfig(). Vector mOriginalPosToNewPos; From 94439ee0e82d13df100d2dd8368879d4bc8fc640 Mon Sep 17 00:00:00 2001 From: Chirayu Desai Date: Sun, 5 May 2013 19:35:45 +0530 Subject: [PATCH 0684/1315] aapt: add check for untranslatable "string-array"s Change-Id: Id884af0505c0bcdfa20b400fcdd54f699f8ef38f Signed-off-by: Chirayu Desai Signed-off-by: Pranav Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- tools/aapt/ResourceTable.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index 4d833aa213a1..5934e2ffd4b8 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -1472,6 +1472,11 @@ status_t compileResourceFile(Bundle* bundle, } } } else if (strcmp16(block.getElementName(&len), string_array16.c_str()) == 0) { + // Note the existence and locale of every string array we process + char rawLocale[RESTABLE_MAX_LOCALE_LEN]; + curParams.getBcp47Locale(rawLocale); + String8 locale(rawLocale); + String16 name; // Check whether these strings need valid formats. // (simplified form of what string16 does above) bool isTranslatable = false; @@ -1482,7 +1487,9 @@ status_t compileResourceFile(Bundle* bundle, for (size_t i = 0; i < n; i++) { size_t length; const char16_t* attr = block.getAttributeName(i, &length); - if (strcmp16(attr, formatted16.c_str()) == 0) { + if (strcmp16(attr, name16.c_str()) == 0) { + name.setTo(block.getAttributeStringValue(i, &length)); + } else if (strcmp16(attr, formatted16.c_str()) == 0) { const char16_t* value = block.getAttributeStringValue(i, &length); if (strcmp16(value, false16.c_str()) == 0) { curIsFormatted = false; @@ -1491,6 +1498,15 @@ status_t compileResourceFile(Bundle* bundle, const char16_t* value = block.getAttributeStringValue(i, &length); if (strcmp16(value, false16.c_str()) == 0) { isTranslatable = false; + // Untranslatable string arrays must only exist + // in the default [empty] locale + if (locale.size() > 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).warning( + "string-array '%s' marked untranslatable but exists" + " in locale '%s'\n", String8(name).string(), + locale.string()); + // hasErrors = localHasErrors = true; + } } } } From fe9c233fb40af7cd177b65ca9fc89df501a00f9c Mon Sep 17 00:00:00 2001 From: Cyber Knight Date: Mon, 26 Jan 2026 21:41:56 +0800 Subject: [PATCH 0685/1315] fixup! aapt: add check for untranslatable "string-array"s - Adapt this to A16/QPR2. Change-Id: Iee4ae74f1f202a48834c9323cda1f7d4bce9f2d3 Signed-off-by: Cyber Knight Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- tools/aapt/ResourceTable.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index 5934e2ffd4b8..541c5d40d4d5 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -1503,8 +1503,8 @@ status_t compileResourceFile(Bundle* bundle, if (locale.size() > 0) { SourcePos(in->getPrintableSource(), block.getLineNumber()).warning( "string-array '%s' marked untranslatable but exists" - " in locale '%s'\n", String8(name).string(), - locale.string()); + " in locale '%s'\n", String8(name).c_str(), + locale.c_str()); // hasErrors = localHasErrors = true; } } From 2c5af54b5831a3cb3d0ed3f8ee5b346879f2b85f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 1 Feb 2026 10:30:20 +0000 Subject: [PATCH 0686/1315] services: Add clo like fling boost Change-Id: Ie2dd7d28beab3892c30954ee07c475b8e89e50bb Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/wm/DisplayPolicy.java | 16 ++++++++++++++++ .../wm/SystemGesturesPointerEventListener.java | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index f93f856923d9..c128b1ae9ba6 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -576,6 +576,22 @@ public void onFling(int duration) { } } + @Override + public void onVerticalFling(int duration) { + if (mService.mPowerManagerInternal != null) { + mService.mPowerManagerInternal.setPowerBoost( + Boost.INTERACTION, duration); + } + } + + @Override + public void onHorizontalFling(int duration) { + if (mService.mPowerManagerInternal != null) { + mService.mPowerManagerInternal.setPowerBoost( + Boost.INTERACTION, duration); + } + } + @Override public void onDebug() { // no-op diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java index a83e8c7a28bd..3b654c23fcd5 100644 --- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java +++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java @@ -446,6 +446,10 @@ public boolean onFling(MotionEvent down, MotionEvent up, if (duration > MAX_FLING_TIME_MILLIS) { duration = MAX_FLING_TIME_MILLIS; } + if(Math.abs(velocityY) >= Math.abs(velocityX)) + mCallbacks.onVerticalFling(duration); + else + mCallbacks.onHorizontalFling(duration); mLastFlingTime = now; mCallbacks.onFling(duration); return true; @@ -458,6 +462,8 @@ interface Callbacks { void onSwipeFromRight(); void onSwipeFromLeft(); void onFling(int durationMs); + void onVerticalFling(int durationMs); + void onHorizontalFling(int durationMs); void onDown(); void onUpOrCancel(); void onMouseHoverAtLeft(); From ded3a3ca0c36685e3243e957ab7dc2e1c3e8d4b5 Mon Sep 17 00:00:00 2001 From: Alex Naidis Date: Thu, 16 Mar 2017 14:59:53 +0100 Subject: [PATCH 0687/1315] ViewConfiguration: Set scroll friction to 0.012 Reduce scroll friction to a better default value which is used in Scroller and Overscroller. Change-Id: I66a7663a18bb80263c51f3d54a2bb1e3fe5d0b4d Signed-off-by: Alex Naidis Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/view/ViewConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index f070d87ef354..afd7abfd7fe4 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -267,7 +267,7 @@ public class ViewConfiguration { * The coefficient of friction applied to flings/scrolls. */ @UnsupportedAppUsage - private static final float SCROLL_FRICTION = 0.015f; + private static final float SCROLL_FRICTION = 0.012f; /** * Max distance in dips to overscroll for edge effects From f4f0056eb56e5a1982a5e47172229ae911ec7061 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 11 Jan 2026 16:29:40 +0000 Subject: [PATCH 0688/1315] SystemUI: Allow Omnijaw weather to show on aod Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/systemui/weather/WeatherViewController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index 5f63515b3e6b..327c2d106b56 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -53,7 +53,7 @@ class WeatherViewController( val weatherEnabled = weatherSettingsFlow.value.weatherEnabled - if (mDozing || !weatherEnabled) { + if (!weatherEnabled) { hideAllViews() OmniJawsClient.get().removeObserver(context, this@WeatherViewController) } else { @@ -110,7 +110,7 @@ class WeatherViewController( Settings.System.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 private fun applyWeatherSettings(settings: WeatherSettings) { - if (mDozing || !settings.weatherEnabled) { + if (!settings.weatherEnabled) { hideAllViews() OmniJawsClient.get().removeObserver(context, this@WeatherViewController) } else { From 2a9c79b93f7110b3675cadf897298ce2f41145cb Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 11 Jan 2026 17:01:03 +0000 Subject: [PATCH 0689/1315] SystemUI: Add grayscale tint to weather icon in AOD - Follow clock style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/weather/WeatherViewController.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index 327c2d106b56..ad1fde2be6fa 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -16,6 +16,8 @@ package com.android.systemui.weather import android.content.Context +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter import android.os.UserHandle import android.provider.Settings import android.view.View @@ -48,19 +50,8 @@ class WeatherViewController( override fun onStateChanged(newState: Int) {} override fun onDozingChanged(dozing: Boolean) { - if (mDozing == dozing) return mDozing = dozing - - val weatherEnabled = weatherSettingsFlow.value.weatherEnabled - - if (!weatherEnabled) { - hideAllViews() - OmniJawsClient.get().removeObserver(context, this@WeatherViewController) - } else { - OmniJawsClient.get().addObserver(context, this@WeatherViewController) - updateWeather() - showAllViews() - } + updateIconTint() } } @@ -122,6 +113,16 @@ class WeatherViewController( override fun weatherUpdated() = updateWeather() + private fun updateIconTint() { + if (mDozing) { + val matrix = ColorMatrix() + matrix.setSaturation(0f) + weatherIcon.colorFilter = ColorMatrixColorFilter(matrix) + } else { + weatherIcon.colorFilter = null + } + } + private fun updateWeather() { if (!weatherSettingsFlow.value.weatherEnabled) { hideAllViews() @@ -135,6 +136,7 @@ class WeatherViewController( weatherIcon.setImageDrawable( OmniJawsClient.get().getWeatherConditionImage(context, info.conditionCode)) + updateIconTint() weatherTemp.text = buildWeatherText(info) weatherTemp.isSelected = true } From 001181992ce0525e8cee182a8613844301fe98a7 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 17 Dec 2025 07:29:13 +0800 Subject: [PATCH 0690/1315] SystemUI: Axion waveform seekbar Change-Id: If2e44ff42d2c3051504c36421267a1a8e7eb08b9 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/layout/media_session_view.xml | 3 +- .../controls/ui/binder/SeekBarObserver.kt | 9 + .../media/controls/ui/view/MediaViewHolder.kt | 2 +- .../media/controls/ui/view/WaveformSeekBar.kt | 339 ++++++++++++++++++ 4 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/controls/ui/view/WaveformSeekBar.kt diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 21c7b550af8c..04f9c1e3b572 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -282,9 +282,8 @@ - (R.id.device_suggestion_button) // Seekbar views - val seekBar = itemView.requireViewById(R.id.media_progress_bar) + val seekBar = itemView.requireViewById(R.id.media_progress_bar) // These views are only shown while the user is actively scrubbing val scrubbingElapsedTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_elapsed_time) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/WaveformSeekBar.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/WaveformSeekBar.kt new file mode 100644 index 000000000000..e49ff9d3284f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/WaveformSeekBar.kt @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.media.controls.ui.view + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.animation.LinearInterpolator +import android.widget.SeekBar +import com.android.systemui.media.MediaSessionManager +import kotlin.math.* + +class WaveformSeekBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.seekBarStyle, +) : SeekBar(context, attrs, defStyleAttr), MediaSessionManager.MediaDataListener { + + private val density = resources.displayMetrics.density + + private val progressPath = Path() + private val backgroundRect = RectF() + private val progressRect = RectF() + + private val trackHeight = 6f * density + private val cornerRadius = 8f * density + private val thumbRadius = 8f * density + + private val waveHeight = 12f * density + private val waveLength = 80f * density + private val minWaveTrackLength = waveLength * 1.0f + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.WHITE + alpha = 77 + } + + private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.WHITE + } + + private val thumbShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.BLACK + alpha = 60 + } + + private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.WHITE + setShadowLayer(4f * density, 0f, 2f * density, Color.argb(80, 0, 0, 0)) + } + + private var wavePhase = 0f + private var waveAmplitudeMultiplier = 0f + private var waveAnimator: ValueAnimator? = null + private var fadeAnimator: ValueAnimator? = null + var isPlaying = false + private set + + init { + thumb = TransparentDrawable() + splitTrack = false + progressDrawable = TransparentDrawable() + } + + fun startWaveAnimation() { + if (isPlaying && waveAnimator?.isRunning == true) return + isPlaying = true + + fadeAnimator?.cancel() + fadeAnimator = ValueAnimator.ofFloat(waveAmplitudeMultiplier, 1f).apply { + duration = 300L + addUpdateListener { + waveAmplitudeMultiplier = it.animatedValue as Float + invalidate() + } + start() + } + + waveAnimator?.cancel() + waveAnimator = ValueAnimator.ofFloat(0f, (2 * Math.PI).toFloat()).apply { + duration = 3500L + repeatCount = ValueAnimator.INFINITE + interpolator = LinearInterpolator() + addUpdateListener { + wavePhase = it.animatedValue as Float + invalidate() + } + start() + } + } + + fun stopWaveAnimation() { + isPlaying = false + waveAnimator?.cancel() + waveAnimator = null + + fadeAnimator?.cancel() + fadeAnimator = ValueAnimator.ofFloat(waveAmplitudeMultiplier, 0f).apply { + duration = 300L + addUpdateListener { + waveAmplitudeMultiplier = it.animatedValue as Float + invalidate() + } + start() + } + } + + fun regenerateWaveform(seed: Long = System.currentTimeMillis()) { + invalidate() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + MediaSessionManager.get().addListener(this) + + if (isPlaying && waveAnimator?.isRunning != true) { + waveAnimator?.cancel() + waveAnimator = ValueAnimator.ofFloat(0f, (2 * Math.PI).toFloat()).apply { + duration = 3500L + repeatCount = ValueAnimator.INFINITE + interpolator = LinearInterpolator() + addUpdateListener { + wavePhase = it.animatedValue as Float + invalidate() + } + start() + } + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + MediaSessionManager.get().removeListener(this) + waveAnimator?.cancel() + } + + override fun onMediaColorsChanged(color: Int) { + post { setWaveformColor(color) } + } + + override fun onDraw(canvas: Canvas) { + val width = width.toFloat() + val height = height.toFloat() + val pLeft = paddingLeft.toFloat() + val pRight = paddingRight.toFloat() + + val drawWidth = width - pLeft - pRight + if (drawWidth <= 0) return + + val centerY = height / 2f + val trackTop = centerY - trackHeight / 2f + val trackBottom = centerY + trackHeight / 2f + + val ratio = if (max > 0) progress.toFloat() / max else 0f + val progressX = pLeft + drawWidth * ratio + + backgroundRect.set(pLeft, trackTop, pLeft + drawWidth, trackBottom) + canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) + + if (progressX > pLeft) { + if (waveAmplitudeMultiplier > 0.01f) { + drawWaveProgress(canvas, pLeft, progressX, centerY, trackTop, trackBottom) + } else { + progressRect.set(pLeft, trackTop, progressX, trackBottom) + canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint) + } + } + canvas.drawCircle(progressX, centerY + 2 * density, thumbRadius, thumbShadowPaint) + canvas.drawCircle(progressX, centerY, thumbRadius, thumbPaint) + } + + private fun drawWaveProgress( + canvas: Canvas, + startX: Float, + endX: Float, + centerY: Float, + trackTop: Float, + trackBottom: Float + ) { + progressPath.reset() + + val progressLength = endX - startX + + val minLength = waveLength * 0.8f + val lengthRatio = (progressLength / minLength).coerceIn(0f, 1f) + val progressLengthFactor = (sqrt(lengthRatio) * 0.7f + 0.3f).coerceIn(0.3f, 1f) + + val edgeRadius = (trackHeight / 2f).coerceAtMost(cornerRadius) + + progressPath.moveTo(startX + edgeRadius, trackBottom) + + progressPath.arcTo( + startX, trackTop, + startX + edgeRadius * 2, trackBottom, + 90f, 90f, false + ) + + val waveStartX = startX + edgeRadius + val firstWaveY = calculateWaveY(waveStartX, waveStartX, endX, trackTop, progressLengthFactor) + progressPath.quadTo(startX, trackTop, waveStartX, firstWaveY) + + drawSmoothWave(waveStartX, endX, trackTop, progressLengthFactor) + + progressPath.lineTo(endX, trackTop) + progressPath.lineTo(endX, trackBottom) + progressPath.close() + + canvas.drawPath(progressPath, progressPaint) + } + + private fun calculateWaveY( + x: Float, + waveStartX: Float, + waveEndX: Float, + trackTop: Float, + progressLengthFactor: Float + ): Float { + val totalDist = waveEndX - waveStartX + if (totalDist <= 0) return trackTop + + val waveProgress = (x - waveStartX) / waveLength + val sinValue = sin(waveProgress * 2 * PI.toFloat() + wavePhase) + + val normalizedWave = (sinValue + 1f) / 2f + + val distFromStart = x - waveStartX + val distFromEnd = waveEndX - x + val taperZone = waveLength * 0.4f + + val envelope = when { + totalDist < taperZone * 2 -> { + val t = distFromStart / totalDist + 4f * t * (1f - t) + } + distFromStart < taperZone -> { + val t = distFromStart / taperZone + t * t * (3f - 2f * t) + } + distFromEnd < taperZone -> { + val t = distFromEnd / taperZone + t * t * (3f - 2f * t) + } + else -> 1f + }.coerceIn(0f, 1f) + + return trackTop - (normalizedWave * waveHeight * envelope * waveAmplitudeMultiplier * progressLengthFactor) + } + + private fun drawSmoothWave( + startX: Float, + endX: Float, + trackTop: Float, + progressLengthFactor: Float + ) { + if (endX <= startX) return + + val step = 2f * density + val points = mutableListOf>() + + var x = startX + while (x <= endX) { + val y = calculateWaveY(x, startX, endX, trackTop, progressLengthFactor) + points.add(Pair(x, y)) + x += step + } + + if (points.isEmpty() || points.last().first < endX) { + val y = calculateWaveY(endX, startX, endX, trackTop, progressLengthFactor) + points.add(Pair(endX, y)) + } + + if (points.size < 2) { + progressPath.lineTo(endX, trackTop) + return + } + + for (i in 0 until points.size - 1) { + val p0 = if (i > 0) points[i - 1] else points[i] + val p1 = points[i] + val p2 = points[i + 1] + val p3 = if (i + 2 < points.size) points[i + 2] else points[i + 1] + + val tension = 0.5f + + val cp1x = p1.first + (p2.first - p0.first) * tension / 3f + val cp1y = p1.second + (p2.second - p0.second) * tension / 3f + val cp2x = p2.first - (p3.first - p1.first) * tension / 3f + val cp2y = p2.second - (p3.second - p1.second) * tension / 3f + + progressPath.cubicTo(cp1x, cp1y, cp2x, cp2y, p2.first, p2.second) + } + } + + + fun setWaveformColor(color: Int) { + progressPaint.color = color + backgroundPaint.color = color + backgroundPaint.alpha = 77 + invalidate() + } + + fun setThumbColor(color: Int) { + thumbPaint.color = color + thumbPaint.setShadowLayer(4f * density, 0f, 2f * density, Color.argb(80, 0, 0, 0)) + invalidate() + } + + private class TransparentDrawable : Drawable() { + override fun draw(canvas: Canvas) {} + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: ColorFilter?) {} + override fun getOpacity(): Int = PixelFormat.TRANSPARENT + } +} From c0f8f256ad7d58ac8f8ff5b2949c09c17bc3065b Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 25 Dec 2025 06:43:56 +0000 Subject: [PATCH 0691/1315] SystemUI: Allow toggle waveform seekbar [1/2] Change-Id: Idf7832ee85ef90070d3c1654eea4bb72ad24aaf9 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 5 ++ .../res/layout/media_session_view.xml | 3 +- .../controls/ui/binder/SeekBarObserver.kt | 4 ++ .../media/controls/ui/view/MediaViewHolder.kt | 67 ++++++++++++++++++- 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0badc4573871..11d5cf56c96a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7442,6 +7442,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SCREEN_OFF_ANIMATION = "screen_off_animation"; + /** + * @hide + */ + public static final String MEDIA_WAVEFORM_SEEKBAR = "media_waveform_seekbar"; + /** * Whether to show rotation suggestion * @hide diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 04f9c1e3b572..21c7b550af8c 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -282,8 +282,9 @@ - (R.id.device_suggestion_progressbar) val deviceSuggestionButton = itemView.findViewById(R.id.device_suggestion_button) - // Seekbar views - val seekBar = itemView.requireViewById(R.id.media_progress_bar) + // Seekbar views - dynamically replaced based on settings + var seekBar: SeekBar = itemView.requireViewById(R.id.media_progress_bar) + private set // These views are only shown while the user is actively scrubbing val scrubbingElapsedTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_elapsed_time) @@ -95,6 +98,66 @@ class MediaViewHolder constructor(itemView: View) { // Pagination val pageLeft = itemView.requireViewById(R.id.page_left) val pageRight = itemView.requireViewById(R.id.page_right) + + init { + replaceSeekBarIfNeeded() + } + + private fun replaceSeekBarIfNeeded() { + val waveformEnabled = Settings.System.getInt( + player.context.contentResolver, + Settings.System.MEDIA_WAVEFORM_SEEKBAR, + 0 + ) == 1 + + if (waveformEnabled && seekBar !is WaveformSeekBar) { + val parent = seekBar.parent as ViewGroup + val layoutParams = seekBar.layoutParams + val index = parent.indexOfChild(seekBar) + + val oldProgress = seekBar.progress + val oldMax = seekBar.max + val oldEnabled = seekBar.isEnabled + val oldId = seekBar.id + + parent.removeView(seekBar) + + val waveformSeekBar = WaveformSeekBar(player.context).apply { + id = oldId + this.layoutParams = layoutParams + progress = oldProgress + max = oldMax + isEnabled = oldEnabled + layoutDirection = View.LAYOUT_DIRECTION_LTR + } + + parent.addView(waveformSeekBar, index) + seekBar = waveformSeekBar + } else if (!waveformEnabled && seekBar is WaveformSeekBar) { + val parent = seekBar.parent as ViewGroup + val layoutParams = seekBar.layoutParams + val index = parent.indexOfChild(seekBar) + + val oldProgress = seekBar.progress + val oldMax = seekBar.max + val oldEnabled = seekBar.isEnabled + val oldId = seekBar.id + + parent.removeView(seekBar) + + val defaultSeekBar = SeekBar(player.context).apply { + id = oldId + this.layoutParams = layoutParams + progress = oldProgress + max = oldMax + isEnabled = oldEnabled + layoutDirection = View.LAYOUT_DIRECTION_LTR + } + + parent.addView(defaultSeekBar, index) + seekBar = defaultSeekBar + } + } fun getAction(id: Int): ImageButton { return when (id) { From 61f6896fc98fccd3f10ce855ca10fbac0a4fb63c Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 23 Dec 2025 05:54:37 +0000 Subject: [PATCH 0692/1315] SystemUI: Update affordance camera icon Change-Id: If4b1871fed2b3cf189bcb8371c0b7cd6f639c09d Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/drawable/ic_camera.xml | 36 +++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/res/drawable/ic_camera.xml b/packages/SystemUI/res/drawable/ic_camera.xml index ef1406c1c58a..fac551cf4a63 100644 --- a/packages/SystemUI/res/drawable/ic_camera.xml +++ b/packages/SystemUI/res/drawable/ic_camera.xml @@ -1,10 +1,28 @@ + + - - + android:height="17dp" + android:viewportHeight="24" + android:viewportWidth="24" + android:width="17dp" > + + + \ No newline at end of file From d4193421ddd98bdb2a3022d8b0f733bae6a78f96 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 18 Dec 2025 14:40:50 +0000 Subject: [PATCH 0693/1315] SystemUI: Add low battery shutdown warning dialogs Change-Id: I91e954d61044018073d1289b9ed68fa1b0cf7fba Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/BatteryService.java | 96 ++++++++++++++++++- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 4eeec8b9957e..37a43944032e 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -29,6 +29,7 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.content.ContentResolver; @@ -76,6 +77,7 @@ import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; +import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; @@ -169,6 +171,9 @@ public final class BatteryService extends SystemService { private final HealthInfo mLastHealthInfo = new HealthInfo(); private boolean mBatteryLevelCritical; + private AlertDialog mLowBatteryShutdownDialog = null; + private boolean mShutdownDialogShown = false; + /** * {@link HealthInfo#batteryStatus} value when {@link Intent#ACTION_BATTERY_CHANGED} * broadcast was sent last. @@ -679,16 +684,67 @@ private void shutdownIfNoPowerLocked() { // shut down gracefully if our battery is critically low and we are not powered. // wait until the system has booted before attempting to display the shutdown dialog. if (shouldShutdownLocked()) { + if (!mShutdownDialogShown) { + mHandler.post(() -> showLowBatteryShutdownDialog()); + mShutdownDialogShown = true; + + mHandler.postDelayed(() -> { + Intent intent = new Intent(Intent.ACTION_REQUEST_SHUTDOWN); + intent.putExtra(Intent.EXTRA_KEY_CONFIRM, false); + intent.putExtra(Intent.EXTRA_REASON, PowerManager.SHUTDOWN_LOW_BATTERY); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startShutdownActivity(intent); + }, 10000); + } + } else { + mShutdownDialogShown = false; + if (mLowBatteryShutdownDialog != null && mLowBatteryShutdownDialog.isShowing()) { + mLowBatteryShutdownDialog.dismiss(); + mLowBatteryShutdownDialog = null; + } + } + } + + private void showLowBatteryShutdownDialog() { + if (!mActivityManagerInternal.isSystemReady()) { + return; + } + + if (mLowBatteryShutdownDialog != null && mLowBatteryShutdownDialog.isShowing()) { + mLowBatteryShutdownDialog.dismiss(); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle("Critical Battery Warning"); + builder.setMessage("Battery level is critically low (" + mHealthInfo.batteryLevel + "%).\n\n" + + "Device will shut down in 10 seconds to prevent data loss.\n\n" + + "Please connect your charger immediately!"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setCancelable(false); + + builder.setPositiveButton("I'll Connect Charger", (dialog, which) -> { + dialog.dismiss(); + }); + + builder.setNegativeButton("Shutdown Now", (dialog, which) -> { + dialog.dismiss(); Intent intent = new Intent(Intent.ACTION_REQUEST_SHUTDOWN); intent.putExtra(Intent.EXTRA_KEY_CONFIRM, false); - intent.putExtra(Intent.EXTRA_REASON, - PowerManager.SHUTDOWN_LOW_BATTERY); + intent.putExtra(Intent.EXTRA_REASON, PowerManager.SHUTDOWN_LOW_BATTERY); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mHandler.post(() -> startShutdownActivity(intent)); - } + startShutdownActivity(intent); + }); + + mLowBatteryShutdownDialog = builder.create(); + + if (mLowBatteryShutdownDialog.getWindow() != null) { + mLowBatteryShutdownDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + } + + mLowBatteryShutdownDialog.show(); } - private void shutdownIfOverTempLocked() { // shut down gracefully if temperature is too high (> 68.0C by default) // wait until the system has booted before attempting to display the @@ -915,6 +971,7 @@ private void processValuesLocked(boolean force) { && mPlugType == BATTERY_PLUGGED_NONE) { // We want to make sure we log discharge cycle outliers // if the battery is about to die. + mHandler.post(() -> showCriticalBatteryWarningDialog()); dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime; logOutlier = true; } @@ -1711,6 +1768,35 @@ private void processValuesLocked(boolean forceUpdate, @Nullable PrintWriter pw) } } + private void showCriticalBatteryWarningDialog() { + if (!mActivityManagerInternal.isSystemReady()) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle("⚠️ Critical Battery Level"); + builder.setMessage("Battery: " + mHealthInfo.batteryLevel + "%\n\n" + + "Your device battery is critically low.\n" + + "Please connect to a charger immediately to prevent automatic shutdown."); + builder.setIcon(com.android.internal.R.drawable.stat_sys_battery_charge); + builder.setCancelable(true); + + builder.setPositiveButton("OK", (dialog, which) -> dialog.dismiss()); + + builder.setNeutralButton("Battery Saver", (dialog, which) -> { + dialog.dismiss(); + Intent intent = new Intent(android.provider.Settings.ACTION_BATTERY_SAVER_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + }); + + AlertDialog dialog = builder.create(); + if (dialog.getWindow() != null) { + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + } + dialog.show(); + } + private void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { synchronized (mLock) { if (args == null || args.length == 0 || "-a".equals(args[0])) { From 8d264f20b84131b161078ae73cd0e38cb40985a6 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 14 Nov 2025 11:45:20 +0000 Subject: [PATCH 0694/1315] SettingsLib: Improve expressive card text Change-Id: Ib449fe5f32945fb7749fc00865c4743fd4fe5a07 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../settingslib_expressive_icon_back.xml | 4 ++-- .../res/values-v36/styles_expressive.xml | 4 +++- ...gslib_expressive_preference_text_frame.xml | 20 +++++++++++++++++-- .../SettingsTheme/res/values-night/colors.xml | 5 ++++- .../SettingsTheme/res/values/colors.xml | 5 ++++- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v36/settingslib_expressive_icon_back.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v36/settingslib_expressive_icon_back.xml index 9986a60250fe..1ef24ad12903 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v36/settingslib_expressive_icon_back.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v36/settingslib_expressive_icon_back.xml @@ -22,7 +22,7 @@ + android:radius="14dp" /> @@ -44,4 +44,4 @@ android:pathData="M3.626,9L8.526,13.9C8.726,14.1 8.817,14.333 8.801,14.6C8.801,14.867 8.701,15.1 8.501,15.3C8.301,15.483 8.067,15.583 7.801,15.6C7.534,15.6 7.301,15.5 7.101,15.3L0.501,8.7C0.401,8.6 0.326,8.492 0.276,8.375C0.242,8.258 0.226,8.133 0.226,8C0.226,7.867 0.242,7.742 0.276,7.625C0.326,7.508 0.401,7.4 0.501,7.3L7.101,0.7C7.284,0.517 7.509,0.425 7.776,0.425C8.059,0.425 8.301,0.517 8.501,0.7C8.701,0.9 8.801,1.142 8.801,1.425C8.801,1.692 8.701,1.925 8.501,2.125L3.626,7H14.801C15.084,7 15.317,7.1 15.501,7.3C15.701,7.483 15.801,7.717 15.801,8C15.801,8.283 15.701,8.525 15.501,8.725C15.317,8.908 15.084,9 14.801,9H3.626Z"/> - \ No newline at end of file + diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml index 87782f14b286..a2d1206f92e4 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v36/styles_expressive.xml @@ -38,10 +38,12 @@ + diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index f11ae909a1b5..2640e647f61f 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -769,4 +769,6 @@ #1A73E8 + + @*android:color/system_neutral1_10 diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index fa37c65c9ca2..085272fbb411 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -995,7 +995,7 @@ please see styles_device_defaults.xml. 14sp 20sp - ?android:attr/textColorPrimary + @*android:color/system_neutral1_900 @@ -982,6 +982,10 @@ @android:color/system_on_primary_dark @drawable/qs_media_round_button_background @color/media_player_solid_button_bg + 6dp + 6dp + 6dp + 6dp + + diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 699a360b4263..5cd37e29f8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -56,6 +56,16 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.tuner.TunerService; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.media.dialog.MediaOutputDialogManager; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.FlashlightController; import dagger.Lazy; @@ -154,6 +164,15 @@ public class Dependency { @Inject Lazy mStatusBarWindowControllerStoreLazy; @Inject Lazy mSysUIStateDisplaysInteractor; @Inject Lazy mRotationPolicyWrapperLazy; + @Inject Lazy mActivityStarter; + @Inject Lazy mAccessPointController; + @Inject Lazy mNetworkController; + @Inject Lazy mInternetDialogManager; + @Inject Lazy mMediaOutputDialogManager; + @Inject Lazy mConfigurationController; + @Inject Lazy mFlashlightController; + @Inject Lazy mBluetoothDetailsContentViewModel; + @Inject Lazy mHotspotController; @Inject public Dependency() { @@ -202,6 +221,15 @@ protected void start() { mProviders.put( StatusBarWindowControllerStore.class, mStatusBarWindowControllerStoreLazy::get); mProviders.put(RotationPolicyWrapper.class, mRotationPolicyWrapperLazy::get); + mProviders.put(MediaOutputDialogManager.class, mMediaOutputDialogManager::get); + mProviders.put(AccessPointController.class, mAccessPointController::get); + mProviders.put(NetworkController.class, mNetworkController::get); + mProviders.put(InternetDialogManager.class, mInternetDialogManager::get); + mProviders.put(ConfigurationController.class, mConfigurationController::get); + mProviders.put(FlashlightController.class, mFlashlightController::get); + mProviders.put(BluetoothDetailsContentViewModel.class, mBluetoothDetailsContentViewModel::get); + mProviders.put(ActivityStarter.class, mActivityStarter::get); + mProviders.put(HotspotController.class, mHotspotController::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index d6210280d12c..6d4d4f2e1e1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -36,6 +36,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SystemSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -84,6 +85,7 @@ class KeyguardClockRepositoryImpl @Inject constructor( private val secureSettings: SecureSettings, + private val systemSettings: SystemSettings, private val clockRegistry: ClockRegistry, override val clockEventController: ClockEventController, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -175,7 +177,12 @@ constructor( 0, // Default value UserHandle.USER_CURRENT ) != 0 - val clockSettingValue = if (clockStyleEnabled) { + val lockscreenWidgetsEnabled = systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + val clockSettingValue = if (clockStyleEnabled || lockscreenWidgetsEnabled) { 0 } else { isDoubleLineClock diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java new file mode 100644 index 000000000000..29a82ab10b12 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java @@ -0,0 +1,130 @@ +/* + Copyright (C) 2024 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.android.systemui.lockscreen; + +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.AudioManager; +import android.os.DeviceIdleManager; +import android.provider.AlarmClock; +import android.provider.MediaStore; +import android.speech.RecognizerIntent; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +import com.android.systemui.Dependency; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.res.R; + +import java.util.List; + +public class ActivityLauncherUtils { + + private final static String PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings$personalizationSettingsLayoutActivity"; + private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; + + private final Context mContext; + private final ActivityStarter mActivityStarter; + private PackageManager mPackageManager; + + public ActivityLauncherUtils(Context context) { + this.mContext = context; + this.mActivityStarter = Dependency.get(ActivityStarter.class); + mPackageManager = mContext.getPackageManager(); + } + + public String getInstalledMusicApp() { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_APP_MUSIC); + final List musicApps = mPackageManager.queryIntentActivities(intent, 0); + ResolveInfo musicApp = musicApps.isEmpty() ? null : musicApps.get(0); + return musicApp != null ? musicApp.activityInfo.packageName : ""; + } + + public void launchAppIfAvailable(Intent launchIntent, @StringRes int appTypeResId) { + final List apps = mPackageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (!apps.isEmpty()) { + mActivityStarter.startActivity(launchIntent, true); + } else { + showNoDefaultAppFoundToast(appTypeResId); + } + } + + public void launchVoiceAssistant() { + DeviceIdleManager dim = mContext.getSystemService(DeviceIdleManager.class); + if (dim != null) { + dim.endIdle("voice-search"); + } + Intent voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE); + voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE, true); + mActivityStarter.startActivity(voiceIntent, true); + } + + public void launchCamera() { + final Intent launchIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); + launchAppIfAvailable(launchIntent, R.string.camera); + } + + public void launchTimer() { + final Intent launchIntent = new Intent(AlarmClock.ACTION_SET_TIMER); + launchAppIfAvailable(launchIntent, R.string.clock_timer); + } + + public void launchCalculator() { + final Intent launchIntent = new Intent(); + launchIntent.setAction(Intent.ACTION_MAIN); + launchIntent.addCategory(Intent.CATEGORY_APP_CALCULATOR); + launchAppIfAvailable(launchIntent, R.string.calculator); + } + + public void launchSettingsComponent(String className) { + if (mActivityStarter == null) return; + Intent intent = className.equals(PERSONALIZATIONS_ACTIVITY) ? new Intent(Intent.ACTION_MAIN) : new Intent(); + intent.setComponent(new ComponentName("com.android.settings", className)); + mActivityStarter.startActivity(intent, true); + } + + public void launchWeatherApp() { + final Intent launchIntent = new Intent(); + launchIntent.setAction(Intent.ACTION_MAIN); + launchIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); + launchAppIfAvailable(launchIntent, R.string.omnijaws_weather); + } + + public void launchMediaPlayerApp(String packageName) { + if (!packageName.isEmpty()) { + Intent launchIntent = mPackageManager.getLaunchIntentForPackage(packageName); + if (launchIntent != null) { + mActivityStarter.startActivity(launchIntent, true); + } + } + } + + public void startSettingsActivity() { + if (mActivityStarter == null) return; + mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), true /* dismissShade */); + } + + public void startIntent(Intent intent) { + if (mActivityStarter == null) return; + mActivityStarter.startActivity(intent, true /* dismissShade */); + } + + private void showNoDefaultAppFoundToast(@StringRes int appTypeResId) { + Toast.makeText(mContext, mContext.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt new file mode 100644 index 000000000000..45f6b02fc47a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt @@ -0,0 +1,67 @@ +/* + Copyright (C) 2024 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.android.systemui.lockscreen + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout + +import com.android.internal.jank.InteractionJankMonitor + +import com.android.systemui.Dependency +import com.android.systemui.animation.Expandable +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.media.dialog.MediaOutputDialogManager + +class LockScreenWidgets(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { + + private val mViewController: LockScreenWidgetsController? = LockScreenWidgetsController(this) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mViewController?.registerCallbacks() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mViewController?.unregisterCallbacks() + } + + override fun onFinishInflate() { + super.onFinishInflate() + mViewController?.initViews() + } + + fun showMediaDialog(view: View, lastMediaPackage: String) { + val packageName = lastMediaPackage.takeIf { it.isNotEmpty() } ?: return + Dependency.get(MediaOutputDialogManager::class.java) + .createAndShowWithController( + packageName, + true, + Expandable.fromView(view).dialogController() + ) + } + + private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? { + return dialogTransitionController( + cuj = + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + MediaOutputDialogManager.INTERACTION_JANK_TAG + ) + ) + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java new file mode 100644 index 000000000000..dfc615b727e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -0,0 +1,1040 @@ +/* + Copyright (C) 2024 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.android.systemui.lockscreen; + +import android.annotation.NonNull; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.ColorStateList; +import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.hardware.camera2.CameraManager; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.session.MediaSessionLegacyHelper; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.KeyEvent; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.AttributeSet; +import android.os.UserHandle; +import android.text.TextUtils; +import android.widget.LinearLayout; +import android.widget.Toast; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.settingslib.net.DataUsageController; +import com.android.settingslib.Utils; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.animation.Expandable; +import com.android.systemui.animation.view.LaunchableImageView; +import com.android.systemui.animation.view.LaunchableFAB; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.statusbar.policy.BluetoothController; +import com.android.systemui.statusbar.policy.BluetoothController.Callback; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; +import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.IconState; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.MobileDataIndicators; +import com.android.systemui.statusbar.connectivity.WifiIndicators; +import com.android.systemui.util.MediaSessionManagerHelper; +import com.android.internal.util.android.VibrationUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.android.internal.util.android.OmniJawsClient; + +public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObserver, MediaSessionManagerHelper.MediaMetadataListener { + + private static final String LOCKSCREEN_WIDGETS_ENABLED = + "lockscreen_widgets_enabled"; + + private static final String LOCKSCREEN_WIDGETS = + "lockscreen_widgets"; + + private static final String LOCKSCREEN_WIDGETS_EXTRAS = + "lockscreen_widgets_extras"; + + private static final String LOCKSCREEN_WIDGETS_STYLE = + "lockscreen_widgets_style"; + + private static final int[] MAIN_WIDGETS_VIEW_IDS = { + R.id.main_kg_item_placeholder1, + R.id.main_kg_item_placeholder2 + }; + + private static final int[] WIDGETS_VIEW_IDS = { + R.id.kg_item_placeholder1, + R.id.kg_item_placeholder2, + R.id.kg_item_placeholder3, + R.id.kg_item_placeholder4 + }; + + public static final int BT_ACTIVE = R.drawable.qs_bluetooth_icon_on; + public static final int BT_INACTIVE = R.drawable.qs_bluetooth_icon_off; + public static final int DATA_ACTIVE = R.drawable.ic_signal_cellular_alt_24; + public static final int DATA_INACTIVE = R.drawable.ic_mobiledata_off_24; + public static final int RINGER_ACTIVE = R.drawable.ic_vibration_24; + public static final int RINGER_INACTIVE = R.drawable.ic_ring_volume_24; + public static final int TORCH_RES_ACTIVE = R.drawable.ic_flashlight_on; + public static final int TORCH_RES_INACTIVE = R.drawable.ic_flashlight_off; + public static final int WIFI_ACTIVE = R.drawable.ic_wifi_24; + public static final int WIFI_INACTIVE = R.drawable.ic_wifi_off_24; + public static final int HOTSPOT_ACTIVE = R.drawable.qs_hotspot_icon_on; + public static final int HOTSPOT_INACTIVE = R.drawable.qs_hotspot_icon_off; + + public static final int BT_LABEL_INACTIVE = R.string.quick_settings_bluetooth_label; + public static final int DATA_LABEL_INACTIVE = R.string.quick_settings_data_label; + public static final int RINGER_LABEL_INACTIVE = R.string.quick_settings_ringer_label; + public static final int TORCH_LABEL_ACTIVE = R.string.torch_active; + public static final int TORCH_LABEL_INACTIVE = R.string.quick_settings_flashlight_label; + public static final int WIFI_LABEL_INACTIVE = R.string.quick_settings_wifi_label; + public static final int HOTSPOT_LABEL = R.string.accessibility_status_bar_hotspot; + + private OmniJawsClient mWeatherClient; + private OmniJawsClient.WeatherInfo mWeatherInfo; + + private final AccessPointController mAccessPointController; + private final BluetoothController mBluetoothController; + private final BluetoothDetailsContentViewModel mBluetoothDetailsContentViewModel; + private final ConfigurationController mConfigurationController; + private final DataUsageController mDataController; + private final FlashlightController mFlashlightController; + private final InternetDialogManager mInternetDialogManager; + private final NetworkController mNetworkController; + private final StatusBarStateController mStatusBarStateController; + private final MediaSessionManagerHelper mMediaSessionManagerHelper; + private final LockscreenWidgetsObserver mLockscreenWidgetsObserver; + private final ActivityLauncherUtils mActivityLauncherUtils; + private final HotspotController mHotspotController; + + protected final CellSignalCallback mCellSignalCallback = new CellSignalCallback(); + protected final WifiSignalCallback mWifiSignalCallback = new WifiSignalCallback(); + private final HotspotCallback mHotspotCallback = new HotspotCallback(); + + private boolean mIsHotspotEnabled = false; + + private Context mContext; + private LaunchableImageView mWidget1, mWidget2, mWidget3, mWidget4, mediaButton, torchButton, weatherButton; + private LaunchableFAB mediaButtonFab, torchButtonFab, weatherButtonFab, hotspotButtonFab; + private LaunchableFAB wifiButtonFab, dataButtonFab, ringerButtonFab, btButtonFab; + private LaunchableImageView wifiButton, dataButton, ringerButton, btButton, hotspotButton; + private int mDarkColor, mDarkColorActive, mLightColor, mLightColorActive; + + private CameraManager mCameraManager; + private String mCameraId; + private boolean isFlashOn = false; + + private String mMainLockscreenWidgetsList; + private String mSecondaryLockscreenWidgetsList; + private LaunchableFAB[] mMainWidgetViews; + private LaunchableImageView[] mSecondaryWidgetViews; + private List mMainWidgetsList = new ArrayList<>(); + private List mSecondaryWidgetsList = new ArrayList<>(); + private String mWidgetImagePath; + + private AudioManager mAudioManager; + private String mLastTrackTitle = null; + + private boolean mDozing; + + private boolean mIsInflated = false; + private GestureDetector mGestureDetector; + private boolean mIsLongPress = false; + + private boolean mLockscreenWidgetsEnabled; + private int mThemeStyle = 0; + + final ConfigurationListener mConfigurationListener = new ConfigurationListener() { + @Override + public void onUiModeChanged() { + updateWidgetViews(); + } + @Override + public void onThemeChanged() { + updateWidgetViews(); + } + }; + + private final View mView; + private final Handler mHandler = new Handler(); + + public LockScreenWidgetsController(View view) { + mView = view; + mContext = mView.getContext(); + mAccessPointController = Dependency.get(AccessPointController.class); + mBluetoothDetailsContentViewModel = Dependency.get(BluetoothDetailsContentViewModel.class); + mConfigurationController = Dependency.get(ConfigurationController.class); + mFlashlightController = Dependency.get(FlashlightController.class); + mInternetDialogManager = Dependency.get(InternetDialogManager.class); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mBluetoothController = Dependency.get(BluetoothController.class); + mNetworkController = Dependency.get(NetworkController.class); + mDataController = mNetworkController.getMobileDataController(); + mHotspotController = Dependency.get(HotspotController.class); + mMediaSessionManagerHelper = MediaSessionManagerHelper.Companion.getInstance(mContext); + + mActivityLauncherUtils = new ActivityLauncherUtils(mContext); + + mLockscreenWidgetsObserver = new LockscreenWidgetsObserver(); + mLockscreenWidgetsObserver.observe(); + + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + + initResources(); + + if (mWeatherClient == null) { + mWeatherClient = new OmniJawsClient(mContext); + } + + try { + mCameraId = mCameraManager.getCameraIdList()[0]; + } catch (Exception e) {} + + IntentFilter ringerFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + mContext.registerReceiver(mRingerModeReceiver, ringerFilter); + } + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + updateContainerVisibility(); + } + }; + + private final FlashlightController.FlashlightListener mFlashlightCallback = + new FlashlightController.FlashlightListener() { + @Override + public void onFlashlightChanged(boolean enabled) { + isFlashOn = enabled; + updateTorchButtonState(); + } + @Override + public void onFlashlightError() { + } + @Override + public void onFlashlightAvailabilityChanged(boolean available) { + isFlashOn = mFlashlightController.isEnabled() && available; + updateTorchButtonState(); + } + }; + + private void initResources() { + mDarkColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_dark); + mLightColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_light); + mDarkColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_dark); + mLightColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_light); + } + + public void registerCallbacks() { + if (isWidgetEnabled("hotspot")) { + mHotspotController.addCallback(mHotspotCallback); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.addCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.addCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.addCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.addCallback(mFlashlightCallback); + } + mConfigurationController.addCallback(mConfigurationListener); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + mMediaSessionManagerHelper.addMediaMetadataListener(this); + updateWidgetViews(); + updateMediaPlaybackState(); + } + + public void unregisterCallbacks() { + if (isWidgetEnabled("weather")) { + disableWeatherUpdates(); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.removeCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.removeCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.removeCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.removeCallback(mFlashlightCallback); + } + if (isWidgetEnabled("hotspot")) { + mHotspotController.removeCallback(mHotspotCallback); + } + mConfigurationController.removeCallback(mConfigurationListener); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mContext.unregisterReceiver(mRingerModeReceiver); + mLockscreenWidgetsObserver.unobserve(); + mHandler.removeCallbacksAndMessages(null); + mMediaSessionManagerHelper.removeMediaMetadataListener(this); + } + + public void initViews() { + mMainWidgetViews = new LaunchableFAB[MAIN_WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mMainWidgetViews.length; i++) { + mMainWidgetViews[i] = mView.findViewById(MAIN_WIDGETS_VIEW_IDS[i]); + } + mSecondaryWidgetViews = new LaunchableImageView[WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + mSecondaryWidgetViews[i] = mView.findViewById(WIDGETS_VIEW_IDS[i]); + } + mIsInflated = true; + updateWidgetViews(); + } + + public void updateWidgetViews() { + if (!mIsInflated) return; + if (mMainWidgetViews != null && mMainWidgetsList != null) { + for (int i = 0; i < mMainWidgetViews.length; i++) { + if (mMainWidgetViews[i] != null) { + mMainWidgetViews[i].setVisibility(i < mMainWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mMainWidgetsList.size(), mMainWidgetViews.length); i++) { + String widgetType = mMainWidgetsList.get(i); + if (widgetType != null && i < mMainWidgetViews.length && mMainWidgetViews[i] != null) { + setUpWidgetWiews(null, mMainWidgetViews[i], widgetType); + updateMainWidgetResources(mMainWidgetViews[i], false); + } + } + } + if (mSecondaryWidgetViews != null && mSecondaryWidgetsList != null) { + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + if (mSecondaryWidgetViews[i] != null) { + mSecondaryWidgetViews[i].setVisibility(i < mSecondaryWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mSecondaryWidgetsList.size(), mSecondaryWidgetViews.length); i++) { + String widgetType = mSecondaryWidgetsList.get(i); + if (widgetType != null && i < mSecondaryWidgetViews.length && mSecondaryWidgetViews[i] != null) { + setUpWidgetWiews(mSecondaryWidgetViews[i], null, widgetType); + updateWidgetsResources(mSecondaryWidgetViews[i]); + } + } + } + updateContainerVisibility(); + } + + private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { + if (efab == null) return; + efab.setElevation(0); + setButtonActiveState(null, efab, false); + long visibleWidgetCount = mMainWidgetsList.stream().filter(widget -> !"none".equals(widget)).count(); + ViewGroup.LayoutParams params = efab.getLayoutParams(); + if (params instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) params; + if (efab.getVisibility() == View.VISIBLE && visibleWidgetCount == 1) { + layoutParams.width = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_width); + layoutParams.height = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_height); + } else { + layoutParams.width = 0; + layoutParams.weight = 1; + } + efab.setLayoutParams(layoutParams); + } + } + + private void updateContainerVisibility() { + final boolean isMainWidgetsEmpty = mMainLockscreenWidgetsList == null + || TextUtils.isEmpty(mMainLockscreenWidgetsList); + final boolean isSecondaryWidgetsEmpty = mSecondaryLockscreenWidgetsList == null + || TextUtils.isEmpty(mSecondaryLockscreenWidgetsList); + final boolean isEmpty = isMainWidgetsEmpty && isSecondaryWidgetsEmpty; + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(isMainWidgetsEmpty ? View.GONE : View.VISIBLE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(isSecondaryWidgetsEmpty ? View.GONE : View.VISIBLE); + } + final boolean shouldHideContainer = isEmpty || mDozing || !mLockscreenWidgetsEnabled; + mView.setVisibility(shouldHideContainer ? View.GONE : View.VISIBLE); + } + + private void updateWidgetsResources(LaunchableImageView iv) { + if (iv == null) return; + final int themeStyle = mThemeStyle; + int bgRes; + switch (themeStyle) { + case 0: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + } + iv.setBackgroundResource(bgRes); + setButtonActiveState(iv, null, false); + } + + private boolean isNightMode() { + final Configuration config = mContext.getResources().getConfiguration(); + return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + } + + private void setUpWidgetWiews(LaunchableImageView iv, LaunchableFAB efab, String type) { + switch (type) { + case "none": + if (iv != null) iv.setVisibility(View.GONE); + if (efab != null) efab.setVisibility(View.GONE); + break; + case "wifi": + if (iv != null) { + wifiButton = iv; + wifiButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + if (efab != null) { + wifiButtonFab = efab; + wifiButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleWiFi(), WIFI_INACTIVE, R.string.quick_settings_wifi_label); + break; + case "data": + if (iv != null) { + dataButton = iv; + dataButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + if (efab != null) { + dataButtonFab = efab; + dataButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleMobileData(), DATA_INACTIVE, DATA_LABEL_INACTIVE); + break; + case "ringer": + if (iv != null) ringerButton = iv; + if (efab != null) ringerButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleRingerMode(), RINGER_INACTIVE, RINGER_LABEL_INACTIVE); + break; + case "bt": + if (iv != null) { + btButton = iv; + btButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + if (efab != null) { + btButtonFab = efab; + btButtonFab.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleBluetoothState(), BT_INACTIVE, BT_LABEL_INACTIVE); + break; + case "torch": + if (iv != null) torchButton = iv; + if (efab != null) torchButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleFlashlight(), TORCH_RES_INACTIVE, TORCH_LABEL_INACTIVE); + break; + case "timer": + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchTimer(), R.drawable.ic_alarm, R.string.clock_timer); + break; + case "calculator": + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchCalculator(), R.drawable.ic_calculator, R.string.calculator); + break; + case "media": + if (iv != null) { + mediaButton = iv; + mediaButton.setOnLongClickListener(v -> { showMediaDialog(v); return true; }); + } + if (efab != null) mediaButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleMediaPlaybackState(), R.drawable.ic_media_play, R.string.controls_media_button_play); + break; + case "weather": + if (iv != null) weatherButton = iv; + if (efab != null) weatherButtonFab = efab; + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchWeatherApp(), R.drawable.ic_weather, R.string.weather_data_unavailable); + enableWeatherUpdates(); + break; + case "hotspot": + if (iv != null) { + hotspotButton = iv; + hotspotButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + if (efab != null) { + hotspotButtonFab = efab; + hotspotButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleHotspot(), HOTSPOT_INACTIVE, HOTSPOT_LABEL); + break; + default: + break; + } + } + + private void setUpWidgetResources(LaunchableImageView iv, LaunchableFAB efab, + View.OnClickListener cl, int drawableRes, int stringRes){ + if (efab != null) { + efab.setOnClickListener(cl); + efab.setIcon(mContext.getDrawable(drawableRes)); + efab.setText(mContext.getResources().getString(stringRes)); + if (mediaButtonFab == efab) { + attachSwipeGesture(efab); + } + } + if (iv != null) { + iv.setOnClickListener(cl); + iv.setImageResource(drawableRes); + } + } + + private void attachSwipeGesture(LaunchableFAB efab) { + final GestureDetector gestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS); + VibrationUtils.triggerVibration(mContext, 2); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT); + VibrationUtils.triggerVibration(mContext, 2); + } + return true; + } + return false; + } + @Override + public void onLongPress(MotionEvent e) { + super.onLongPress(e); + mIsLongPress = true; + showMediaDialog(efab); + mHandler.postDelayed(() -> { + mIsLongPress = false; + }, 2500); + } + }); + efab.setOnTouchListener((v, event) -> { + boolean isClick = gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && !isClick && !mIsLongPress) { + v.performClick(); + } + return true; + }); + } + + private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { + int bgTint; + int tintColor; + if (mThemeStyle == 2) { + if (active) { + bgTint = Utils.applyAlpha(0.3f, mDarkColorActive); + tintColor = mDarkColorActive; + } else { + bgTint = Utils.applyAlpha(0.3f, Color.WHITE); + tintColor = Color.WHITE; + } + } else { + if (active) { + bgTint = isNightMode() ? mDarkColorActive : mLightColorActive; + tintColor = isNightMode() ? mDarkColor : mLightColor; + } else { + bgTint = isNightMode() ? mDarkColor : mLightColor; + tintColor = isNightMode() ? mLightColor : mDarkColor; + } + } + if (iv != null) { + iv.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + if (iv != weatherButton) { + iv.setImageTintList(ColorStateList.valueOf(tintColor)); + } else { + iv.setImageTintList(null); + } + } + if (efab != null) { + efab.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + if (efab != weatherButtonFab) { + efab.setIconTint(ColorStateList.valueOf(tintColor)); + } else { + efab.setIconTint(null); + } + efab.setTextColor(tintColor); + } + } + + private void toggleMediaPlaybackState() { + if (mMediaSessionManagerHelper.isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY); + } + } + + private void showMediaDialog(View view) { + String lastMediaPkg = getLastUsedMedia(); + if (TextUtils.isEmpty(lastMediaPkg)) return; // Return if null or empty + mHandler.post(() -> { + ((LockScreenWidgets) mView).showMediaDialog(view, lastMediaPkg); + VibrationUtils.triggerVibration(mContext, 2); // Trigger vibration + }); + } + + private String getLastUsedMedia() { + return Settings.System.getString(mContext.getContentResolver(), + "media_session_last_package_name"); + } + + private void dispatchMediaKeyWithWakeLockToMediaSession(final int keycode) { + final MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + if (helper == null) return; + KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), KeyEvent.ACTION_DOWN, keycode, 0); + helper.sendMediaButtonEvent(event, true); + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); + helper.sendMediaButtonEvent(event, true); + mHandler.postDelayed(() -> { + updateMediaPlaybackState(); + }, 250); + } + + private void updateMediaPlaybackState() { + boolean isPlaying = mMediaSessionManagerHelper.isMediaPlaying(); + int stateIcon = isPlaying ? R.drawable.ic_media_pause : R.drawable.ic_media_play; + if (mediaButton != null) { + mediaButton.setImageResource(stateIcon); + setButtonActiveState(mediaButton, null, isPlaying); + } + if (mediaButtonFab != null) { + MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getMediaMetadata(); + String trackTitle = mMediaMetadata != null ? mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE) : ""; + if (!TextUtils.isEmpty(trackTitle) && mLastTrackTitle != trackTitle) { + mLastTrackTitle = trackTitle; + } + final boolean canShowTrackTitle = isPlaying || !TextUtils.isEmpty(mLastTrackTitle); + mediaButtonFab.setIcon(mContext.getDrawable(isPlaying ? R.drawable.ic_media_pause : R.drawable.ic_media_play)); + mediaButtonFab.setText(canShowTrackTitle ? mLastTrackTitle : mContext.getResources().getString(R.string.controls_media_button_play)); + setButtonActiveState(null, mediaButtonFab, isPlaying); + } + } + + private void toggleFlashlight() { + if (torchButton == null && torchButtonFab == null) return; + try { + mCameraManager.setTorchMode(mCameraId, !isFlashOn); + isFlashOn = !isFlashOn; + updateTorchButtonState(); + } catch (Exception e) {} + } + + private void toggleWiFi() { + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + mNetworkController.setWifiEnabled(!cbi.enabled); + updateWiFiButtonState(!cbi.enabled); + mHandler.postDelayed(() -> { + updateWiFiButtonState(cbi.enabled); + }, 250); + } + + private boolean isMobileDataEnabled() { + return mDataController.isMobileDataEnabled(); + } + + private void toggleMobileData() { + mDataController.setMobileDataEnabled(!isMobileDataEnabled()); + updateMobileDataState(!isMobileDataEnabled()); + mHandler.postDelayed(() -> { + updateMobileDataState(isMobileDataEnabled()); + }, 250); + } + + private void showInternetDialog(View view) { + mHandler.post(() -> mInternetDialogManager.create(true, + mAccessPointController.canConfigMobileData(), + mAccessPointController.canConfigWifi(), Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void toggleRingerMode() { + if (mAudioManager != null) { + int mode = mAudioManager.getRingerMode(); + if (mode == mAudioManager.RINGER_MODE_NORMAL) { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_VIBRATE); + } else { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + } + updateRingerButtonState(); + } + } + + private void updateTileButtonState( + LaunchableImageView iv, LaunchableFAB efab, + boolean active, int activeResource, int inactiveResource, + String activeString, String inactiveString) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (iv != null) { + iv.setImageResource(active ? activeResource : inactiveResource); + setButtonActiveState(iv, null, active); + } + if (efab != null) { + efab.setIcon(mContext.getDrawable(active ? activeResource : inactiveResource)); + efab.setText(active ? activeString : inactiveString); + setButtonActiveState(null, efab, active); + } + } + }); + } + + public void updateTorchButtonState() { + if (!isWidgetEnabled("torch")) return; + String activeString = mContext.getResources().getString(TORCH_LABEL_ACTIVE); + String inactiveString = mContext.getResources().getString(TORCH_LABEL_INACTIVE); + updateTileButtonState(torchButton, torchButtonFab, isFlashOn, + TORCH_RES_ACTIVE, TORCH_RES_INACTIVE, activeString, inactiveString); + } + + private BroadcastReceiver mRingerModeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateRingerButtonState(); + } + }; + + private final BluetoothController.Callback mBtCallback = new BluetoothController.Callback() { + @Override + public void onBluetoothStateChange(boolean enabled) { + updateBtState(); + } + @Override + public void onBluetoothDevicesChanged() { + updateBtState(); + } + }; + + private void updateWiFiButtonState(boolean enabled) { + if (!isWidgetEnabled("wifi")) return; + if (wifiButton == null && wifiButtonFab == null) return; + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + String inactiveString = mContext.getResources().getString(WIFI_LABEL_INACTIVE); + updateTileButtonState(wifiButton, wifiButtonFab, enabled, + WIFI_ACTIVE, WIFI_INACTIVE, cbi.ssid != null ? removeDoubleQuotes(cbi.ssid) : inactiveString, inactiveString); + } + + private void updateRingerButtonState() { + if (!isWidgetEnabled("ringer")) return; + if (ringerButton == null && ringerButtonFab == null) return; + if (mAudioManager != null) { + boolean isVibrateActive = mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; + String inactiveString = mContext.getResources().getString(RINGER_LABEL_INACTIVE); + updateTileButtonState(ringerButton, ringerButtonFab, isVibrateActive, + RINGER_ACTIVE, RINGER_INACTIVE, inactiveString, inactiveString); + } + } + + private void updateMobileDataState(boolean enabled) { + if (!isWidgetEnabled("data")) return; + if (dataButton == null && dataButtonFab == null) return; + String networkName = mNetworkController == null ? "" : mNetworkController.getMobileDataNetworkName(); + boolean hasNetwork = !TextUtils.isEmpty(networkName) && mNetworkController != null + && mNetworkController.hasMobileDataFeature(); + String inactiveString = mContext.getResources().getString(DATA_LABEL_INACTIVE); + updateTileButtonState(dataButton, dataButtonFab, enabled, + DATA_ACTIVE, DATA_INACTIVE, hasNetwork && enabled ? networkName : inactiveString, inactiveString); + } + + private void toggleBluetoothState() { + mBluetoothController.setBluetoothEnabled(!isBluetoothEnabled()); + updateBtState(); + mHandler.postDelayed(() -> { + updateBtState(); + }, 250); + } + + private void showBluetoothDialog(View view) { + mHandler.post(() -> + mBluetoothDetailsContentViewModel.showDialog(Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void updateBtState() { + if (!isWidgetEnabled("bt")) return; + if (btButton == null && btButtonFab == null) return; + String deviceName = isBluetoothEnabled() ? mBluetoothController.getConnectedDeviceName() : ""; + boolean isConnected = !TextUtils.isEmpty(deviceName); + String inactiveString = mContext.getResources().getString(BT_LABEL_INACTIVE); + updateTileButtonState(btButton, btButtonFab, isBluetoothEnabled(), + BT_ACTIVE, BT_INACTIVE, isConnected ? deviceName : inactiveString, inactiveString); + } + + private boolean isBluetoothEnabled() { + final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled(); + } + + @Nullable + private static String removeDoubleQuotes(String string) { + if (string == null) return null; + final int length = string.length(); + if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { + return string.substring(1, length - 1); + } + return string; + } + + protected static final class WifiCallbackInfo { + boolean enabled; + @Nullable + String ssid; + } + + protected final class WifiSignalCallback implements SignalCallback { + final WifiCallbackInfo mInfo = new WifiCallbackInfo(); + @Override + public void setWifiIndicators(@NonNull WifiIndicators indicators) { + if (indicators.qsIcon == null) { + updateWiFiButtonState(false); + return; + } + mInfo.enabled = indicators.enabled; + mInfo.ssid = indicators.description; + updateWiFiButtonState(mInfo.enabled); + } + } + + private final class CellSignalCallback implements SignalCallback { + @Override + public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { + if (indicators.qsIcon == null) { + updateMobileDataState(false); + return; + } + updateMobileDataState(isMobileDataEnabled()); + } + @Override + public void setNoSims(boolean show, boolean simDetected) { + updateMobileDataState(simDetected && isMobileDataEnabled()); + } + @Override + public void setIsAirplaneMode(@NonNull IconState icon) { + updateMobileDataState(!icon.visible && isMobileDataEnabled()); + } + } + + public void enableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.addObserver(this); + queryAndUpdateWeather(); + } + } + + public void disableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.removeObserver(this); + } + } + + @Override + public void weatherError(int errorReason) { + if (errorReason == OmniJawsClient.EXTRA_ERROR_DISABLED) { + mWeatherInfo = null; + } + } + + @Override + public void weatherUpdated() { + queryAndUpdateWeather(); + } + + @Override + public void updateSettings() { + queryAndUpdateWeather(); + } + + private void queryAndUpdateWeather() { + try { + if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled()) return; + mWeatherClient.queryWeather(); + mWeatherInfo = mWeatherClient.getWeatherInfo(); + if (mWeatherInfo != null) { + // OpenWeatherMap + String formattedCondition = mWeatherInfo.condition; + if (formattedCondition.toLowerCase().contains("clouds")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_clouds); + } else if (formattedCondition.toLowerCase().contains("rain")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_rain); + } else if (formattedCondition.toLowerCase().contains("clear")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_clear); + } else if (formattedCondition.toLowerCase().contains("storm")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_storm); + } else if (formattedCondition.toLowerCase().contains("snow")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_snow); + } else if (formattedCondition.toLowerCase().contains("wind")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_wind); + } else if (formattedCondition.toLowerCase().contains("mist")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_mist); + } + // MET Norway + if (formattedCondition.toLowerCase().contains("_")) { + final String[] words = formattedCondition.split("_"); + final StringBuilder formattedConditionBuilder = new StringBuilder(); + for (String word : words) { + final String capitalizedWord = word.substring(0, 1).toUpperCase() + word.substring(1); + formattedConditionBuilder.append(capitalizedWord).append(" "); + } + formattedCondition = formattedConditionBuilder.toString().trim(); + } + final Drawable d = mWeatherClient.getWeatherConditionImage(mWeatherInfo.conditionCode); + if (weatherButtonFab != null) { + weatherButtonFab.setIcon(d); + weatherButtonFab.setText(mWeatherInfo.temp + mWeatherInfo.tempUnits + " \u2022 " + formattedCondition); + weatherButtonFab.setIconTint(null); + } + if (weatherButton != null) { + weatherButton.setImageDrawable(d); + weatherButton.setImageTintList(null); + } + } + } catch(Exception e) {} + } + + private boolean isWidgetEnabled(String widget) { + return (mMainLockscreenWidgetsList != null + && !mMainLockscreenWidgetsList.contains(widget)) + || (mSecondaryLockscreenWidgetsList != null + && !mSecondaryLockscreenWidgetsList.contains(widget)); + } + + @Override + public void onMediaMetadataChanged() { + updateMediaPlaybackState(); + } + + @Override + public void onPlaybackStateChanged() { + updateMediaPlaybackState(); + } + + private class LockscreenWidgetsObserver extends ContentObserver { + public LockscreenWidgetsObserver() { + super(null); + } + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateSettings(); + } + void observe() { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_ENABLED), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_EXTRAS), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_STYLE), + false, + this); + updateSettings(); + } + void unobserve() { + mContext.getContentResolver().unregisterContentObserver(this); + } + void updateSettings() { + mLockscreenWidgetsEnabled = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_ENABLED, 0) == 1; + mMainLockscreenWidgetsList = Settings.System.getString(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS); + mSecondaryLockscreenWidgetsList = Settings.System.getString(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_EXTRAS); + mThemeStyle = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_STYLE, 0); + if (mMainLockscreenWidgetsList != null) { + mMainWidgetsList = Arrays.asList(mMainLockscreenWidgetsList.split(",")); + } + if (mSecondaryLockscreenWidgetsList != null) { + mSecondaryWidgetsList = Arrays.asList(mSecondaryLockscreenWidgetsList.split(",")); + } + updateWidgetViews(); + } + }; + + private void updateHotspotState() { + if (!isWidgetEnabled("hotspot")) return; + if (hotspotButton == null && hotspotButtonFab == null) return; + String hotspotString = mContext.getResources().getString(HOTSPOT_LABEL); + updateTileButtonState(hotspotButton, hotspotButtonFab, mIsHotspotEnabled, + HOTSPOT_ACTIVE, HOTSPOT_INACTIVE, hotspotString, hotspotString); + } + + private void toggleHotspot() { + mHotspotController.setHotspotEnabled(!mIsHotspotEnabled); + updateHotspotState(); + mHandler.postDelayed(() -> { + updateHotspotState(); + }, 250); + } + + private final class HotspotCallback implements HotspotController.Callback { + @Override + public void onHotspotChanged(boolean enabled, int numDevices) { + mIsHotspotEnabled = enabled; + updateHotspotState(); + } + @Override + public void onHotspotAvailabilityChanged(boolean available) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 79a7bec32b17..2bed3d0668bc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1275,9 +1275,11 @@ private ClockSize computeDesiredClockSizeForSplitShade() { private boolean shouldForceSmallClock() { return !isOnAod() // True on small landscape screens - && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) - || Settings.Secure.getIntForUser( - mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0; + && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + (Settings.Secure.getIntForUser( + mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0 || + Settings.System.getIntForUser( + mContentResolver, "lockscreen_widgets_enabled", 0, UserHandle.USER_CURRENT) != 0); } private void updateKeyguardStatusViewAlignment() { From 9682f1d20e6ffd1da965393e37ffba736decb671 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 5 Dec 2024 22:01:42 +0800 Subject: [PATCH 0908/1315] SystemUI: Introduce Lockscreen info widgets * inspired from motorola lockscreen widgets Co-authored-by: DrDisagree Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/drawable/arc_progress.xml | 17 ++ .../res-keyguard/drawable/ic_battery.xml | 10 + .../res-keyguard/drawable/ic_memory.xml | 28 +++ .../res-keyguard/drawable/ic_temperature.xml | 10 + .../res-keyguard/drawable/ic_volume_eq.xml | 10 + .../layout/keyguard_info_widgets.xml | 59 ++++++ .../SystemUI/res/values/matrixx_attrs.xml | 4 + .../systemui/util/ArcProgressWidget.java | 83 ++++++++ .../systemui/util/ProgressImageView.kt | 188 ++++++++++++++++++ 9 files changed, 409 insertions(+) create mode 100644 packages/SystemUI/res-keyguard/drawable/arc_progress.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_battery.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_memory.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_temperature.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml create mode 100644 packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt diff --git a/packages/SystemUI/res-keyguard/drawable/arc_progress.xml b/packages/SystemUI/res-keyguard/drawable/arc_progress.xml new file mode 100644 index 000000000000..194c813e7217 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/arc_progress.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/drawable/ic_battery.xml b/packages/SystemUI/res-keyguard/drawable/ic_battery.xml new file mode 100644 index 000000000000..9138349dd22b --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_battery.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_memory.xml b/packages/SystemUI/res-keyguard/drawable/ic_memory.xml new file mode 100644 index 000000000000..ada36c58ff1d --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_memory.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml b/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml new file mode 100644 index 000000000000..25dc1f40fc6b --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml b/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml new file mode 100644 index 000000000000..3e256c0ba5b3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml b/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml new file mode 100644 index 000000000000..e54599eb0220 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/matrixx_attrs.xml b/packages/SystemUI/res/values/matrixx_attrs.xml index 5182981a6229..3ff73ed58daf 100644 --- a/packages/SystemUI/res/values/matrixx_attrs.xml +++ b/packages/SystemUI/res/values/matrixx_attrs.xml @@ -20,4 +20,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java new file mode 100644 index 000000000000..ed5485b6d5e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java @@ -0,0 +1,83 @@ +package com.android.systemui.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BlendMode; +import android.graphics.BlendModeColorFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import androidx.annotation.Nullable; + +public class ArcProgressWidget { + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable String textBottom, int textBottomSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, null, 28, textBottom, textBottomSizePx, tf); + } + + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable Drawable iconDrawable, int iconSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, iconDrawable, iconSizePx, "Usage", 28, tf); + } + + public static Bitmap generateBitmap(Context context, + int percentage, + String textInside, + int textInsideSizePx, + @Nullable Drawable iconDrawable, + int iconSizePx, + @Nullable String textBottom, + int textBottomSizePx, + @Nullable String tf) { + int width = 400; + int height = 400; + int stroke = 40; + int padding = 5; + int minAngle = 135; + int maxAngle = 275; + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG); + paint.setStrokeWidth(stroke); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextSize(dp2px(context, textInsideSizePx)); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setTextAlign(Paint.Align.CENTER); + final RectF arc = new RectF(); + arc.set(((float) stroke / 2) + padding, ((float) stroke / 2) + padding, width - padding - ((float) stroke / 2), height - padding - ((float) stroke / 2)); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + paint.setColor(Color.argb(75, 255, 255, 255)); + canvas.drawArc(arc, minAngle, maxAngle, false, paint); + paint.setColor(Color.WHITE); + canvas.drawArc(arc, minAngle, ((float) maxAngle / 100) * percentage, false, paint); + if (tf != null) { + mTextPaint.setTypeface(Typeface.create(tf, Typeface.BOLD)); + } else { + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } + canvas.drawText(textInside, (float) bitmap.getWidth() / 2, (bitmap.getHeight() - mTextPaint.ascent() * 0.7f) / 2, mTextPaint); + if (iconDrawable != null) { + int size = dp2px(context, iconSizePx); + int left = (bitmap.getWidth() - size) / 2; + int top = bitmap.getHeight() - (int) (size / 1.3) - (stroke + padding) - dp2px(context, 4); + int right = left + size; + int bottom = top + size; + iconDrawable.setBounds(left, top, right, bottom); + iconDrawable.setColorFilter(new BlendModeColorFilter(Color.WHITE, BlendMode.SRC_IN)); + iconDrawable.draw(canvas); + } else if (textBottom != null) { + mTextPaint.setTextSize(dp2px(context, textBottomSizePx)); + canvas.drawText(textBottom, (float) bitmap.getWidth() / 2, bitmap.getHeight() - (stroke + padding), mTextPaint); + } + return bitmap; + } + + private static int dp2px(Context context, float dp) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt new file mode 100644 index 000000000000..1f9edfd32083 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2023-2024 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util + +import android.app.ActivityManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContentResolver +import android.content.Intent +import android.content.IntentFilter +import android.database.ContentObserver +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.AudioManager +import android.os.BatteryManager +import android.provider.Settings +import android.util.AttributeSet +import android.widget.ImageView +import androidx.core.content.ContextCompat +import kotlinx.coroutines.* + +import com.android.systemui.res.R + +class ProgressImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ImageView(context, attrs, defStyleAttr) { + + private var progressType: ProgressType = ProgressType.UNKNOWN + private var progressPercent = -1 + private var batteryLevel = -1 + private var batteryTemperature = -1 + private var updateJob: Job? = null + private var receiverRegistered = false + private var typeface: String? = null + + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BATTERY_CHANGED) { + batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + batteryLevel = batteryLevel.coerceIn(0, 100) + batteryTemperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10 + updateProgress() + } + } + } + + private val settingsObserver = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateVisibility() + } + } + + enum class ProgressType(val iconRes: Int) { + BATTERY(R.drawable.ic_battery), + MEMORY(R.drawable.ic_memory), + TEMPERATURE(R.drawable.ic_temperature), + VOLUME(R.drawable.ic_volume_eq), + UNKNOWN(-1) + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView, defStyleAttr, 0) + typeface = typedArray.getString(R.styleable.ProgressImageView_typeface) + typedArray.recycle() + when (id) { + R.id.battery_progress -> progressType = ProgressType.BATTERY + R.id.memory_progress -> progressType = ProgressType.MEMORY + R.id.temperature_progress -> progressType = ProgressType.TEMPERATURE + R.id.volume_progress -> progressType = ProgressType.VOLUME + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + context.contentResolver.registerContentObserver( + Settings.System.getUriFor("lockscreen_info_widgets_enabled"), + false, + settingsObserver + ) + if (!receiverRegistered) { + if (progressType == ProgressType.BATTERY || progressType == ProgressType.TEMPERATURE) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + context.registerReceiver(batteryReceiver, filter, Context.RECEIVER_EXPORTED) + receiverRegistered = true + } + } + startProgressUpdates() + updateVisibility() + updateProgress() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + context.contentResolver.unregisterContentObserver(settingsObserver) + if (receiverRegistered) { + context.unregisterReceiver(batteryReceiver) + } + stopProgressUpdates() + } + + private fun startProgressUpdates() { + if (progressType == ProgressType.MEMORY || progressType == ProgressType.VOLUME) { + updateJob = CoroutineScope(Dispatchers.Main).launch { + while (isActive) { + updateProgress() + delay(1000L) + } + } + } + } + + private fun stopProgressUpdates() { + updateJob?.cancel() + } + + private fun updateProgress() { + val newProgressPercent = when (progressType) { + ProgressType.BATTERY -> batteryLevel + ProgressType.MEMORY -> getMemoryLevel() + ProgressType.TEMPERATURE -> batteryTemperature + ProgressType.VOLUME -> getVolumeLevel() + ProgressType.UNKNOWN -> -1 + } + if (newProgressPercent != progressPercent) { + progressPercent = newProgressPercent + updateImageView() + } + } + + private fun updateImageView() { + val degree = "\u2103" + val progressText = if (progressType == ProgressType.TEMPERATURE) { + if (progressPercent != -1) "$progressPercent$degree" else "N/A" + } else { + if (progressPercent == -1) "..." else "$progressPercent%" + } + val icon: Drawable? = ContextCompat.getDrawable(context, progressType.iconRes) + val widgetBitmap: Bitmap = ArcProgressWidget.generateBitmap( + context, + if (progressPercent == -1) 0 else progressPercent, + progressText, + 40, + icon, + 36, + typeface + ) + setImageBitmap(widgetBitmap) + } + + private fun getMemoryLevel(): Int { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val usedMemory = memoryInfo.totalMem - memoryInfo.availMem + val usedMemoryPercentage = ((usedMemory * 100) / memoryInfo.totalMem).toInt() + return usedMemoryPercentage.coerceIn(0, 100) + } + + private fun getVolumeLevel(): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC) ?: 1 + val currentVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC) ?: 0 + return ((currentVolume * 100) / maxVolume).coerceIn(0, 100) + } + + private fun updateVisibility() { + val enabled = Settings.System.getInt( + context.contentResolver, + "lockscreen_info_widgets_enabled", 0 + ) == 1 + visibility = if (enabled) VISIBLE else GONE + } +} From 8a4978a9d1e1f6cd4492d441354b87db7a62f7ab Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Sat, 18 Jan 2025 10:10:26 +0800 Subject: [PATCH 0909/1315] Lockscreen widget styles update Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../lockscreen/LockScreenWidgetsController.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index dfc615b727e2..49de94370bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -96,6 +96,9 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser private static final String LOCKSCREEN_WIDGETS_STYLE = "lockscreen_widgets_style"; + + private static final String LOCKSCREEN_WIDGETS_TRANSPARENCY = + "lockscreen_widgets_transparency"; private static final int[] MAIN_WIDGETS_VIEW_IDS = { R.id.main_kg_item_placeholder1, @@ -183,6 +186,7 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser private boolean mLockscreenWidgetsEnabled; private int mThemeStyle = 0; + private float mTransparency = 0.3f; final ConfigurationListener mConfigurationListener = new ConfigurationListener() { @Override @@ -413,6 +417,7 @@ private void updateWidgetsResources(LaunchableImageView iv) { int bgRes; switch (themeStyle) { case 0: + case 3: default: bgRes = R.drawable.lockscreen_widget_background_circle; break; @@ -573,12 +578,12 @@ public void onLongPress(MotionEvent e) { private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { int bgTint; int tintColor; - if (mThemeStyle == 2) { + if (mThemeStyle == 2 || mThemeStyle == 3) { if (active) { - bgTint = Utils.applyAlpha(0.3f, mDarkColorActive); + bgTint = Utils.applyAlpha(mTransparency, mDarkColorActive); tintColor = mDarkColorActive; } else { - bgTint = Utils.applyAlpha(0.3f, Color.WHITE); + bgTint = Utils.applyAlpha(mTransparency, Color.WHITE); tintColor = Color.WHITE; } } else { @@ -988,6 +993,10 @@ void observe() { Settings.System.getUriFor(LOCKSCREEN_WIDGETS_STYLE), false, this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_TRANSPARENCY), + false, + this); updateSettings(); } void unobserve() { @@ -1002,6 +1011,8 @@ void updateSettings() { LOCKSCREEN_WIDGETS_EXTRAS); mThemeStyle = Settings.System.getInt(mContext.getContentResolver(), LOCKSCREEN_WIDGETS_STYLE, 0); + mTransparency = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_TRANSPARENCY, 30) / 100f; if (mMainLockscreenWidgetsList != null) { mMainWidgetsList = Arrays.asList(mMainLockscreenWidgetsList.split(",")); } From f7ade421275600bf7aedd2e87fa5f2e464c1716e Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Mon, 27 Jan 2025 08:05:44 +0800 Subject: [PATCH 0910/1315] SystemUI: Fixes, improvements to lockscreen widget and google wallet integration - Ghost: Adapt new omnijaw changes Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/values/kg_widgets_strings.xml | 1 + .../SystemUI/res/values/matrixx_dimens.xml | 3 - .../SystemUI/res/values/matrixx_strings.xml | 9 - .../lockscreen/ActivityLauncherUtils.java | 130 ---------- .../lockscreen/ActivityLauncherUtils.kt | 155 ++++++++++++ .../LockScreenWidgetsController.java | 236 ++++++++++++------ 6 files changed, 314 insertions(+), 220 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt diff --git a/packages/SystemUI/res/values/kg_widgets_strings.xml b/packages/SystemUI/res/values/kg_widgets_strings.xml index a9eb69090e30..93a0839c90d5 100644 --- a/packages/SystemUI/res/values/kg_widgets_strings.xml +++ b/packages/SystemUI/res/values/kg_widgets_strings.xml @@ -9,4 +9,5 @@ Mobile data Ringer mode Torch Active + Google Wallet diff --git a/packages/SystemUI/res/values/matrixx_dimens.xml b/packages/SystemUI/res/values/matrixx_dimens.xml index 16222627daa7..d795e25ebd74 100644 --- a/packages/SystemUI/res/values/matrixx_dimens.xml +++ b/packages/SystemUI/res/values/matrixx_dimens.xml @@ -120,9 +120,6 @@ 2dp 22dp - - 24dp - 180dp 0dp diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index 2a75d54302d6..0ef4b86fc5e7 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -221,15 +221,6 @@ VPN tethering turned off. VPN tethering turned on. - - Cloudy - Rainy - Sunny - Stormy - Snowy - Windy - Misty - null diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java deleted file mode 100644 index 29a82ab10b12..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - Copyright (C) 2024 the risingOS Android Project - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package com.android.systemui.lockscreen; - -import android.content.Context; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.media.AudioManager; -import android.os.DeviceIdleManager; -import android.provider.AlarmClock; -import android.provider.MediaStore; -import android.speech.RecognizerIntent; -import android.widget.Toast; - -import androidx.annotation.StringRes; - -import com.android.systemui.Dependency; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.res.R; - -import java.util.List; - -public class ActivityLauncherUtils { - - private final static String PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings$personalizationSettingsLayoutActivity"; - private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; - - private final Context mContext; - private final ActivityStarter mActivityStarter; - private PackageManager mPackageManager; - - public ActivityLauncherUtils(Context context) { - this.mContext = context; - this.mActivityStarter = Dependency.get(ActivityStarter.class); - mPackageManager = mContext.getPackageManager(); - } - - public String getInstalledMusicApp() { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_APP_MUSIC); - final List musicApps = mPackageManager.queryIntentActivities(intent, 0); - ResolveInfo musicApp = musicApps.isEmpty() ? null : musicApps.get(0); - return musicApp != null ? musicApp.activityInfo.packageName : ""; - } - - public void launchAppIfAvailable(Intent launchIntent, @StringRes int appTypeResId) { - final List apps = mPackageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (!apps.isEmpty()) { - mActivityStarter.startActivity(launchIntent, true); - } else { - showNoDefaultAppFoundToast(appTypeResId); - } - } - - public void launchVoiceAssistant() { - DeviceIdleManager dim = mContext.getSystemService(DeviceIdleManager.class); - if (dim != null) { - dim.endIdle("voice-search"); - } - Intent voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE); - voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE, true); - mActivityStarter.startActivity(voiceIntent, true); - } - - public void launchCamera() { - final Intent launchIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - launchAppIfAvailable(launchIntent, R.string.camera); - } - - public void launchTimer() { - final Intent launchIntent = new Intent(AlarmClock.ACTION_SET_TIMER); - launchAppIfAvailable(launchIntent, R.string.clock_timer); - } - - public void launchCalculator() { - final Intent launchIntent = new Intent(); - launchIntent.setAction(Intent.ACTION_MAIN); - launchIntent.addCategory(Intent.CATEGORY_APP_CALCULATOR); - launchAppIfAvailable(launchIntent, R.string.calculator); - } - - public void launchSettingsComponent(String className) { - if (mActivityStarter == null) return; - Intent intent = className.equals(PERSONALIZATIONS_ACTIVITY) ? new Intent(Intent.ACTION_MAIN) : new Intent(); - intent.setComponent(new ComponentName("com.android.settings", className)); - mActivityStarter.startActivity(intent, true); - } - - public void launchWeatherApp() { - final Intent launchIntent = new Intent(); - launchIntent.setAction(Intent.ACTION_MAIN); - launchIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); - launchAppIfAvailable(launchIntent, R.string.omnijaws_weather); - } - - public void launchMediaPlayerApp(String packageName) { - if (!packageName.isEmpty()) { - Intent launchIntent = mPackageManager.getLaunchIntentForPackage(packageName); - if (launchIntent != null) { - mActivityStarter.startActivity(launchIntent, true); - } - } - } - - public void startSettingsActivity() { - if (mActivityStarter == null) return; - mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), true /* dismissShade */); - } - - public void startIntent(Intent intent) { - if (mActivityStarter == null) return; - mActivityStarter.startActivity(intent, true /* dismissShade */); - } - - private void showNoDefaultAppFoundToast(@StringRes int appTypeResId) { - Toast.makeText(mContext, mContext.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt new file mode 100644 index 000000000000..1866196925ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt @@ -0,0 +1,155 @@ +/* + Copyright (C) 2023-2025 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.android.systemui.lockscreen + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.media.AudioManager +import android.os.DeviceIdleManager +import android.provider.AlarmClock +import android.provider.MediaStore +import android.speech.RecognizerIntent +import android.widget.Toast +import androidx.annotation.StringRes +import com.android.systemui.Dependency +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R + +class ActivityLauncherUtils(private val context: Context) { + + companion object { + private const val PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings\$mistifySettingsLayoutActivity" + private const val SERVICE_PACKAGE = "org.omnirom.omnijaws" + } + + private val activityStarter: ActivityStarter? = Dependency.get(ActivityStarter::class.java) + private val packageManager: PackageManager = context.packageManager + + fun getInstalledMusicApp(): String { + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_MUSIC) + } + val musicApps = packageManager.queryIntentActivities(intent, 0) + return musicApps.firstOrNull()?.activityInfo?.packageName.orEmpty() + } + + fun launchAppIfAvailable(launchIntent: Intent, @StringRes appTypeResId: Int) { + val apps = packageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY) + if (apps.isNotEmpty()) { + activityStarter?.startActivity(launchIntent, true) + } else { + showNoDefaultAppFoundToast(appTypeResId) + } + } + + fun launchVoiceAssistant() { + val dim = context.getSystemService(DeviceIdleManager::class.java) + dim?.endIdle("voice-search") + val voiceIntent = Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE).apply { + putExtra(RecognizerIntent.EXTRA_SECURE, true) + } + activityStarter?.startActivity(voiceIntent, true) + } + + fun launchCamera() { + val launchIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + launchAppIfAvailable(launchIntent, R.string.camera) + } + + fun launchTimer() { + val launchIntent = Intent(AlarmClock.ACTION_SET_TIMER) + launchAppIfAvailable(launchIntent, R.string.clock_timer) + } + + fun launchCalculator() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_CALCULATOR) + } + launchAppIfAvailable(launchIntent, R.string.calculator) + } + + fun launchSettingsComponent(className: String) { + val intent = if (className == PERSONALIZATIONS_ACTIVITY) { + Intent(Intent.ACTION_MAIN) + } else { + Intent().setComponent(ComponentName("com.android.settings", className)) + } + activityStarter?.startActivity(intent, true) + } + + fun launchWeatherApp() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + setClassName(SERVICE_PACKAGE, "$SERVICE_PACKAGE.WeatherActivity") + } + launchAppIfAvailable(launchIntent, R.string.omnijaws_weather) + } + + fun launchWalletApp() { + val launchIntent = context.packageManager.getLaunchIntentForPackage("com.google.android.apps.walletnfcrel")?.apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + launchIntent?.let { + launchAppIfAvailable(it, R.string.google_wallet) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { + activityStarter?.startActivity(it, true) + } + } + } + + fun launchQrScanner() { + try { + val qrScannerComponent = context.resources.getString( + com.android.internal.R.string.config_defaultQrCodeComponent + ) + val intent = if (qrScannerComponent.isNotEmpty()) { + Intent().apply { + component = ComponentName.unflattenFromString(qrScannerComponent) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } else { + Intent().apply { + component = ComponentName( + "com.google.android.googlequicksearchbox", + "com.google.android.apps.search.lens.LensActivity" + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + startIntent(intent) + } catch (e: Exception) { + Toast.makeText(context, "Unable to launch QR Scanner", Toast.LENGTH_SHORT).show() + } + } + + fun startSettingsActivity() { + val settingsIntent = Intent(android.provider.Settings.ACTION_SETTINGS) + activityStarter?.startActivity(settingsIntent, true) + } + + fun startIntent(intent: Intent) { + activityStarter?.startActivity(intent, true) + } + + private fun showNoDefaultAppFoundToast(@StringRes appTypeResId: Int) { + Toast.makeText(context, context.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index 49de94370bb0..3b1bd0044de3 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -60,7 +60,7 @@ import com.android.systemui.animation.view.LaunchableFAB; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel; import com.android.systemui.qs.tiles.dialog.InternetDialogManager; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.BluetoothController.Callback; @@ -75,13 +75,13 @@ import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.WifiIndicators; import com.android.systemui.util.MediaSessionManagerHelper; -import com.android.internal.util.android.VibrationUtils; +import com.android.internal.util.matrixx.VibrationUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.android.internal.util.android.OmniJawsClient; +import com.android.internal.util.matrixx.OmniJawsClient; public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObserver, MediaSessionManagerHelper.MediaMetadataListener { @@ -153,8 +153,6 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser protected final CellSignalCallback mCellSignalCallback = new CellSignalCallback(); protected final WifiSignalCallback mWifiSignalCallback = new WifiSignalCallback(); private final HotspotCallback mHotspotCallback = new HotspotCallback(); - - private boolean mIsHotspotEnabled = false; private Context mContext; private LaunchableImageView mWidget1, mWidget2, mWidget3, mWidget4, mediaButton, torchButton, weatherButton; @@ -227,8 +225,9 @@ public LockScreenWidgetsController(View view) { initResources(); + // FIX 1: OmniJawsClient no longer takes Context in constructor; use get() if (mWeatherClient == null) { - mWeatherClient = new OmniJawsClient(mContext); + mWeatherClient = OmniJawsClient.get(); } try { @@ -268,6 +267,12 @@ public void onFlashlightAvailabilityChanged(boolean available) { isFlashOn = mFlashlightController.isEnabled() && available; updateTorchButtonState(); } + // Add the missing method + @Override + public void onFlashlightStrengthChanged(int level) { + // Handle flashlight strength changes if needed + updateTorchButtonState(); + } }; private void initResources() { @@ -343,6 +348,39 @@ public void initViews() { public void updateWidgetViews() { if (!mIsInflated) return; + + // Forcefully hide all widgets if lockscreen widgets are disabled + if (!mLockscreenWidgetsEnabled) { + // Hide all main widget views + if (mMainWidgetViews != null) { + for (int i = 0; i < mMainWidgetViews.length; i++) { + if (mMainWidgetViews[i] != null) { + mMainWidgetViews[i].setVisibility(View.GONE); + } + } + } + // Hide all secondary widget views + if (mSecondaryWidgetViews != null) { + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + if (mSecondaryWidgetViews[i] != null) { + mSecondaryWidgetViews[i].setVisibility(View.GONE); + } + } + } + // Hide all containers + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(View.GONE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(View.GONE); + } + // Hide the main view itself + mView.setVisibility(View.GONE); + return; + } + if (mMainWidgetViews != null && mMainWidgetsList != null) { for (int i = 0; i < mMainWidgetViews.length; i++) { if (mMainWidgetViews[i] != null) { @@ -394,6 +432,20 @@ private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { } private void updateContainerVisibility() { + // If widgets are disabled, forcefully hide everything + if (!mLockscreenWidgetsEnabled) { + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(View.GONE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(View.GONE); + } + mView.setVisibility(View.GONE); + return; + } + final boolean isMainWidgetsEmpty = mMainLockscreenWidgetsList == null || TextUtils.isEmpty(mMainLockscreenWidgetsList); final boolean isSecondaryWidgetsEmpty = mSecondaryLockscreenWidgetsList == null @@ -437,102 +489,128 @@ private boolean isNightMode() { } private void setUpWidgetWiews(LaunchableImageView iv, LaunchableFAB efab, String type) { + View.OnClickListener clickListener = null; + View.OnLongClickListener longClickListener = null; + int drawableRes = 0; + int stringRes = 0; + switch (type) { case "none": if (iv != null) iv.setVisibility(View.GONE); if (efab != null) efab.setVisibility(View.GONE); - break; + return; case "wifi": - if (iv != null) { - wifiButton = iv; - wifiButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - if (efab != null) { - wifiButtonFab = efab; - wifiButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleWiFi(), WIFI_INACTIVE, R.string.quick_settings_wifi_label); + clickListener = v -> toggleWiFi(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = WIFI_INACTIVE; + stringRes = R.string.quick_settings_wifi_label; + if (iv != null) wifiButton = iv; + if (efab != null) wifiButtonFab = efab; break; case "data": - if (iv != null) { - dataButton = iv; - dataButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - if (efab != null) { - dataButtonFab = efab; - dataButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleMobileData(), DATA_INACTIVE, DATA_LABEL_INACTIVE); + clickListener = v -> toggleMobileData(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = DATA_INACTIVE; + stringRes = DATA_LABEL_INACTIVE; + if (iv != null) dataButton = iv; + if (efab != null) dataButtonFab = efab; break; case "ringer": + clickListener = v -> toggleRingerMode(); + drawableRes = RINGER_INACTIVE; + stringRes = RINGER_LABEL_INACTIVE; if (iv != null) ringerButton = iv; if (efab != null) ringerButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleRingerMode(), RINGER_INACTIVE, RINGER_LABEL_INACTIVE); break; case "bt": - if (iv != null) { - btButton = iv; - btButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - if (efab != null) { - btButtonFab = efab; - btButtonFab.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleBluetoothState(), BT_INACTIVE, BT_LABEL_INACTIVE); + clickListener = v -> toggleBluetoothState(); + longClickListener = v -> { + showBluetoothDialog(v); + return true; + }; + drawableRes = BT_INACTIVE; + stringRes = BT_LABEL_INACTIVE; + if (iv != null) btButton = iv; + if (efab != null) btButtonFab = efab; break; case "torch": + clickListener = v -> toggleFlashlight(); + drawableRes = TORCH_RES_INACTIVE; + stringRes = TORCH_LABEL_INACTIVE; if (iv != null) torchButton = iv; if (efab != null) torchButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleFlashlight(), TORCH_RES_INACTIVE, TORCH_LABEL_INACTIVE); break; case "timer": - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchTimer(), R.drawable.ic_alarm, R.string.clock_timer); + clickListener = v -> mActivityLauncherUtils.launchTimer(); + drawableRes = R.drawable.ic_alarm; + stringRes = R.string.clock_timer; break; case "calculator": - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchCalculator(), R.drawable.ic_calculator, R.string.calculator); + clickListener = v -> mActivityLauncherUtils.launchCalculator(); + drawableRes = R.drawable.ic_calculator; + stringRes = R.string.calculator; break; case "media": - if (iv != null) { - mediaButton = iv; - mediaButton.setOnLongClickListener(v -> { showMediaDialog(v); return true; }); - } + clickListener = v -> toggleMediaPlaybackState(); + longClickListener = v -> { + showMediaDialog(v); + return true; + }; + drawableRes = R.drawable.ic_media_play; + stringRes = R.string.controls_media_button_play; + if (iv != null) mediaButton = iv; if (efab != null) mediaButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleMediaPlaybackState(), R.drawable.ic_media_play, R.string.controls_media_button_play); break; - case "weather": + case "weather": + clickListener = v -> mActivityLauncherUtils.launchWeatherApp(); + drawableRes = R.drawable.ic_weather; + stringRes = R.string.weather_data_unavailable; if (iv != null) weatherButton = iv; if (efab != null) weatherButtonFab = efab; - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchWeatherApp(), R.drawable.ic_weather, R.string.weather_data_unavailable); enableWeatherUpdates(); break; case "hotspot": - if (iv != null) { - hotspotButton = iv; - hotspotButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - if (efab != null) { - hotspotButtonFab = efab; - hotspotButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleHotspot(), HOTSPOT_INACTIVE, HOTSPOT_LABEL); + clickListener = v -> toggleHotspot(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = HOTSPOT_INACTIVE; + stringRes = HOTSPOT_LABEL; + if (iv != null) hotspotButton = iv; + if (efab != null) hotspotButtonFab = efab; break; - default: + case "wallet": + clickListener = v -> mActivityLauncherUtils.launchWalletApp(); + drawableRes = R.drawable.ic_wallet_lockscreen; + stringRes = R.string.google_wallet; break; + case "qrscanner": + clickListener = v -> mActivityLauncherUtils.launchQrScanner(); + drawableRes = R.drawable.ic_qr_code_scanner; + stringRes = R.string.qr_code_scanner_title; + break; + default: + return; } - } - private void setUpWidgetResources(LaunchableImageView iv, LaunchableFAB efab, - View.OnClickListener cl, int drawableRes, int stringRes){ if (efab != null) { - efab.setOnClickListener(cl); + efab.setOnClickListener(clickListener); efab.setIcon(mContext.getDrawable(drawableRes)); efab.setText(mContext.getResources().getString(stringRes)); - if (mediaButtonFab == efab) { - attachSwipeGesture(efab); - } + if (longClickListener != null) efab.setOnLongClickListener(longClickListener); + if (mediaButtonFab == efab) attachSwipeGesture(efab); } + if (iv != null) { - iv.setOnClickListener(cl); + iv.setOnClickListener(clickListener); + if (longClickListener != null) iv.setOnLongClickListener(longClickListener); iv.setImageResource(drawableRes); } } @@ -657,7 +735,7 @@ private void updateMediaPlaybackState() { setButtonActiveState(mediaButton, null, isPlaying); } if (mediaButtonFab != null) { - MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getMediaMetadata(); + MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getCurrentMediaMetadata(); String trackTitle = mMediaMetadata != null ? mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE) : ""; if (!TextUtils.isEmpty(trackTitle) && mLastTrackTitle != trackTitle) { mLastTrackTitle = trackTitle; @@ -672,9 +750,8 @@ private void updateMediaPlaybackState() { private void toggleFlashlight() { if (torchButton == null && torchButtonFab == null) return; try { - mCameraManager.setTorchMode(mCameraId, !isFlashOn); - isFlashOn = !isFlashOn; - updateTorchButtonState(); + boolean newState = !isFlashOn; + mFlashlightController.setFlashlight(newState); } catch (Exception e) {} } @@ -872,17 +949,19 @@ public void setIsAirplaneMode(@NonNull IconState icon) { updateMobileDataState(!icon.visible && isMobileDataEnabled()); } } - + + // FIX 2: addObserver now requires Context as first argument public void enableWeatherUpdates() { if (mWeatherClient != null) { - mWeatherClient.addObserver(this); + mWeatherClient.addObserver(mContext, this); queryAndUpdateWeather(); } } + // FIX 3: removeObserver now requires Context as first argument public void disableWeatherUpdates() { if (mWeatherClient != null) { - mWeatherClient.removeObserver(this); + mWeatherClient.removeObserver(mContext, this); } } @@ -905,8 +984,10 @@ public void updateSettings() { private void queryAndUpdateWeather() { try { - if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled()) return; - mWeatherClient.queryWeather(); + // FIX 4: isOmniJawsEnabled and queryWeather now require Context argument + // FIX 5: getWeatherConditionImage now requires Context as first argument + if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled(mContext)) return; + mWeatherClient.queryWeather(mContext); mWeatherInfo = mWeatherClient.getWeatherInfo(); if (mWeatherInfo != null) { // OpenWeatherMap @@ -936,7 +1017,7 @@ private void queryAndUpdateWeather() { } formattedCondition = formattedConditionBuilder.toString().trim(); } - final Drawable d = mWeatherClient.getWeatherConditionImage(mWeatherInfo.conditionCode); + final Drawable d = mWeatherClient.getWeatherConditionImage(mContext, mWeatherInfo.conditionCode); if (weatherButtonFab != null) { weatherButtonFab.setIcon(d); weatherButtonFab.setText(mWeatherInfo.temp + mWeatherInfo.tempUnits + " \u2022 " + formattedCondition); @@ -952,9 +1033,9 @@ private void queryAndUpdateWeather() { private boolean isWidgetEnabled(String widget) { return (mMainLockscreenWidgetsList != null - && !mMainLockscreenWidgetsList.contains(widget)) + && mMainLockscreenWidgetsList.contains(widget)) || (mSecondaryLockscreenWidgetsList != null - && !mSecondaryLockscreenWidgetsList.contains(widget)); + && mSecondaryLockscreenWidgetsList.contains(widget)); } @Override @@ -1027,12 +1108,12 @@ private void updateHotspotState() { if (!isWidgetEnabled("hotspot")) return; if (hotspotButton == null && hotspotButtonFab == null) return; String hotspotString = mContext.getResources().getString(HOTSPOT_LABEL); - updateTileButtonState(hotspotButton, hotspotButtonFab, mIsHotspotEnabled, + updateTileButtonState(hotspotButton, hotspotButtonFab, mHotspotController.isHotspotEnabled(), HOTSPOT_ACTIVE, HOTSPOT_INACTIVE, hotspotString, hotspotString); } private void toggleHotspot() { - mHotspotController.setHotspotEnabled(!mIsHotspotEnabled); + mHotspotController.setHotspotEnabled(!mHotspotController.isHotspotEnabled()); updateHotspotState(); mHandler.postDelayed(() -> { updateHotspotState(); @@ -1042,7 +1123,6 @@ private void toggleHotspot() { private final class HotspotCallback implements HotspotController.Callback { @Override public void onHotspotChanged(boolean enabled, int numDevices) { - mIsHotspotEnabled = enabled; updateHotspotState(); } @Override From 4b2497a08bc8b55b28c09d4b3217e9b802b5413f Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 3 Jan 2025 13:44:50 +0800 Subject: [PATCH 0911/1315] SystemUI: Introduce MediaSessionManagerHelper Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../util/MediaSessionManagerHelper.kt | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt diff --git a/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt new file mode 100644 index 000000000000..1e416e40a017 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt @@ -0,0 +1,263 @@ +/* +* Copyright (C) 2023-2024 The risingOS Android Project +* Copyright (C) 2025 The AxionAOSP Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package com.android.systemui.util + +import android.content.Context +import android.content.res.Configuration +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionLegacyHelper +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.SystemClock +import android.provider.Settings +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +class MediaSessionManagerHelper private constructor(private val context: Context) { + + interface MediaMetadataListener { + fun onMediaMetadataChanged() {} + fun onPlaybackStateChanged() {} + } + + private val _mediaMetadata = MutableStateFlow(null) + val mediaMetadata: StateFlow = _mediaMetadata + + private val _playbackState = MutableStateFlow(null) + val playbackState: StateFlow = _playbackState + + private val scope = CoroutineScope(Dispatchers.Main) + private var collectJob: Job? = null + + private var lastSavedPackageName: String? = null + private val mediaSessionManager: MediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! + private var activeController: MediaController? = null + private val listeners = mutableSetOf() + + private val mediaControllerCallback = object : MediaController.Callback() { + override fun onMetadataChanged(metadata: MediaMetadata?) { + _mediaMetadata.value = metadata + } + + override fun onPlaybackStateChanged(state: PlaybackState?) { + _playbackState.value = state + } + } + + private val tickerFlow = flow { + while (true) { + emit(Unit) + delay(1000) + } + }.flowOn(Dispatchers.Default) + + init { + lastSavedPackageName = Settings.System.getString( + context.contentResolver, + "media_session_last_package_name" + ) + + scope.launch { + tickerFlow + .map { fetchActiveController() } + .distinctUntilChanged { old, new -> sameSessions(old, new) } + .collect { controller -> + activeController?.unregisterCallback(mediaControllerCallback) + activeController = controller + controller?.registerCallback(mediaControllerCallback) + _mediaMetadata.value = controller?.metadata + _playbackState.value = controller?.playbackState + saveLastNonNullPackageName() + } + } + } + + private suspend fun fetchActiveController(): MediaController? = withContext(Dispatchers.IO) { + var localController: MediaController? = null + val remoteSessions = mutableSetOf() + + mediaSessionManager.getActiveSessions(null) + .filter { controller -> + controller.playbackState?.state == PlaybackState.STATE_PLAYING && + controller.playbackInfo != null + } + .forEach { controller -> + when (controller.playbackInfo?.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + remoteSessions.add(controller.packageName) + if (localController?.packageName == controller.packageName) { + localController = null + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (!remoteSessions.contains(controller.packageName)) { + localController = localController ?: controller + } + } + } + } + localController + } + + fun addMediaMetadataListener(listener: MediaMetadataListener) { + listeners.add(listener) + if (listeners.size == 1) { + startCollecting() + } + listener.onMediaMetadataChanged() + listener.onPlaybackStateChanged() + } + + fun removeMediaMetadataListener(listener: MediaMetadataListener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + stopCollecting() + } + } + + private fun startCollecting() { + collectJob = scope.launch { + launch { mediaMetadata.collect { notifyListeners { onMediaMetadataChanged() } } } + launch { playbackState.collect { notifyListeners { onPlaybackStateChanged() } } } + } + } + + private fun stopCollecting() { + collectJob?.cancel() + collectJob = null + } + + private fun notifyListeners(action: MediaMetadataListener.() -> Unit) { + listeners.forEach { it.action() } + } + + fun seekTo(time: Long) { + activeController?.transportControls?.seekTo(time) + } + + fun getTotalDuration() = mediaMetadata.value?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + private fun saveLastNonNullPackageName() { + activeController?.packageName?.takeIf { it.isNotEmpty() }?.let { pkg -> + if (pkg != lastSavedPackageName) { + Settings.System.putString( + context.contentResolver, + "media_session_last_package_name", + pkg + ) + lastSavedPackageName = pkg + } + } + } + + fun getMediaBitmap(): Bitmap? = mediaMetadata.value?.let { + it.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + } + + fun getCurrentMediaMetadata(): MediaMetadata? { + return mediaMetadata.value + } + + fun getMediaAppIcon(): Drawable? { + val packageName = activeController?.packageName ?: return null + return try { + val pm = context.packageManager + pm.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun isMediaControllerAvailable() = activeController?.packageName?.isNotEmpty() ?: false + + fun isMediaPlaying() = playbackState.value?.state == PlaybackState.STATE_PLAYING + + fun getMediaControllerPlaybackState(): PlaybackState? { + return activeController?.playbackState ?: null + } + + private fun sameSessions(a: MediaController?, b: MediaController?): Boolean { + if (a == b) return true + if (a == null) return false + return a.controlsSameSession(b) + } + + private fun dispatchMediaKeyWithWakeLockToMediaSession(keycode: Int) { + val helper = MediaSessionLegacyHelper.getHelper(context) ?: return + var event = KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keycode, + 0 + ) + helper.sendMediaButtonEvent(event, true) + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP) + helper.sendMediaButtonEvent(event, true) + } + + fun prevSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS) + } + + fun nextSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT) + } + + fun toggleMediaPlaybackState() { + if (isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE) + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY) + } + } + + fun launchMediaApp() { + lastSavedPackageName?.takeIf { it.isNotEmpty() }?.let { + launchMediaPlayerApp(it) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { intent -> + context.startActivity(intent) + } + } + } + + companion object { + @Volatile + private var instance: MediaSessionManagerHelper? = null + + fun getInstance(context: Context): MediaSessionManagerHelper = + instance ?: synchronized(this) { + instance ?: MediaSessionManagerHelper(context).also { instance = it } + } + } +} From 181d1cd77fb6011ffde09c015906fb6e8fb20af5 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 17 Oct 2024 16:53:46 +0800 Subject: [PATCH 0912/1315] SystemUI: Introduce AOD styles [1/2] Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_aod_style.xml | 21 ++ .../com/android/systemui/clocks/AODStyle.java | 221 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml b/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml new file mode 100644 index 000000000000..659f966e7147 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java new file mode 100644 index 000000000000..275073c604d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023-2024 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.settingslib.drawable.CircleFramedDrawable; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.tuner.TunerService; + +public class AODStyle extends RelativeLayout implements TunerService.Tunable { + + private static final String CUSTOM_AOD_IMAGE_URI_KEY = "system:custom_aod_image_uri"; + private static final String CUSTOM_AOD_IMAGE_ENABLED_KEY = "system:custom_aod_image_enabled"; + + private final Context mContext; + private final TunerService mTunerService; + + private final StatusBarStateController mStatusBarStateController; + + private boolean mDozing; + + private ImageView mAodImageView; + private String mImagePath; + private String mCurrImagePath; + private boolean mAodImageEnabled; + private boolean mImageLoaded = false; + private boolean mCustomClockEnabled; + + // Burn-in protection + private static final int BURN_IN_PROTECTION_INTERVAL = 10000; // 10 seconds + private static final int BURN_IN_PROTECTION_MAX_SHIFT = 4; // 4 pixels + private final Handler mBurnInProtectionHandler = new Handler(); + private int mCurrentShiftX = 0; + private int mCurrentShiftY = 0; + + private final Runnable mBurnInProtectionRunnable = new Runnable() { + @Override + public void run() { + if (mDozing) { + mCurrentShiftX = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + mCurrentShiftY = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + if (mAodImageView != null) { + mAodImageView.setTranslationX(mCurrentShiftX); + mAodImageView.setTranslationY(mCurrentShiftY); + } + invalidate(); + mBurnInProtectionHandler.postDelayed(this, BURN_IN_PROTECTION_INTERVAL); + } + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + updateAodImageView(); + if (mDozing) { + startBurnInProtection(); + } else { + stopBurnInProtection(); + } + } + }; + + public AODStyle(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mTunerService = Dependency.get(TunerService.class); + mTunerService.addTunable(this, ClockStyle.CLOCK_STYLE_KEY, CUSTOM_AOD_IMAGE_URI_KEY, CUSTOM_AOD_IMAGE_ENABLED_KEY); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mAodImageView = findViewById(R.id.custom_aod_image_view); + loadAodImage(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.animate().cancel(); + mAodImageView.setImageDrawable(null); + } + } + + private void startBurnInProtection() { + mBurnInProtectionHandler.post(mBurnInProtectionRunnable); + } + + private void stopBurnInProtection() { + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.setTranslationX(0); + mAodImageView.setTranslationY(0); + } + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case ClockStyle.CLOCK_STYLE_KEY: + int clockStyle = TunerService.parseInteger(newValue, 0); + mCustomClockEnabled = clockStyle != 0; + break; + case CUSTOM_AOD_IMAGE_URI_KEY: + mImagePath = newValue; + if (mImagePath != null && !mImagePath.isEmpty() + && !mImagePath.equals(mCurrImagePath)) { + mCurrImagePath = mImagePath; + mImageLoaded = false; + loadAodImage(); + } + break; + case CUSTOM_AOD_IMAGE_ENABLED_KEY: + mAodImageEnabled = TunerService.parseIntegerSwitch( + newValue, false) && mCustomClockEnabled; + break; + } + } + + private void updateAodImageView() { + if (mAodImageView == null || !mAodImageEnabled) { + if (mAodImageView != null) mAodImageView.setVisibility(View.GONE); + return; + } + loadAodImage(); + if (mDozing) { + mAodImageView.setVisibility(View.VISIBLE); + mAodImageView.setScaleX(0f); + mAodImageView.setScaleY(0f); + mAodImageView.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(500) + .withEndAction(this::startBurnInProtection) + .start(); + } else { + mAodImageView.animate() + .scaleX(0f) + .scaleY(0f) + .setDuration(250) + .withEndAction(() -> { + mAodImageView.setVisibility(View.GONE); + stopBurnInProtection(); + }) + .start(); + } + } + + private void loadAodImage() { + if (mAodImageView == null || mCurrImagePath == null || mCurrImagePath.isEmpty() || mImageLoaded) return; + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(mCurrImagePath); + if (bitmap != null) { + int targetSize = (int) mContext.getResources().getDimension(R.dimen.custom_aod_image_size); + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true); + try (java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream()) { + scaledBitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 90, stream); + byte[] byteArray = stream.toByteArray(); + Bitmap compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + Drawable roundedImg = new CircleFramedDrawable(compressedBitmap, targetSize); + mAodImageView.setImageDrawable(roundedImg); + scaledBitmap.recycle(); + compressedBitmap.recycle(); + mImageLoaded = true; + } + } else { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } + } catch (Exception e) { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } finally { + if (bitmap != null) { + bitmap.recycle(); + } + } + } +} From df2173bc8b5e77dd26369dea9a32fab66502186a Mon Sep 17 00:00:00 2001 From: Arman-ATI Date: Tue, 24 Feb 2026 16:11:16 +0000 Subject: [PATCH 0913/1315] SystemUI: Adapt lockscreen features to the latest A16 keyguard changes Arman-ATI: * Added back dummy keyguard_clock_switch that does nothing (only a pleace holder) * Removed the flags that we were using to exclude these changes (now you are forced to use the blueprint workaround) This is a complete rewrite of the following widgets to work with the latest changes android made * AODStyles * ClockStyles * Omni Weather * Info Widgets * Lockscreen Widgets TODO: add translucent widget options, add peek display height, fix the notification approach, AOD Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/AndroidManifest.xml | 2 + .../clocks/common/res/values/dimens.xml | 1 + .../layout/keyguard_clock_switch.xml | 92 ++++++ .../layout/keyguard_clock_widgets.xml | 2 +- .../src/com/android/systemui/Dependency.java | 2 +- .../blueprints/DefaultKeyguardBlueprint.kt | 13 + .../blueprints/SplitShadeKeyguardBlueprint.kt | 9 + .../view/layout/sections/AODStyleSection.kt | 178 ++++++++++++ .../layout/sections/InfoWidgetsSection.kt | 139 +++++++++ .../sections/KeyguardClockStyleSection.kt | 135 +++++++++ .../sections/KeyguardWeatherViewSection.kt | 192 ++++++++---- .../sections/KeyguardWidgetViewSection.kt | 273 ++++++++++++++++++ 12 files changed, 987 insertions(+), 51 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 73d058145f39..c1297b7b26f6 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -247,6 +247,8 @@ + + diff --git a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml index 8b1edd8506ea..4c81e51b3db6 100644 --- a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml +++ b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml @@ -33,6 +33,7 @@ 114dp 28dp 28dp + 100dp 28dp 8dp diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml new file mode 100644 index 000000000000..797f28c81783 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml index 1542bb40aed8..baab1b46d4a3 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml @@ -2,7 +2,7 @@ (R.id.aod_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + // Inflate the AOD style layout + aodStyleView = LayoutInflater.from(context).inflate( + R.layout.keyguard_aod_style, + constraintLayout, + false + ).apply { + id = R.id.aod_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(aodStyleView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // The AODStyle component handles its own data binding + // through its TunerService integration and StatusBarStateController callbacks + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // Position AOD style within the keyguard_status_area + connect( + R.id.aod_ls, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.aod_ls, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Position at the top of status area with proper margin to avoid status bar overlap + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect( + R.id.aod_ls, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + topMargin + ) + + // Set dimensions + constrainHeight(R.id.aod_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.aod_ls, ConstraintSet.MATCH_CONSTRAINT) + + // Set appropriate margins matching the original XML structure + setMargin(R.id.aod_ls, ConstraintSet.START, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + setMargin(R.id.aod_ls, ConstraintSet.END, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + + // Ensure proper layering within the status area + setElevation(R.id.aod_ls, 2f) // Higher elevation than info widgets + + // Update other elements to position below AOD style when it's visible + if (constraintSet.getConstraint(R.id.lockscreen_clock_view) != null) { + connect( + R.id.lockscreen_clock_view, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) + } + + // Apply consistent top margin to clock_ls whether AOD is visible or not + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + // If AOD is present, position clock below it + // If AOD is hidden/gone, ensure clock still has proper top margin + val clockTopMargin = if (aodStyleView?.visibility == View.VISIBLE) { + 8 // Small gap below AOD + } else { + topMargin // Same margin as AOD would have had + } + + if (aodStyleView?.visibility == View.VISIBLE) { + connect( + R.id.clock_ls, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + clockTopMargin + ) + } else { + // When AOD is hidden, position clock at top with proper margin + connect( + R.id.clock_ls, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + clockTopMargin + ) + } + } + + // Update the barrier to include AOD style for proper notification positioning + // This ensures notifications appear below all status area content including AOD + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.aod_ls, + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets + ) + ) + + // Ensure notification icons are positioned below the barrier + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + aodStyleView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + aodStyleView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt new file mode 100644 index 000000000000..ef20ce8897a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject + +class InfoWidgetsSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var infoWidgetsView: View? = null + + override fun addViews(constraintLayout: ConstraintLayout) { + + constraintLayout.findViewById(R.id.keyguard_info_widgets)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + infoWidgetsView = LayoutInflater.from(context).inflate( + R.layout.keyguard_info_widgets, + constraintLayout, + false + ).apply { + id = R.id.keyguard_info_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(infoWidgetsView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // ProgressImageView components handle their own data binding + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + + constraintSet.apply { + // Info widgets positioning - below WEATHER (3rd in hierarchy) + connect(R.id.keyguard_info_widgets, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.keyguard_info_widgets, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to weather (primary) or fallback hierarchy + when { + constraintSet.getConstraint(R.id.keyguard_weather) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_weather, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.default_weather_image) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.clock_ls) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.keyguard_slice_view) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 12) + } + else -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 12) + } + } + + constrainHeight(R.id.keyguard_info_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_info_widgets, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.END, 0) + // Add small bottom margin for AOD to prevent notification overlap + setMargin(R.id.keyguard_info_widgets, ConstraintSet.BOTTOM, 6) // 6dp bottom margin for AOD + setElevation(R.id.keyguard_info_widgets, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + infoWidgetsView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + infoWidgetsView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt new file mode 100644 index 000000000000..8402a9066dc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.os.UserHandle +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.clocks.ClockStyle +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject + +class KeyguardClockStyleSection +@Inject +constructor( + private val context: Context, + private val secureSettings: SecureSettings, +) : KeyguardSection() { + + private var clockStyleView: ClockStyle? = null + private var isCustomClockEnabled: Boolean = false + + override fun addViews(constraintLayout: ConstraintLayout) { + + val clockStyle = secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) + isCustomClockEnabled = clockStyle != 0 + + if (!isCustomClockEnabled) return + + constraintLayout.findViewById(R.id.clock_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + val inflater = android.view.LayoutInflater.from(context) + clockStyleView = inflater.inflate(R.layout.keyguard_clock_style, null) as ClockStyle + clockStyleView?.apply { + id = R.id.clock_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + clockStyleView?.let { constraintLayout.addView(it) } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + clockView.onTimeChanged() + clockView.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + if (!isCustomClockEnabled) return + + constraintSet.apply { + // Clock positioning - TOP of hierarchy + connect(R.id.clock_ls, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.clock_ls, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect(R.id.clock_ls, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, topMargin) + + constrainHeight(R.id.clock_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.clock_ls, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.clock_ls, ConstraintSet.START, 0) + setMargin(R.id.clock_ls, ConstraintSet.END, 0) + setElevation(R.id.clock_ls, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + (clockView.parent as? ViewGroup)?.removeView(clockView) + } + clockStyleView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt index f0817a139680..07ee85aadd9e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2026 crDroid Android Project + * Copyright (C) 2025 the RisingOS Revived Android Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,91 +12,185 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.clocks.R as clocksR +import com.android.systemui.customization.R as custR import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import com.android.systemui.weather.WeatherImageView +import com.android.systemui.weather.WeatherTextView import javax.inject.Inject -import com.android.systemui.weather.WeatherInfoView - -class KeyguardWeatherViewSection -@Inject -constructor( +class KeyguardWeatherViewSection @Inject constructor( private val context: Context, - val layoutInflater: LayoutInflater, - val smartspaceController: LockscreenSmartspaceController, ) : KeyguardSection() { - private lateinit var weatherView: WeatherInfoView + + private var weatherImageView: WeatherImageView? = null + private var weatherTextView: WeatherTextView? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - weatherView = - layoutInflater.inflate(R.layout.keyguard_weather_area, null, false) as WeatherInfoView - constraintLayout.addView(weatherView) + val weatherContainer = constraintLayout.findViewById(R.id.keyguard_weather) + + if (weatherContainer != null) { + weatherImageView = weatherContainer.findViewById(R.id.default_weather_image) + weatherTextView = weatherContainer.findViewById(R.id.default_weather_text) + + (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) + constraintLayout.addView(weatherContainer) + } else { + createWeatherViews(constraintLayout) + } + + initializeWeatherViews() } - override fun bindData(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return + private fun createWeatherViews(constraintLayout: ConstraintLayout) { + weatherImageView = WeatherImageView(context).apply { + id = R.id.default_weather_image + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.GONE + } + + weatherTextView = WeatherTextView(context).apply { + id = R.id.default_weather_text + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + setTextColor(context.getColor(android.R.color.white)) + textSize = 20f + visibility = View.GONE + } - weatherView.init() + weatherImageView?.let { constraintLayout.addView(it) } + weatherTextView?.let { constraintLayout.addView(it) } + } + + private fun initializeWeatherViews() { + // Weather views initialize automatically when attached to window + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // Weather data binding handled by individual weather views } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return constraintSet.apply { - connect( - R.id.keyguard_weather_area, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(clocksR.dimen.status_view_margin_horizontal), - ) - connect( - R.id.keyguard_weather_area, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END - ) - constrainHeight(R.id.keyguard_weather_area, ConstraintSet.WRAP_CONTENT) + val startMargin = context.resources.getDimensionPixelSize(custR.dimen.clock_padding_start) + + context.resources.getDimensionPixelSize(custR.dimen.status_view_margin_horizontal) - connect( - R.id.keyguard_weather_area, - ConstraintSet.TOP, - R.id.keyguard_slice_view, - ConstraintSet.BOTTOM - ) + // Weather positioning - below CLOCK (2nd in hierarchy) + if (constraintSet.getConstraint(R.id.keyguard_weather) != null) { + connect(R.id.keyguard_weather, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + connect(R.id.keyguard_weather, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to clock (primary) or fallback to slice_view + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.keyguard_weather, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_weather, ConstraintSet.MATCH_CONSTRAINT) + } else { + applyWeatherImageConstraints(constraintSet, startMargin) + applyWeatherTextConstraints(constraintSet) + } + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + private fun applyWeatherImageConstraints(constraintSet: ConstraintSet, startMargin: Int) { + if (constraintSet.getConstraint(R.id.default_weather_image) != null) { + constraintSet.apply { + connect(R.id.default_weather_image, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + } + } + } + + private fun applyWeatherTextConstraints(constraintSet: ConstraintSet) { + if (constraintSet.getConstraint(R.id.default_weather_text) != null) { + constraintSet.apply { + connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, + context.resources.getDimensionPixelSize(R.dimen.weather_text_margin_start)) + connect(R.id.default_weather_text, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.TOP) + connect(R.id.default_weather_text, ConstraintSet.BOTTOM, R.id.default_weather_image, ConstraintSet.BOTTOM) + connect(R.id.default_weather_text, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + constrainHeight(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + } + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements createBarrier( R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, - *intArrayOf(R.id.keyguard_weather_area) + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } } } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintLayout.findViewById(R.id.keyguard_weather_area)?.let { weatherArea -> - weatherArea.cleanup() - constraintLayout.removeView(weatherArea) + constraintLayout.findViewById(R.id.keyguard_weather)?.let { weatherContainer -> + constraintLayout.removeView(weatherContainer) } + + weatherImageView?.let { constraintLayout.removeView(it) } + weatherTextView?.let { constraintLayout.removeView(it) } + + weatherImageView = null + weatherTextView = null } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt new file mode 100644 index 000000000000..0e26d298d258 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject +import com.android.systemui.lockscreen.LockScreenWidgets + +class KeyguardWidgetViewSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var widgetView: LockScreenWidgets? = null + private val TAG = "KeyguardWidgetViewSection" + + private fun createWidgetView(): LockScreenWidgets? { + Log.d(TAG, "Creating LockScreenWidgets view") + return try { + val layoutInflater = android.view.LayoutInflater.from(context) + // Try to inflate from your actual layout file + val view = layoutInflater.inflate(R.layout.keyguard_clock_widgets, null) as LockScreenWidgets + + view.apply { + // Override the ID to match what the keyguard system expects + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + Log.d(TAG, "Successfully inflated LockScreenWidgets from keyguard_clock_widgets layout") + view + } catch (e: Exception) { + Log.e(TAG, "Failed to inflate from keyguard_clock_widgets, trying direct instantiation", e) + try { + // Create a dummy AttributeSet for direct instantiation + val parser = context.resources.getLayout(android.R.layout.simple_list_item_1) + val attrs = android.util.Xml.asAttributeSet(parser) + + LockScreenWidgets(context, attrs).apply { + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + } catch (e2: Exception) { + Log.e(TAG, "Failed to create LockScreenWidgets directly", e2) + null + } + } + } + + override fun addViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "addViews called") + + // Check if the widget view already exists in the layout + val existingView = constraintLayout.findViewById(R.id.keyguard_widgets) + + if (existingView != null) { + Log.d(TAG, "Found existing widget view") + widgetView = existingView as? LockScreenWidgets + return + } + + // Check if we already have a widget view instance + if (widgetView != null) { + Log.d(TAG, "Reusing existing widget view instance") + // Remove from any previous parent + (widgetView?.parent as? ViewGroup)?.removeView(widgetView) + } else { + Log.d(TAG, "Creating new widget view") + widgetView = createWidgetView() + } + + widgetView?.let { view -> + try { + constraintLayout.addView(view) + Log.d(TAG, "Successfully added widget view to constraint layout") + } catch (e: Exception) { + Log.e(TAG, "Failed to add widget view", e) + } + } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + Log.d(TAG, "bindData called") + // Ensure the widget view is properly initialized and visible + widgetView?.let { view -> + if (view.visibility != View.VISIBLE) { + view.visibility = View.VISIBLE + Log.d(TAG, "Set widget view visibility to VISIBLE") + } + + // Force a layout pass to ensure the view is measured and laid out + view.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + Log.d(TAG, "applyConstraints called") + + // Only apply constraints if the widget view exists + widgetView ?: run { + Log.w(TAG, "Widget view is null, skipping constraints") + return + } + + try { + constraintSet.apply { + // Position widgets within the keyguard layout + connect( + R.id.keyguard_widgets, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.keyguard_widgets, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Try to position below other status content with priority order + val anchorViews = listOf( + R.id.keyguard_info_widgets, + R.id.clock_ls, + R.id.keyguard_weather, + R.id.keyguard_slice_view, + R.id.lockscreen_clock_view + ) + + var positioned = false + for (anchorId in anchorViews) { + try { + // Check if the constraint exists by trying to get it + constraintSet.getConstraint(anchorId) + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + anchorId, + ConstraintSet.BOTTOM, + 8 // 8dp margin + ) + positioned = true + Log.d(TAG, "Positioned widgets below anchor: ${context.resources.getResourceEntryName(anchorId)}") + break + } catch (e: Exception) { + // Continue to next anchor if this one fails + continue + } + } + + // If no anchor found, position at top with margin + if (!positioned) { + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + 16 // 16dp margin from top + ) + Log.d(TAG, "No anchor found, positioned at top") + } + + // Set dimensions + constrainHeight(R.id.keyguard_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_widgets, ConstraintSet.MATCH_CONSTRAINT) + + // Set margins + setMargin(R.id.keyguard_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_widgets, ConstraintSet.END, 0) + + // Set elevation for proper layering + setElevation(R.id.keyguard_widgets, 2f) + + // Update barrier to include widgets + try { + val barrierViews = mutableListOf() + val potentialBarrierViews = listOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets + ) + + potentialBarrierViews.forEach { viewId -> + try { + constraintSet.getConstraint(viewId) + barrierViews.add(viewId) + } catch (e: Exception) { + // Skip this view if it doesn't exist + } + } + + if (barrierViews.isNotEmpty()) { + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *barrierViews.toIntArray() + ) + Log.d(TAG, "Created barrier with ${barrierViews.size} views") + } + + // Position notification icons below barrier + try { + constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + try { + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + } catch (e: Exception) { + 8 // fallback margin + } + ) + } catch (e: Exception) { + // Notification container doesn't exist, skip + } + } catch (e: Exception) { + Log.w(TAG, "Failed to create barrier", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to apply constraints", e) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "removeViews called") + widgetView?.let { view -> + try { + constraintLayout.removeView(view) + Log.d(TAG, "Successfully removed widget view") + } catch (e: Exception) { + Log.w(TAG, "Failed to remove widget view", e) + } + } + widgetView = null + } +} From 64c1803567c311c630c36c5d62e825671e3c62ea Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 9 Nov 2024 13:09:45 +0000 Subject: [PATCH 0914/1315] SystemUI: Added Label clock style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_clock_label.xml | 99 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml new file mode 100644 index 000000000000..958d1930b3fc --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 47b6c97f3c97..6fb208fa99ab 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -46,7 +46,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_simple, R.layout.keyguard_clock_miui, R.layout.keyguard_clock_ide, - R.layout.keyguard_clock_moto + R.layout.keyguard_clock_moto, + R.layout.keyguard_clock_label }; private final static int[] mCenterClocks = {2, 3, 5, 6}; From 10bbf1a344c69f3648d007f6dce5f2cfbae9929f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 06:42:50 +0000 Subject: [PATCH 0915/1315] SystemUI: Add ios like clock Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_clock_ios.xml | 45 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 ++- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml new file mode 100644 index 000000000000..e9b816442596 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 6fb208fa99ab..e492c51092c4 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -47,10 +47,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_miui, R.layout.keyguard_clock_ide, R.layout.keyguard_clock_moto, - R.layout.keyguard_clock_label + R.layout.keyguard_clock_label, + R.layout.keyguard_clock_ios }; - private final static int[] mCenterClocks = {2, 3, 5, 6}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 044e7c3884fefbb7a5cf6d5cdc9fba6e6f8b1e2d Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 10 Nov 2024 05:11:11 +0000 Subject: [PATCH 0916/1315] SystemUI: Add number clockface Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_clock_num.xml | 87 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml new file mode 100644 index 000000000000..550e5643d170 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index e492c51092c4..20e47b6e79d3 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -48,10 +48,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_ide, R.layout.keyguard_clock_moto, R.layout.keyguard_clock_label, - R.layout.keyguard_clock_ios + R.layout.keyguard_clock_ios, + R.layout.keyguard_clock_num }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 0e0b8cf455873ee9f5f96ff94a871eef16f3d574 Mon Sep 17 00:00:00 2001 From: DrDisagree Date: Sun, 10 Nov 2024 07:19:51 +0000 Subject: [PATCH 0917/1315] SystemUI: Add 3 new clock face from iconify Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/font/astthillabeltaden.otf | Bin 0 -> 214632 bytes .../res-keyguard/font/montserrat_bold.ttf | Bin 0 -> 29560 bytes .../layout/keyguard_clock_accent.xml | 86 ++++++++++++++++++ .../layout/keyguard_clock_mont.xml | 66 ++++++++++++++ .../layout/keyguard_clock_taden.xml | 52 +++++++++++ .../android/systemui/clocks/ClockStyle.java | 7 +- 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/astthillabeltaden.otf create mode 100644 packages/SystemUI/res-keyguard/font/montserrat_bold.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml diff --git a/packages/SystemUI/res-keyguard/font/astthillabeltaden.otf b/packages/SystemUI/res-keyguard/font/astthillabeltaden.otf new file mode 100644 index 0000000000000000000000000000000000000000..63a9380b7d5efe1f1df3889073fb197bfae1b7c9 GIT binary patch literal 214632 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/montserrat_bold.ttf b/packages/SystemUI/res-keyguard/font/montserrat_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ae33a4538132c8fc174dd53b3ce771009405d7a4 GIT binary patch literal 29560 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml new file mode 100644 index 000000000000..dbd70c43e492 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml new file mode 100644 index 000000000000..fbe12b4932e2 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml new file mode 100644 index 000000000000..83cc92afad57 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 20e47b6e79d3..61dcf998759b 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -49,10 +49,13 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_moto, R.layout.keyguard_clock_label, R.layout.keyguard_clock_ios, - R.layout.keyguard_clock_num + R.layout.keyguard_clock_num, + R.layout.keyguard_clock_taden, + R.layout.keyguard_clock_mont, + R.layout.keyguard_clock_accent }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 24932bd779cf8530af00babc551a9fc0a1e65400 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 10 Nov 2024 07:53:06 +0000 Subject: [PATCH 0918/1315] SystemUI: Add NOS3 clock style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res-keyguard/font/ntype82.ttf | Bin 0 -> 39724 bytes .../SystemUI/res-keyguard/font/subway.ttf | Bin 0 -> 70804 bytes .../layout/keyguard_clock_nos1.xml | 68 +++++++++++ .../layout/keyguard_clock_nos2.xml | 108 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 6 +- 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/ntype82.ttf create mode 100644 packages/SystemUI/res-keyguard/font/subway.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml diff --git a/packages/SystemUI/res-keyguard/font/ntype82.ttf b/packages/SystemUI/res-keyguard/font/ntype82.ttf new file mode 100644 index 0000000000000000000000000000000000000000..761cfd9c2030c590a604e3284aa0378ab0d9a0bd GIT binary patch literal 39724 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 F|39J~0Z0G< literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/subway.ttf b/packages/SystemUI/res-keyguard/font/subway.ttf new file mode 100644 index 0000000000000000000000000000000000000000..06d5cc037a2409477829f847381424403803399d GIT binary patch literal 70804 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 z + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml new file mode 100644 index 000000000000..4e0ad942506c --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 61dcf998759b..31e6cb77f17b 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -52,10 +52,12 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_num, R.layout.keyguard_clock_taden, R.layout.keyguard_clock_mont, - R.layout.keyguard_clock_accent + R.layout.keyguard_clock_accent, + R.layout.keyguard_clock_nos1, + R.layout.keyguard_clock_nos2 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 099018f4df451fb584a6b442c66536b5028a7cdf Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 19 Nov 2024 15:14:23 +0000 Subject: [PATCH 0919/1315] SystemUI:: Added life clockface Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res-keyguard/font/Cotta.otf | Bin 0 -> 14520 bytes .../res-keyguard/font/PlayfairDisplay.ttf | Bin 0 -> 176736 bytes .../layout/keyguard_clock_life.xml | 57 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/Cotta.otf create mode 100644 packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_life.xml diff --git a/packages/SystemUI/res-keyguard/font/Cotta.otf b/packages/SystemUI/res-keyguard/font/Cotta.otf new file mode 100644 index 0000000000000000000000000000000000000000..5979cec2f081199a6386af05d5d0368fa5ff9859 GIT binary patch literal 14520 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf b/packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0587b8821539c5d6197573a735734de9c9684ab1 GIT binary patch literal 176736 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

Wi^30mew_W6*-Nr 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

OE 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�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 z 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 z zvL@9aFLz?JPNCv+Eakz_xj9MG9-k`#sIk{yyMFlyzmG(RiuI!tWdu>jB zQBLmSoKE99<>b!H$tB_1RdbBmK7lG0rp%!fr93OS&Z6|C7AmQS9LjYz=i_hM!uftH z$SJMLDbA^`ES^;|zqs=1oQm0ro0+ss%k#%0I#j}zdMYr8n5(9F4p!`*O#RD|NA4RI zP-<><^@72D`Yc|&IIp&-Q1Ilb$2rY%y3|=r^j%=TE&*xx{mcwM>0t*|&JTVS~2;)R8UfOpKl1bX2P!^wVl$v7NrHEsZZ!Vb^lC6o$P9CKk;| zI<7@Dfj_Hj)ph6u*Q*=V4QiEo4vX)b)Gg{(wO<`jtJw)Z2}21s_R)>iC+a9K9#b&^ zN>>rB*lnkY&XiROI!kBkX1Y0(=Ph+B-CDQNZFM^|uYdCHRV+wybVuDuch+5WSKUo_ z*FAJk-HYvb&r~0&tvZ)Cj(v1rov#b9Coa#6zzJx%>feXRbd_UH?-p}3f@ zz+I{@V+;Mu^%eR`eU+ZBi}egWQ_s>RdbXaU=ju{DPyJ9|t;_U$H2y!Sy=s}RU~`FU zbfvD+)p{Yi|1Py%{g&_X{+@4X?bM6Zi|VWDf7Dm>VtuV%qOa4}>l^fq`X+rdUwyh& z-==TZcj!CyUHWc)kG@ymr|;Ka)DP$f`I5tz^uziQ{iuFSe_21yw_=~rPwJ=iSNQDU zGx}NmRlQU{r=Qnf(=X_+>t*^I`bGUsyt#b52?r0m-PYt zf%<{^iCU)*>c6sg+~4$J{davt|3iPs8uX9!zx2oY6Ma-iWWHNIn9VmAk__h1rjcoE zQcNlOsd)83q6I+z^O(R4DMO&8PEU`K6w zn4YGW>21z5xhBu_F?~(GDKLem$n-P)%>Xmd3^Iew5Hr*aGiRBz&2Tfqj5MRnXfwu) zHRH^9bB>u{&NUOwd1jKCY^Ip=%~W%NnPx6zUGl}|5_2hQYrkMFH&>V|%~fW)DK<0A zOf$=rnAv8InQKbTJae@vGxJTksW1yzNn2^EOto2P7O~R$TC>DlXRbFlm>bPa=4Nw? zxz*feZZ~(BJI!6@ZgY>h*W73BH(xXlm

6=1b;b^N4xWJZ8RZ9ykAEo-j}H;h?XW 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 + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 31e6cb77f17b..0c7883c9e9af 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -54,10 +54,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_mont, R.layout.keyguard_clock_accent, R.layout.keyguard_clock_nos1, - R.layout.keyguard_clock_nos2 + R.layout.keyguard_clock_nos2, + R.layout.keyguard_clock_life }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 9d4ba6451344a1a1f8414481f056e6a2a0e05733 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 05:41:22 +0000 Subject: [PATCH 0920/1315] SystemUI: Add Android Q clock face Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_clock_word.xml | 34 ++++++++ .../android/systemui/clocks/ClockStyle.java | 3 +- .../systemui/clocks/WordClockView.java | 78 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml new file mode 100644 index 000000000000..6b295877b6b8 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 0c7883c9e9af..79599498fbc5 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -55,7 +55,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_accent, R.layout.keyguard_clock_nos1, R.layout.keyguard_clock_nos2, - R.layout.keyguard_clock_life + R.layout.keyguard_clock_life, + R.layout.keyguard_clock_word }; private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; diff --git a/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java new file mode 100644 index 000000000000..62211f7b34a3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java @@ -0,0 +1,78 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class WordClockView extends TextView { + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Runnable updateTimeRunnable = new Runnable() { + @Override + public void run() { + updateClock(); + handler.postDelayed(this, 1000); + } + }; + + public WordClockView(Context context) { + super(context); + init(); + } + + public WordClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public WordClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + updateClock(); + handler.post(updateTimeRunnable); + } + + private void updateClock() { + String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date()); + String[] timeParts = currentTime.split(":"); + String hour = timeParts[0]; + String minute = timeParts[1].split(" ")[0]; + + String hourInWords = convertToWords(Integer.parseInt(hour)); + String minuteInWords = convertToWords(Integer.parseInt(minute)); + + // Set only the hour and minute in words + setText(hourInWords + "\n" + minuteInWords); + } + + private String convertToWords(int num) { + String[] words = { + "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", + "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", + "Seventeen", "Eighteen", "Nineteen", "Twenty", "Twenty-One", "Twenty-Two", + "Twenty-Three", "Twenty-Four", "Twenty-Five", "Twenty-Six", "Twenty-Seven", + "Twenty-Eight", "Twenty-Nine", "Thirty", "Thirty-One", "Thirty-Two", + "Thirty-Three", "Thirty-Four", "Thirty-Five", "Thirty-Six", "Thirty-Seven", + "Thirty-Eight", "Thirty-Nine", "Forty", "Forty-One", "Forty-Two", + "Forty-Three", "Forty-Four", "Forty-Five", "Forty-Six", "Forty-Seven", + "Forty-Eight", "Forty-Nine", "Fifty", "Fifty-One", "Fifty-Two", "Fifty-Three", + "Fifty-Four", "Fifty-Five", "Fifty-Six", "Fifty-Seven", "Fifty-Eight", + "Fifty-Nine" + }; + return num < words.length ? words[num] : ""; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacks(updateTimeRunnable); // Stop updates when view is detached + } +} From 724d3b5b211619d07925b20332b12534ec90c839 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 07:05:42 +0000 Subject: [PATCH 0921/1315] SystemUI: Add Encode clock face Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/font/EncodeSansBold.ttf | Bin 0 -> 160700 bytes .../res-keyguard/font/EncodeSansLight.ttf | Bin 0 -> 155272 bytes .../layout/keyguard_clock_encode.xml | 47 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf create mode 100644 packages/SystemUI/res-keyguard/font/EncodeSansLight.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_encode.xml diff --git a/packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf b/packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5aec42814817281c5166c0af20d9a87729d54768 GIT binary patch literal 160700 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%0AS_7nS|W0p+0bhH_dtt6WsBsH*C( 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 wd(Xfk)`3+2JI#rp>bK% + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 79599498fbc5..6d47e5df87b6 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -56,10 +56,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_nos1, R.layout.keyguard_clock_nos2, R.layout.keyguard_clock_life, - R.layout.keyguard_clock_word + R.layout.keyguard_clock_word, + R.layout.keyguard_clock_encode }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 2d10319b760031d6fdef1af47011543721dccd8c Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 10:01:29 +0000 Subject: [PATCH 0922/1315] SystemUI: Simple analog clock Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../layout/keyguard_clock_analog.xml | 29 ++++ .../layout/keyguard_clock_nos3.xml | 29 ++++ .../systemui/clocks/AnalogClockView.java | 100 +++++++++++++ .../android/systemui/clocks/ClockStyle.java | 6 +- .../clocks/NothingAnalogClockView.java | 141 ++++++++++++++++++ 5 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml new file mode 100644 index 000000000000..bb0d2a05dce7 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml new file mode 100644 index 000000000000..aaf8e92a8dc1 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java new file mode 100644 index 000000000000..274701adafa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java @@ -0,0 +1,100 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class AnalogClockView extends View { + private Paint mPaint; + private float centerX, centerY, radius; + private final Handler handler = new Handler(Looper.getMainLooper()); + + public AnalogClockView(Context context) { + super(context); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges + handler.postDelayed(new Runnable() { + @Override + public void run() { + invalidate(); // Redraw the view + handler.postDelayed(this, 1000); // Update every second + } + }, 1000); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + centerX = width / 2; + centerY = height / 2; + radius = Math.min(centerX, centerY) - 20; // Leave margin + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Keep the wallpaper background (no transparent background here) + + // Draw clock face with hour markers + mPaint.setColor(0xFFFFFFFF); // White color for hour markers + mPaint.setStyle(Paint.Style.FILL); + mPaint.setStrokeWidth(5); + for (int i = 0; i < 12; i++) { + float angle = (float) (Math.PI / 6 * i); // 30 degrees per hour + float startX = (float) (centerX + radius * 0.85 * Math.cos(angle)); + float startY = (float) (centerY + radius * 0.85 * Math.sin(angle)); + float endX = (float) (centerX + radius * 0.95 * Math.cos(angle)); + float endY = (float) (centerY + radius * 0.95 * Math.sin(angle)); + canvas.drawRect(startX - 5, startY - 5, endX + 5, endY + 5, mPaint); + } + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + + // Draw hour hand + mPaint.setColor(0xFFFFFFFF); // White color for clock hands + mPaint.setStrokeWidth(12); + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(hourAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.5f, mPaint); + canvas.restore(); + + // Draw minute hand + mPaint.setStrokeWidth(8); + float minuteAngle = (float) (Math.PI / 30 * minute); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(minuteAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.7f, mPaint); + canvas.restore(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacksAndMessages(null); // Stop updates when detached + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 6d47e5df87b6..0ca6757f5ac3 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -57,10 +57,12 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_nos2, R.layout.keyguard_clock_life, R.layout.keyguard_clock_word, - R.layout.keyguard_clock_encode + R.layout.keyguard_clock_encode, + R.layout.keyguard_clock_nos3, + R.layout.keyguard_clock_analog }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; diff --git a/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java new file mode 100644 index 000000000000..8faeb31fe565 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java @@ -0,0 +1,141 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class NothingAnalogClockView extends View { + + private Paint mPaint; + private float centerX, centerY, width, height; + + public NothingAnalogClockView(Context context) { + super(context); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges for drawing + mPaint.setStrokeCap(Paint.Cap.ROUND); // Rounded ends for clock hands + + // Refresh the clock every second + postInvalidateOnAnimation(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + this.width = width; + this.height = height; + + centerX = width / 2f; + centerY = height / 2f; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + int second = calendar.get(Calendar.SECOND); + + // Draw clock ticks in a rectangular shape + drawRectangularTicks(canvas); + + // Draw hour hand + drawHourHand(canvas, hour, minute); + + // Draw minute hand + drawMinuteHand(canvas, minute); + + // Draw second hand + drawSecondHand(canvas, second); + + // Trigger continuous updates + postInvalidateDelayed(16); // Smooth 60 FPS animation + } + + private void drawRectangularTicks(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(0xFFFFFFFF); // White color for ticks + + // Rectangular bounds (inset from edges) + float horizontalInset = width * 0.1f; + float verticalInset = height * 0.2f; + + for (int i = 0; i < 60; i++) { + float angle = (float) (Math.PI / 30 * i); + float startX, startY, endX, endY; + + if (i % 5 == 0) { + // Major ticks (longer and thicker) + mPaint.setStrokeWidth(6); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.5) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.5) * Math.sin(angle)); + } else { + // Minor ticks (shorter and thinner) + mPaint.setStrokeWidth(2); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.2) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.2) * Math.sin(angle)); + } + + canvas.drawLine(startX, startY, endX, endY, mPaint); + } + } + + private void drawHourHand(Canvas canvas, int hour, int minute) { + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); + mPaint.setColor(0xFFFFFFFF); // White color for hour hand + mPaint.setStrokeWidth(12); + + float endX = (float) (centerX + (width * 0.25f) * Math.cos(hourAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.25f) * Math.sin(hourAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawMinuteHand(Canvas canvas, int minute) { + float minuteAngle = (float) (Math.PI / 30 * minute); + mPaint.setColor(0xFFFFFFFF); // White color for minute hand + mPaint.setStrokeWidth(8); + + float endX = (float) (centerX + (width * 0.4f) * Math.cos(minuteAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.4f) * Math.sin(minuteAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawSecondHand(Canvas canvas, int second) { + float secondAngle = (float) (Math.PI / 30 * second); + mPaint.setColor(0xFFFF0000); // Red color for second hand + mPaint.setStrokeWidth(4); + + float endX = (float) (centerX + (width * 0.45f) * Math.cos(secondAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.45f) * Math.sin(secondAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } +} From 628bf4353e894de2fd0b6fcdcd53fcaeafcbcf8e Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 10 Apr 2025 16:37:44 +0000 Subject: [PATCH 0923/1315] SystemUI: Lock Screen Clock Accent Color Option Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 12 +++++ .../android/systemui/clocks/ClockStyle.java | 54 +++++++++++++++++-- .../theme/RisingSettingsConstants.java | 2 + 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index af30fe6489f3..16c6a46eb59e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14426,6 +14426,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NAVBAR_IME_SPACE = "navbar_ime_space"; + /** + * Whether to use system accent color for lock screen clock text + * @hide + */ + public static final String CLOCK_TEXT_ACCENT_COLOR = "clock_text_accent_color"; + + /** + * Lock screen clock text opacity (0-100) + * @hide + */ + public static final String CLOCK_TEXT_OPACITY = "clock_text_opacity"; + /** * Whether to show an overlay in the bottom corner of the screen on copying stuff * into the clipboard. diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 0ca6757f5ac3..d4f76b78d13a 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -66,13 +66,19 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; + public static final String CLOCK_TEXT_COLOR_KEY = "clock_text_accent_color"; + public static final String CLOCK_TEXT_OPACITY_KEY = "clock_text_opacity"; + + private static final int DEFAULT_OPACITY = 100; private final Context mContext; private final KeyguardManager mKeyguardManager; private final TunerService mTunerService; private View currentClockView; - private int mClockStyle; + private int mClockStyle; + private boolean mUseAccentColor = false; + private int mClockOpacity = DEFAULT_OPACITY; private static final long UPDATE_INTERVAL_MILLIS = 15 * 1000; private long lastUpdateTimeMillis = 0; @@ -138,7 +144,7 @@ public ClockStyle(Context context, AttributeSet attrs) { mContext = context; mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); mTunerService = Dependency.get(TunerService.class); - mTunerService.addTunable(this, CLOCK_STYLE_KEY); + mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); mStatusBarStateController = Dependency.get(StatusBarStateController.class); mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); @@ -202,6 +208,37 @@ public void onTimeChanged() { } } + private void updateClockTextColor() { + if (currentClockView != null) { + updateTextClockColor(currentClockView); + } + } + + private void updateTextClockColor(View view) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View childView = viewGroup.getChildAt(i); + updateTextClockColor(childView); + } + } + + if (view instanceof TextClock) { + TextClock textClock = (TextClock) view; + int color; + if (mUseAccentColor) { + color = mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + } else { + color = mContext.getColor(android.R.color.white); + } + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); + } + } + private void updateClockView() { if (currentClockView != null) { ((ViewGroup) currentClockView.getParent()).removeView(currentClockView); @@ -216,6 +253,7 @@ private void updateClockView() { if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); } + updateClockTextColor(); } } onTimeChanged(); @@ -233,6 +271,16 @@ public void onTuningChanged(String key, String newValue) { } updateClockView(); break; + case CLOCK_TEXT_COLOR_KEY: + mUseAccentColor = TunerService.parseIntegerSwitch(newValue, false); + updateClockTextColor(); + break; + case CLOCK_TEXT_OPACITY_KEY: + mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); + // Keep opacity within valid range (0-100) + mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); + updateClockTextColor(); + break; } } @@ -244,4 +292,4 @@ private boolean isCenterClock(int clockStyle) { } return false; } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java index 2f2b8f734d7b..5df126e7b9f1 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java +++ b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java @@ -26,6 +26,8 @@ public class RisingSettingsConstants { }; public static final String[] SECURE_SETTINGS_KEYS = { + "clock_text_accent_color", + "clock_text_opacity" }; public static final String[] SYSTEM_SETTINGS_NOTIFY_ONLY_KEYS = { From 9a4d477fa5c4ddda2592af5789f050d19d7a119a Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 23 May 2025 10:28:16 +0000 Subject: [PATCH 0924/1315] SystemUI: Improve clock face color option Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/ids.xml | 4 +++ .../android/systemui/clocks/ClockStyle.java | 31 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 84e84bd0cc79..d85b938ec771 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -311,4 +311,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index d4f76b78d13a..3716b8b7f67e 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -225,17 +225,28 @@ private void updateTextClockColor(View view) { if (view instanceof TextClock) { TextClock textClock = (TextClock) view; - int color; - if (mUseAccentColor) { - color = mContext.getColor( - mContext.getResources().getIdentifier( - "system_accent1_100", "color", "android")); - } else { - color = mContext.getColor(android.R.color.white); + + if (textClock.getTag(R.id.original_text_color) == null) { + int currentColor = textClock.getCurrentTextColor(); + textClock.setTag(R.id.original_text_color, currentColor); + } + + int originalColor = (Integer) textClock.getTag(R.id.original_text_color); + int whiteColor = mContext.getColor(android.R.color.white); + + if ((originalColor & 0x00FFFFFF) == (whiteColor & 0x00FFFFFF)) { + int color; + if (mUseAccentColor) { + color = mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + } else { + color = mContext.getColor(android.R.color.white); + } + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); } - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); } } From b8af5b52cb161e821518214e5452936782565763 Mon Sep 17 00:00:00 2001 From: Lixkote <95425619+Lixkote@users.noreply.github.com> Date: Tue, 6 May 2025 17:26:28 +0000 Subject: [PATCH 0925/1315] SystemUI: Added android 9 style lockscreen clock Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/layout/keyguard_clock_a9.xml | 44 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 ++- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml new file mode 100644 index 000000000000..2d093c0d9fa4 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 3716b8b7f67e..a92e96c882e0 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -59,10 +59,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_word, R.layout.keyguard_clock_encode, R.layout.keyguard_clock_nos3, - R.layout.keyguard_clock_analog + R.layout.keyguard_clock_analog, + R.layout.keyguard_clock_a9 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 4cd23358d424fd40e34288e627357f586b15c27f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 11:32:27 +0000 Subject: [PATCH 0926/1315] SystemUI: allowlist ACCESS_NOTIFICATIONS privileged permission Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- data/etc/com.android.systemui.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 7f89fd5fad51..4d8c29a816ec 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -100,5 +100,6 @@ + From 29280df39fe7ca16b2a1fbc0258149b5705ee8f5 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 12:17:37 +0000 Subject: [PATCH 0927/1315] SystemUI: Set life clock at middle Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/src/com/android/systemui/clocks/ClockStyle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index a92e96c882e0..a9b488546501 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -63,7 +63,7 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_a9 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 7beb6364da6bdc22b43e6a8ab75502def91d2a50 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 06:32:23 +0000 Subject: [PATCH 0928/1315] SystemUI: Use small clock when setset clock style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../data/repository/KeyguardClockRepository.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index 6d4d4f2e1e1b..bc9bdde3ff24 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -100,7 +100,19 @@ constructor( override val forcedClockSize: Flow = if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { configurationRepository.onAnyConfigurationChange.map { - if (context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen)) { + if ( + context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + secureSettings.getIntForUser( + "clock_style", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 || + systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + ) { ClockSize.SMALL } else { null From d90dead15f30bf2f67da92349122e937b4294a97 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 06:33:51 +0000 Subject: [PATCH 0929/1315] SystemUI: Hide clock properly when clock style set Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/view/layout/sections/ClockSection.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 6b278212c50a..53aff4c6232e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.os.UserHandle +import android.provider.Settings import android.view.View import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout @@ -121,6 +123,19 @@ constructor( setAlpha(getTargetClockFace(clock).views, 1F) setAlpha(getNonTargetClockFace(clock).views, 0F) + // Hide small clock when custom clock is enabled by setting alpha to 0 + val isCustomClockEnabled = Settings.Secure.getIntForUser( + context.contentResolver, + "clock_style", + 0, + UserHandle.USER_CURRENT + ) != 0 + + if (isCustomClockEnabled) { + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, 0F) + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, 0F) + } + if (!keyguardClockViewModel.isLargeClockVisible.value) { if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { connect( From b4918e41f84db4239664530b50afff134c0baf00 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 09:57:56 +0000 Subject: [PATCH 0930/1315] SystemUI: Fix default clock omnijaw view Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/view/layout/sections/KeyguardWeatherViewSection.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt index 07ee85aadd9e..e9898053550b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt @@ -21,7 +21,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.R as custR +import com.android.systemui.customization.clocks.R as custR import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.weather.WeatherImageView @@ -69,7 +69,7 @@ class KeyguardWeatherViewSection @Inject constructor( ConstraintLayout.LayoutParams.WRAP_CONTENT ) setTextColor(context.getColor(android.R.color.white)) - textSize = 20f + textSize = 16f visibility = View.GONE } @@ -139,11 +139,9 @@ class KeyguardWeatherViewSection @Inject constructor( private fun applyWeatherTextConstraints(constraintSet: ConstraintSet) { if (constraintSet.getConstraint(R.id.default_weather_text) != null) { constraintSet.apply { - connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, - context.resources.getDimensionPixelSize(R.dimen.weather_text_margin_start)) + connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, 12) connect(R.id.default_weather_text, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.TOP) connect(R.id.default_weather_text, ConstraintSet.BOTTOM, R.id.default_weather_image, ConstraintSet.BOTTOM) - connect(R.id.default_weather_text, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) constrainHeight(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) constrainWidth(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) } From 9e272f9af1d74cc58a237487dc751aa1517d6fff Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 27 Feb 2026 06:39:10 +0000 Subject: [PATCH 0931/1315] SystemUI: Adapt weather view to updated omnijaw controller - separate switch Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../sections/KeyguardWeatherViewSection.kt | 4 +-- .../systemui/weather/WeatherImageView.kt | 28 ++++++++++------ .../systemui/weather/WeatherTextView.kt | 26 +++++++-------- .../systemui/weather/WeatherViewController.kt | 33 +++++++++++-------- 4 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt index e9898053550b..4a43208d64d6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt @@ -53,7 +53,7 @@ class KeyguardWeatherViewSection @Inject constructor( } private fun createWeatherViews(constraintLayout: ConstraintLayout) { - weatherImageView = WeatherImageView(context).apply { + weatherImageView = WeatherImageView(context, isCustomClock = false).apply { id = R.id.default_weather_image layoutParams = ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, @@ -62,7 +62,7 @@ class KeyguardWeatherViewSection @Inject constructor( visibility = View.GONE } - weatherTextView = WeatherTextView(context).apply { + weatherTextView = WeatherTextView(context, isCustomClock = false).apply { id = R.id.default_weather_text layoutParams = ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt index c6c3fba59ede..d8b151f52857 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt @@ -17,37 +17,45 @@ package com.android.systemui.weather import android.content.Context import android.util.AttributeSet -import android.util.DisplayMetrics import android.view.View import android.view.View.MeasureSpec import android.widget.ImageView +import android.widget.TextView import com.android.systemui.res.R class WeatherImageView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyle: Int = 0 + defStyle: Int = 0, + isCustomClock: Boolean = true, ) : ImageView(context, attrs, defStyle) { - private val maxSizePx: Int = context.resources.getDimension(R.dimen.weather_image_max_size).toInt() - private val weatherViewController: WeatherViewController = WeatherViewController(context, this, null, null) + private val maxSizePx: Int = + context.resources.getDimension(R.dimen.weather_image_max_size).toInt() + + private val weatherViewController: WeatherViewController init { visibility = View.GONE - } - - fun setWeatherEnabled(enabled: Boolean) { - visibility = if (enabled) View.VISIBLE else View.GONE + + val stubText = TextView(context) + + weatherViewController = WeatherViewController( + context = context, + weatherIcon = this, + weatherTemp = stubText, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) } override fun onAttachedToWindow() { super.onAttachedToWindow() - weatherViewController.updateWeatherSettings() + weatherViewController.init() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - weatherViewController.disableUpdates() weatherViewController.removeObserver() } diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt index e48d23424cc6..a603256bf48c 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt @@ -24,35 +24,33 @@ import com.android.systemui.res.R class WeatherTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyle: Int = 0 + defStyle: Int = 0, + isCustomClock: Boolean = true, ) : TextView(context, attrs, defStyle) { private val mWeatherViewController: WeatherViewController - private val mWeatherText: String? init { - val a = context.obtainStyledAttributes(attrs, R.styleable.WeatherTextView, defStyle, 0) - mWeatherText = a.getString(R.styleable.WeatherTextView_weatherText) - a.recycle() - - mWeatherViewController = WeatherViewController(context, null, this, mWeatherText) - - text = if (!mWeatherText.isNullOrEmpty()) mWeatherText else "" visibility = View.GONE - } - fun setWeatherEnabled(enabled: Boolean) { - visibility = if (enabled) View.VISIBLE else View.GONE + val stubIcon = android.widget.ImageView(context) + + mWeatherViewController = WeatherViewController( + context = context, + weatherIcon = stubIcon, + weatherTemp = this, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) } override fun onAttachedToWindow() { super.onAttachedToWindow() - mWeatherViewController.updateWeatherSettings() + mWeatherViewController.init() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - mWeatherViewController.disableUpdates() mWeatherViewController.removeObserver() } } diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index a13a268cd4fd..5328267d20ed 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -38,6 +38,7 @@ class WeatherViewController( private val weatherIcon: ImageView, private val weatherTemp: TextView, private val weatherInfoView: View, + private val isCustomClock: Boolean = false, ) : OmniJawsClient.OmniJawsObserver { private var weatherInfo: OmniJawsClient.WeatherInfo? = null @@ -91,11 +92,11 @@ class WeatherViewController( private fun getWeatherSettings() = WeatherSettings( weatherEnabled = getSystemSetting(LOCKSCREEN_WEATHER_ENABLED), - clockFaceEnabled = getSecureSetting(CLOCK_STYLE), showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), - showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO) + showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO), + customClockWeather = getSecureSetting(CUSTOM_CLOCK_WEATHER, defaultValue = 1), ) private fun getSystemSetting(setting: String, defaultValue: Int = 0) = @@ -112,14 +113,6 @@ class WeatherViewController( OmniJawsClient.get().addObserver(context, this@WeatherViewController) updateWeather() showAllViews() - // When a clock face style is active, hide the default weather views - // so they don't overlap the clock face layout (mirrors risingOS behaviour). - if (weatherIcon.id == R.id.default_weather_image) { - scope.launch { updateViewVisibility(weatherIcon, !settings.clockFaceEnabled) } - } - if (weatherTemp.id == R.id.default_weather_text) { - scope.launch { updateViewVisibility(weatherTemp, !settings.clockFaceEnabled) } - } } } @@ -153,6 +146,8 @@ class WeatherViewController( weatherTemp.isSelected = true } } catch (e: Exception) {} + + applyElementVisibility(weatherSettingsFlow.value) } private fun hideAllViews() { @@ -168,6 +163,18 @@ class WeatherViewController( listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { updateViewVisibility(it, true) } + applyElementVisibility(weatherSettingsFlow.value) + } + } + + private fun applyElementVisibility(settings: WeatherSettings) { + scope.launch { + val show = settings.weatherEnabled && ( + if (isCustomClock) settings.customClockWeather + else !settings.customClockWeather + ) + updateViewVisibility(weatherIcon, show) + updateViewVisibility(weatherTemp, show) } } @@ -206,11 +213,11 @@ class WeatherViewController( data class WeatherSettings( val weatherEnabled: Boolean, - val clockFaceEnabled: Boolean, val showWeatherLocation: Boolean, val showWeatherText: Boolean, val showWindInfo: Boolean, - val showHumidityInfo: Boolean + val showHumidityInfo: Boolean, + val customClockWeather: Boolean, ) companion object { @@ -219,7 +226,7 @@ class WeatherViewController( private const val LOCKSCREEN_WEATHER_TEXT = "lockscreen_weather_text" private const val LOCKSCREEN_WEATHER_WIND_INFO = "lockscreen_weather_wind_info" private const val LOCKSCREEN_WEATHER_HUMIDITY_INFO = "lockscreen_weather_humidity_info" - private const val CLOCK_STYLE = "clock_style" + const val CUSTOM_CLOCK_WEATHER = "custom_clock_weather" private val WEATHER_CONDITIONS = mapOf( "clouds" to R.string.weather_condition_clouds, From ec75c84d5446f6dbf44e865026a1d1a47c9766ca Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 27 Feb 2026 07:58:30 +0000 Subject: [PATCH 0932/1315] SystemUI: Refactor clock and widget implementation Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/clocks/ClockStyle.java | 67 +++++++++++++------ .../repository/KeyguardClockRepository.kt | 5 ++ .../view/layout/sections/AODStyleSection.kt | 34 ++-------- .../sections/KeyguardClockStyleSection.kt | 26 +++++-- .../sections/KeyguardWeatherViewSection.kt | 8 ++- .../sections/KeyguardWidgetViewSection.kt | 27 ++------ .../LockScreenWidgetsController.java | 22 ++++-- 7 files changed, 106 insertions(+), 83 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index a9b488546501..dabb9342b613 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -25,8 +25,10 @@ import android.provider.Settings; import android.util.AttributeSet; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.ViewStub; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -77,6 +79,9 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private final TunerService mTunerService; private View currentClockView; + private ViewStub mClockStub; + private ViewGroup mClockContainer; + private int mClockStyle; private boolean mUseAccentColor = false; private int mClockOpacity = DEFAULT_OPACITY; @@ -95,6 +100,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private int mCurrentShiftX = 0; private int mCurrentShiftY = 0; + private boolean mCallbacksRegistered = false; + private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -145,31 +152,43 @@ public ClockStyle(Context context, AttributeSet attrs) { mContext = context; mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); mTunerService = Dependency.get(TunerService.class); - mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); mStatusBarStateController = Dependency.get(StatusBarStateController.class); - mStatusBarStateController.addCallback(mStatusBarStateListener); - mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_TIME_TICK); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction("com.android.systemui.doze.pulse"); - mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); } @Override protected void onFinishInflate() { super.onFinishInflate(); + mClockStub = findViewById(R.id.clock_view_stub); updateClockView(); } - + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!mCallbacksRegistered) { + mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction("com.android.systemui.doze.pulse"); + mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); + mCallbacksRegistered = true; + } + } + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mStatusBarStateController.removeCallback(mStatusBarStateListener); - mTunerService.removeTunable(this); - mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); - mContext.unregisterReceiver(mScreenReceiver); + if (mCallbacksRegistered) { + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + mContext.unregisterReceiver(mScreenReceiver); + mCallbacksRegistered = false; + } } private void startBurnInProtection() { @@ -253,14 +272,24 @@ private void updateTextClockColor(View view) { private void updateClockView() { if (currentClockView != null) { - ((ViewGroup) currentClockView.getParent()).removeView(currentClockView); + ViewParent parent = currentClockView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(currentClockView); + } currentClockView = null; } if (mClockStyle > 0 && mClockStyle < CLOCK_LAYOUTS.length) { - ViewStub stub = findViewById(R.id.clock_view_stub); - if (stub != null) { - stub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); - currentClockView = stub.inflate(); + if (mClockStub != null) { + mClockStub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); + currentClockView = mClockStub.inflate(); + mClockContainer = (ViewGroup) currentClockView.getParent(); + mClockStub = null; + } else if (mClockContainer != null) { + currentClockView = LayoutInflater.from(mContext) + .inflate(CLOCK_LAYOUTS[mClockStyle], mClockContainer, false); + mClockContainer.addView(currentClockView); + } + if (currentClockView != null) { int gravity = isCenterClock(mClockStyle) ? Gravity.CENTER : Gravity.START; if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index bc9bdde3ff24..816b003cd53d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -184,6 +184,11 @@ constructor( UserHandle.USER_CURRENT, ) ) + val isDoubleLineClock = secureSettings.getIntForUser( + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, + 1, // Default value + UserHandle.USER_CURRENT + ) val clockStyleEnabled = secureSettings.getIntForUser( "clock_style", 0, // Default value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt index 6681e9cd2247..c44fd2beaee0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt @@ -111,34 +111,14 @@ constructor( ) } - // Apply consistent top margin to clock_ls whether AOD is visible or not if (constraintSet.getConstraint(R.id.clock_ls) != null) { - // If AOD is present, position clock below it - // If AOD is hidden/gone, ensure clock still has proper top margin - val clockTopMargin = if (aodStyleView?.visibility == View.VISIBLE) { - 8 // Small gap below AOD - } else { - topMargin // Same margin as AOD would have had - } - - if (aodStyleView?.visibility == View.VISIBLE) { - connect( - R.id.clock_ls, - ConstraintSet.TOP, - R.id.aod_ls, - ConstraintSet.BOTTOM, - clockTopMargin - ) - } else { - // When AOD is hidden, position clock at top with proper margin - connect( - R.id.clock_ls, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - clockTopMargin - ) - } + connect( + R.id.clock_ls, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) } // Update the barrier to include AOD style for proper notification positioning diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt index 8402a9066dc0..bb22a0367296 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.os.UserHandle +import android.util.Log import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier @@ -37,13 +38,18 @@ constructor( private var clockStyleView: ClockStyle? = null private var isCustomClockEnabled: Boolean = false + + private val TAG = "KeyguardClockStyleSection" override fun addViews(constraintLayout: ConstraintLayout) { - - val clockStyle = secureSettings.getIntForUser( - ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT - ) - isCustomClockEnabled = clockStyle != 0 + isCustomClockEnabled = try { + val clockStyle = secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) + clockStyle != 0 + } catch (e: Exception) { + false + } if (!isCustomClockEnabled) return @@ -73,7 +79,15 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!isCustomClockEnabled) return + val currentlyEnabled = try { + secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) != 0 + } catch (e: Exception) { + isCustomClockEnabled + } + + if (!currentlyEnabled) return constraintSet.apply { // Clock positioning - TOP of hierarchy diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt index 4a43208d64d6..3e3319dccae5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt @@ -42,9 +42,11 @@ class KeyguardWeatherViewSection @Inject constructor( if (weatherContainer != null) { weatherImageView = weatherContainer.findViewById(R.id.default_weather_image) weatherTextView = weatherContainer.findViewById(R.id.default_weather_text) - - (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) - constraintLayout.addView(weatherContainer) + + if (weatherContainer.parent !== constraintLayout) { + (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) + constraintLayout.addView(weatherContainer) + } } else { createWeatherViews(constraintLayout) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt index 0e26d298d258..4a46d9e1e65b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -160,9 +160,7 @@ constructor( var positioned = false for (anchorId in anchorViews) { - try { - // Check if the constraint exists by trying to get it - constraintSet.getConstraint(anchorId) + if (constraintSet.getConstraint(anchorId) != null) { connect( R.id.keyguard_widgets, ConstraintSet.TOP, @@ -173,9 +171,6 @@ constructor( positioned = true Log.d(TAG, "Positioned widgets below anchor: ${context.resources.getResourceEntryName(anchorId)}") break - } catch (e: Exception) { - // Continue to next anchor if this one fails - continue } } @@ -204,7 +199,6 @@ constructor( // Update barrier to include widgets try { - val barrierViews = mutableListOf() val potentialBarrierViews = listOf( R.id.keyguard_slice_view, R.id.keyguard_weather, @@ -212,29 +206,22 @@ constructor( R.id.keyguard_info_widgets, R.id.keyguard_widgets ) - - potentialBarrierViews.forEach { viewId -> - try { - constraintSet.getConstraint(viewId) - barrierViews.add(viewId) - } catch (e: Exception) { - // Skip this view if it doesn't exist - } - } + val barrierViews = potentialBarrierViews + .filter { constraintSet.getConstraint(it) != null } + .toIntArray() if (barrierViews.isNotEmpty()) { createBarrier( R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, - *barrierViews.toIntArray() + *barrierViews ) Log.d(TAG, "Created barrier with ${barrierViews.size} views") } // Position notification icons below barrier - try { - constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { connect( R.id.left_aligned_notification_icon_container, ConstraintSet.TOP, @@ -246,8 +233,6 @@ constructor( 8 // fallback margin } ) - } catch (e: Exception) { - // Notification container doesn't exist, skip } } catch (e: Exception) { Log.w(TAG, "Failed to create barrier", e) diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index 3b1bd0044de3..3990cc8df607 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -33,6 +33,7 @@ import android.media.session.MediaSessionLegacyHelper; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; @@ -198,7 +199,7 @@ public void onThemeChanged() { }; private final View mView; - private final Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); public LockScreenWidgetsController(View view) { mView = view; @@ -218,7 +219,6 @@ public LockScreenWidgetsController(View view) { mActivityLauncherUtils = new ActivityLauncherUtils(mContext); mLockscreenWidgetsObserver = new LockscreenWidgetsObserver(); - mLockscreenWidgetsObserver.observe(); mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); @@ -231,9 +231,12 @@ public LockScreenWidgetsController(View view) { } try { - mCameraId = mCameraManager.getCameraIdList()[0]; + String[] cameraIds = mCameraManager.getCameraIdList(); + if (cameraIds != null && cameraIds.length > 0) { + mCameraId = cameraIds[0]; + } } catch (Exception e) {} - + IntentFilter ringerFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); mContext.registerReceiver(mRingerModeReceiver, ringerFilter); } @@ -302,6 +305,7 @@ public void registerCallbacks() { mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); mMediaSessionManagerHelper.addMediaMetadataListener(this); + mLockscreenWidgetsObserver.observe(); updateWidgetViews(); updateMediaPlaybackState(); } @@ -327,7 +331,10 @@ public void unregisterCallbacks() { } mConfigurationController.removeCallback(mConfigurationListener); mStatusBarStateController.removeCallback(mStatusBarStateListener); - mContext.unregisterReceiver(mRingerModeReceiver); + try { + mContext.unregisterReceiver(mRingerModeReceiver); + } catch (IllegalArgumentException e) { + } mLockscreenWidgetsObserver.unobserve(); mHandler.removeCallbacksAndMessages(null); mMediaSessionManagerHelper.removeMediaMetadataListener(this); @@ -702,7 +709,8 @@ private void toggleMediaPlaybackState() { private void showMediaDialog(View view) { String lastMediaPkg = getLastUsedMedia(); - if (TextUtils.isEmpty(lastMediaPkg)) return; // Return if null or empty + if (TextUtils.isEmpty(lastMediaPkg)) return; + if (!(mView instanceof LockScreenWidgets)) return; mHandler.post(() -> { ((LockScreenWidgets) mView).showMediaDialog(view, lastMediaPkg); VibrationUtils.triggerVibration(mContext, 2); // Trigger vibration @@ -1050,7 +1058,7 @@ public void onPlaybackStateChanged() { private class LockscreenWidgetsObserver extends ContentObserver { public LockscreenWidgetsObserver() { - super(null); + super(new Handler(Looper.getMainLooper())); } @Override public void onChange(boolean selfChange) { From f26365719142e30b4bd2f03afe858b5434676194 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 12:24:59 +0000 Subject: [PATCH 0933/1315] SystemUI: Allow adjust height of lockscreen clock styles [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/layout/keyguard_clock_a9.xml | 1 + .../layout/keyguard_clock_accent.xml | 1 + .../layout/keyguard_clock_analog.xml | 1 + .../layout/keyguard_clock_center.xml | 1 + .../layout/keyguard_clock_encode.xml | 1 + .../layout/keyguard_clock_ide.xml | 1 + .../layout/keyguard_clock_ios.xml | 1 + .../layout/keyguard_clock_label.xml | 1 + .../layout/keyguard_clock_life.xml | 1 + .../layout/keyguard_clock_miui.xml | 3 ++- .../layout/keyguard_clock_mont.xml | 1 + .../layout/keyguard_clock_moto.xml | 1 + .../layout/keyguard_clock_nos1.xml | 1 + .../layout/keyguard_clock_nos2.xml | 1 + .../layout/keyguard_clock_nos3.xml | 1 + .../layout/keyguard_clock_num.xml | 1 + .../layout/keyguard_clock_oos.xml | 1 + .../layout/keyguard_clock_simple.xml | 1 + .../layout/keyguard_clock_taden.xml | 1 + .../android/systemui/clocks/ClockStyle.java | 26 ++++++++++++++++--- 20 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml index 2d093c0d9fa4..ae8523cb5091 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml @@ -1,5 +1,6 @@ diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml index e9b816442596..589fc86a7a51 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml @@ -1,6 +1,7 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/clock_frame"> Date: Fri, 27 Feb 2026 12:47:55 +0000 Subject: [PATCH 0934/1315] SystemUI: Add support custom clock color [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/clocks/ClockStyle.java | 117 +++++++++++------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 3bfcf41c3eb3..143ebd79013c 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Color; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; @@ -67,14 +68,20 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; - private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; - public static final String CLOCK_TEXT_COLOR_KEY = "clock_text_accent_color"; + public static final String CLOCK_COLOR_MODE_KEY = "clock_color_mode"; + public static final String CLOCK_CUSTOM_COLOR_KEY = "clock_custom_color"; public static final String CLOCK_TEXT_OPACITY_KEY = "clock_text_opacity"; public static final String CLOCK_FRAME_MARGIN_TOP_KEY = "custom_clock_frame_margin_top"; + public static final String COLOR_MODE_DEFAULT = "default"; + public static final String COLOR_MODE_ACCENT = "accent"; + public static final String COLOR_MODE_CUSTOM = "custom"; + + private static final int DEFAULT_STYLE = 0; // Disabled private static final int DEFAULT_OPACITY = 100; private static final int DEFAULT_MARGIN_TOP = 15; + private static final int DEFAULT_CUSTOM_COLOR = Color.WHITE; private final Context mContext; private final KeyguardManager mKeyguardManager; @@ -84,8 +91,9 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private ViewStub mClockStub; private ViewGroup mClockContainer; - private int mClockStyle; - private boolean mUseAccentColor = false; + private int mClockStyle; + private String mColorMode = COLOR_MODE_DEFAULT; + private int mCustomColor = DEFAULT_CUSTOM_COLOR; private int mClockOpacity = DEFAULT_OPACITY; private int mClockFrameMarginTop = DEFAULT_MARGIN_TOP; @@ -169,8 +177,12 @@ protected void onFinishInflate() { protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!mCallbacksRegistered) { - mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, - CLOCK_TEXT_OPACITY_KEY, CLOCK_FRAME_MARGIN_TOP_KEY); + mTunerService.addTunable(this, + CLOCK_STYLE_KEY, + CLOCK_COLOR_MODE_KEY, + CLOCK_CUSTOM_COLOR_KEY, + CLOCK_TEXT_OPACITY_KEY, + CLOCK_FRAME_MARGIN_TOP_KEY); mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); IntentFilter filter = new IntentFilter(); @@ -232,19 +244,23 @@ public void onTimeChanged() { } } - private void updateClockTextColor() { - if (currentClockView != null) { - updateTextClockColor(currentClockView); + private int resolveClockColor() { + switch (mColorMode) { + case COLOR_MODE_ACCENT: + return mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + case COLOR_MODE_CUSTOM: + return mCustomColor; + case COLOR_MODE_DEFAULT: + default: + return mContext.getColor(android.R.color.white); } } - private void updateClockFrameMargin() { - RelativeLayout clockFrame = findViewById(R.id.clock_frame); - if (clockFrame != null) { - ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) clockFrame.getLayoutParams(); - int marginPx = (int) (mClockFrameMarginTop * mContext.getResources().getDisplayMetrics().density); - params.topMargin = marginPx; - clockFrame.setLayoutParams(params); + private void updateClockTextColor() { + if (currentClockView != null) { + updateTextClockColor(currentClockView); } } @@ -252,35 +268,37 @@ private void updateTextClockColor(View view) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { - View childView = viewGroup.getChildAt(i); - updateTextClockColor(childView); + updateTextClockColor(viewGroup.getChildAt(i)); } } - if (view instanceof TextClock) { - TextClock textClock = (TextClock) view; - - if (textClock.getTag(R.id.original_text_color) == null) { - int currentColor = textClock.getCurrentTextColor(); - textClock.setTag(R.id.original_text_color, currentColor); - } - - int originalColor = (Integer) textClock.getTag(R.id.original_text_color); - int whiteColor = mContext.getColor(android.R.color.white); - - if ((originalColor & 0x00FFFFFF) == (whiteColor & 0x00FFFFFF)) { - int color; - if (mUseAccentColor) { - color = mContext.getColor( - mContext.getResources().getIdentifier( - "system_accent1_100", "color", "android")); - } else { - color = mContext.getColor(android.R.color.white); - } - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); - } + if (!(view instanceof TextClock)) return; + TextClock textClock = (TextClock) view; + + if (textClock.getTag(R.id.original_text_color) == null) { + textClock.setTag(R.id.original_text_color, textClock.getCurrentTextColor()); + } + + int originalColor = (Integer) textClock.getTag(R.id.original_text_color); + int whiteColor = mContext.getColor(android.R.color.white); + + if ((originalColor & 0x00FFFFFF) != (whiteColor & 0x00FFFFFF)) return; + + int color = resolveClockColor(); + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); + } + + private void updateClockFrameMargin() { + View clockFrame = findViewById(R.id.clock_frame); + if (clockFrame != null) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) clockFrame.getLayoutParams(); + int marginPx = (int) (mClockFrameMarginTop + * mContext.getResources().getDisplayMetrics().density); + params.topMargin = marginPx; + clockFrame.setLayoutParams(params); } } @@ -322,15 +340,22 @@ public void onTuningChanged(String key, String newValue) { case CLOCK_STYLE_KEY: mClockStyle = TunerService.parseInteger(newValue, DEFAULT_STYLE); if (mClockStyle != 0) { - Settings.Secure.putIntForUser(mContext.getContentResolver(), - Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, UserHandle.USER_CURRENT); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, + UserHandle.USER_CURRENT); } updateClockView(); break; - case CLOCK_TEXT_COLOR_KEY: - mUseAccentColor = TunerService.parseIntegerSwitch(newValue, false); + case CLOCK_COLOR_MODE_KEY: + mColorMode = (newValue != null) ? newValue : COLOR_MODE_DEFAULT; updateClockTextColor(); break; + case CLOCK_CUSTOM_COLOR_KEY: + mCustomColor = TunerService.parseInteger(newValue, DEFAULT_CUSTOM_COLOR); + if (COLOR_MODE_CUSTOM.equals(mColorMode)) { + updateClockTextColor(); + } + break; case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); // Keep opacity within valid range (0-100) From b6a2cd777020863c1ea27d49c5331d7519e3bded Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 08:42:53 +0000 Subject: [PATCH 0935/1315] SystemUI: Use stroke-only widget backgrounds for aod Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- ...ockscreen_widget_background_circle_aod.xml | 9 ++ ...ockscreen_widget_background_square_aod.xml | 9 ++ .../LockScreenWidgetsController.java | 97 ++++++++++++++++--- 3 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml new file mode 100644 index 000000000000..99d14ff6bde4 --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml new file mode 100644 index 000000000000..4262b73eeff7 --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index 3990cc8df607..3f910b9cadf9 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -251,6 +251,7 @@ public void onDozingChanged(boolean dozing) { return; } mDozing = dozing; + updateWidgetViews(); updateContainerVisibility(); } }; @@ -422,8 +423,41 @@ public void updateWidgetViews() { private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { if (efab == null) return; efab.setElevation(0); + + if (mDozing) { + int bgRes; + switch (mThemeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square_aod; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle_aod; + break; + } + efab.setBackgroundTintList(null); + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } else { + int bgRes; + switch (mThemeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + } + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } + setButtonActiveState(null, efab, false); - long visibleWidgetCount = mMainWidgetsList.stream().filter(widget -> !"none".equals(widget)).count(); + long visibleWidgetCount = mMainWidgetsList.stream() + .filter(widget -> !"none".equals(widget)).count(); ViewGroup.LayoutParams params = efab.getLayoutParams(); if (params instanceof LinearLayout.LayoutParams) { LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) params; @@ -466,7 +500,7 @@ private void updateContainerVisibility() { if (secondaryWidgetsContainer != null) { secondaryWidgetsContainer.setVisibility(isSecondaryWidgetsEmpty ? View.GONE : View.VISIBLE); } - final boolean shouldHideContainer = isEmpty || mDozing || !mLockscreenWidgetsEnabled; + final boolean shouldHideContainer = isEmpty || !mLockscreenWidgetsEnabled; mView.setVisibility(shouldHideContainer ? View.GONE : View.VISIBLE); } @@ -474,16 +508,30 @@ private void updateWidgetsResources(LaunchableImageView iv) { if (iv == null) return; final int themeStyle = mThemeStyle; int bgRes; - switch (themeStyle) { - case 0: - case 3: - default: - bgRes = R.drawable.lockscreen_widget_background_circle; - break; - case 1: - case 2: - bgRes = R.drawable.lockscreen_widget_background_square; - break; + if (mDozing) { + switch (themeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square_aod; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle_aod; + break; + } + } else { + switch (themeStyle) { + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + } } iv.setBackgroundResource(bgRes); setButtonActiveState(iv, null, false); @@ -661,6 +709,23 @@ public void onLongPress(MotionEvent e) { } private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { + if (mDozing) { + if (iv != null) { + iv.setBackgroundTintList(null); + iv.setImageTintList(ColorStateList.valueOf(Color.WHITE)); + } + if (efab != null) { + efab.setBackgroundTintList(null); + if (efab != weatherButtonFab) { + efab.setIconTint(ColorStateList.valueOf(Color.WHITE)); + } else { + efab.setIconTint(null); + } + efab.setTextColor(Color.WHITE); + } + return; + } + int bgTint; int tintColor; if (mThemeStyle == 2 || mThemeStyle == 3) { @@ -683,17 +748,17 @@ private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, bo if (iv != null) { iv.setBackgroundTintList(ColorStateList.valueOf(bgTint)); if (iv != weatherButton) { - iv.setImageTintList(ColorStateList.valueOf(tintColor)); + iv.setImageTintList(ColorStateList.valueOf(tintColor)); } else { - iv.setImageTintList(null); + iv.setImageTintList(null); } } if (efab != null) { efab.setBackgroundTintList(ColorStateList.valueOf(bgTint)); if (efab != weatherButtonFab) { - efab.setIconTint(ColorStateList.valueOf(tintColor)); + efab.setIconTint(ColorStateList.valueOf(tintColor)); } else { - efab.setIconTint(null); + efab.setIconTint(null); } efab.setTextColor(tintColor); } From 0bda06a47dc27908a454ac83b316fdd9a9b0660e Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 19:27:46 +0000 Subject: [PATCH 0936/1315] SystemUI: Fix opacity handling for all clock style components Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/clocks/ClockStyle.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 143ebd79013c..c7f4bedd4291 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -258,17 +258,23 @@ private int resolveClockColor() { } } + private void updateClockAppearance() { + if (currentClockView == null) return; + float alpha = mClockOpacity / 100f; + currentClockView.setAlpha(alpha); + applyTextClockColor(currentClockView); + } + private void updateClockTextColor() { - if (currentClockView != null) { - updateTextClockColor(currentClockView); - } + if (currentClockView == null) return; + applyTextClockColor(currentClockView); } - private void updateTextClockColor(View view) { + private void applyTextClockColor(View view) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { - updateTextClockColor(viewGroup.getChildAt(i)); + applyTextClockColor(viewGroup.getChildAt(i)); } } @@ -284,10 +290,7 @@ private void updateTextClockColor(View view) { if ((originalColor & 0x00FFFFFF) != (whiteColor & 0x00FFFFFF)) return; - int color = resolveClockColor(); - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); + textClock.setTextColor(resolveClockColor()); } private void updateClockFrameMargin() { @@ -326,7 +329,7 @@ private void updateClockView() { if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); } - updateClockTextColor(); + updateClockAppearance(); updateClockFrameMargin(); } } @@ -358,9 +361,10 @@ public void onTuningChanged(String key, String newValue) { break; case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); - // Keep opacity within valid range (0-100) mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); - updateClockTextColor(); + if (currentClockView != null) { + currentClockView.setAlpha(mClockOpacity / 100f); + } break; case CLOCK_FRAME_MARGIN_TOP_KEY: mClockFrameMarginTop = TunerService.parseInteger(newValue, DEFAULT_MARGIN_TOP); @@ -378,4 +382,4 @@ private boolean isCenterClock(int clockStyle) { } return false; } -} \ No newline at end of file +} From ba8dc0a076a112229ff5cc05393c02c1bde0d2f7 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 19:29:26 +0000 Subject: [PATCH 0937/1315] SystemUI: Apply clock opacity at 70% during AOD/dozing Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/systemui/clocks/ClockStyle.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index c7f4bedd4291..f79b265be119 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -82,6 +82,7 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final int DEFAULT_OPACITY = 100; private static final int DEFAULT_MARGIN_TOP = 15; private static final int DEFAULT_CUSTOM_COLOR = Color.WHITE; + private static final int AOD_OPACITY_CAP = 70; private final Context mContext; private final KeyguardManager mKeyguardManager; @@ -150,6 +151,7 @@ public void onDozingChanged(boolean dozing) { return; } mDozing = dozing; + applyClockAlpha(); if (mDozing) { startBurnInProtection(); } else { @@ -258,10 +260,15 @@ private int resolveClockColor() { } } + private void applyClockAlpha() { + if (currentClockView == null) return; + int effective = (mDozing && mClockOpacity > AOD_OPACITY_CAP) ? AOD_OPACITY_CAP : mClockOpacity; + currentClockView.setAlpha(effective / 100f); + } + private void updateClockAppearance() { if (currentClockView == null) return; - float alpha = mClockOpacity / 100f; - currentClockView.setAlpha(alpha); + applyClockAlpha(); applyTextClockColor(currentClockView); } @@ -362,9 +369,7 @@ public void onTuningChanged(String key, String newValue) { case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); - if (currentClockView != null) { - currentClockView.setAlpha(mClockOpacity / 100f); - } + applyClockAlpha(); break; case CLOCK_FRAME_MARGIN_TOP_KEY: mClockFrameMarginTop = TunerService.parseInteger(newValue, DEFAULT_MARGIN_TOP); From ea6c1fb8b5cdc18cf7bf4a2282ec40c31f4b7b34 Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Fri, 29 Aug 2025 14:40:07 +0800 Subject: [PATCH 0938/1315] Reduce blocking operation on display thread For example, thread A acquires wm lock 20ms and it posts a message to another thread B that also needs to enter wm lock. Then B will need to wait for the 20ms operation to finish. Currently the common cases are the message of task-layer-rank and idle-check. For rank, it can be done with deferred scope without posting a message. For idle-check, it can post a regular 10ms timeout rather than an immediate message if the activity is in a transition because when transition is finished, scheduleProcessStoppingAndFinishingActivitiesIfNeeded will also check idle. Bug: 422656135 Test: Return to home from an app by pressing home key. Check trace about the timestamp of setProcessGroup. Change-Id: I9f455f1063ef04c049c5743eda9f6c175ec3a0ba Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/ActivityRecord.java | 4 ++-- .../com/android/server/wm/ActivityTaskManagerService.java | 5 +++++ .../java/com/android/server/wm/RootWindowContainer.java | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 139a5d939adb..6e5d18d15d28 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6048,8 +6048,8 @@ void makeInvisible() { case PAUSING: case PAUSED: case STARTED: - addToStopping(true /* scheduleIdle */, - canEnterPictureInPicture /* idleDelayed */, "makeInvisible"); + final boolean idleDelayed = canEnterPictureInPicture || inTransition(); + addToStopping(true /* scheduleIdle */, idleDelayed, "makeInvisible"); break; default: diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 5a1974ac0cc3..2e03fccb832e 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5474,6 +5474,11 @@ void continueWindowLayout() { // ClientTransactions is queued during #deferWindowLayout() for performance. // Notify to continue. mLifecycleManager.onLayoutContinued(); + + if (mRootWindowContainer.mTaskLayersChanged + && !mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mRootWindowContainer.rankTaskLayers(); + } } /** diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index d4d46be9fa80..930e1d173963 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -253,7 +253,7 @@ class RootWindowContainer extends WindowContainer DeviceStateAutoRotateSettingController mDeviceStateAutoRotateSettingController; // Whether tasks have moved and we need to rank the tasks before next OOM scoring - private boolean mTaskLayersChanged = true; + boolean mTaskLayersChanged = true; private int mTmpTaskLayerRank; private final RankTaskLayersRunnable mRankTaskLayersRunnable = new RankTaskLayersRunnable(); private Region mTmpOccludingRegion; @@ -2991,7 +2991,9 @@ void invalidateTaskLayersAndUpdateOomAdjIfNeeded() { void invalidateTaskLayers() { if (!mTaskLayersChanged) { mTaskLayersChanged = true; - mService.mH.post(mRankTaskLayersRunnable); + if (!mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mService.mH.post(mRankTaskLayersRunnable); + } } } From 4ec51a422ecece1f592a720894fbc35bf938eb95 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 1 Sep 2025 08:16:20 +0800 Subject: [PATCH 0939/1315] services: Optimizing home to desktop transition Change-Id: I2e5ace507691b2cefdd702e8f246c269afd91cd0 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/wm/WindowProcessController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 347b30266298..2c1978b45551 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -91,6 +91,7 @@ import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.DisplayThread; import com.android.server.Watchdog; import com.android.server.am.Flags; import com.android.server.am.psc.AsyncBatchSession; @@ -567,7 +568,7 @@ void postPendingUiCleanMsg(boolean pendingUiClean) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiClean, mListener, pendingUiClean); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } long getInteractionEventTime() { @@ -1458,7 +1459,7 @@ public long getInputDispatchingTimeoutMillis() { void clearProfilerIfNeeded() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::clearProfilerIfNeeded, mListener)); } @@ -1505,7 +1506,7 @@ void addToPendingTop() { void updateServiceConnectionActivities() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::updateServiceConnectionActivities, mListener)); } @@ -1514,7 +1515,7 @@ void setPendingUiCleanAndForceProcessStateUpTo(int newState) { final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiCleanAndForceProcessStateUpTo, mListener, newState); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } boolean isRemoved() { @@ -1583,7 +1584,7 @@ void appDied(String reason) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::appDied, mListener, reason); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } /** @@ -2226,7 +2227,7 @@ void removeAnimatingReason(@AnimatingReason int reason) { /** Applies the animating state to activity manager for updating process priority. */ private void setAnimating(boolean animating) { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.post(() -> mListener.setRunningRemoteAnimation(animating)); + DisplayThread.getHandler().post(() -> mListener.setRunningRemoteAnimation(animating)); } boolean isRunningRemoteTransition() { From 0aa66e46b28eea86158159cca17661ecac48cdf9 Mon Sep 17 00:00:00 2001 From: wenbo wang Date: Wed, 20 Aug 2025 06:08:23 -0700 Subject: [PATCH 0940/1315] Optimize the response speed of recents animations In the recent animation scene, we don't need to wait for the launcher to draw, we just need the wallpaper to finish drawing and start the animation. Bug: 422656135 Test:Three key navigation and gesture swipe up to recent to test for any issues. Change-Id: I9d9433434e334ee68cb5b2dd2cc182a584207c7f Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/wm/BLASTSyncEngine.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 70f075658df4..25adf74fd207 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -20,6 +20,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SYNC_ENGINE; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -207,9 +208,14 @@ private boolean tryFinish() { for (int i = mRootMembers.size() - 1; i >= 0; --i) { final WindowContainer wc = mRootMembers.valueAt(i); if (!wc.isSyncFinished(this)) { - ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", - mSyncId, wc); - return false; + if(canIgnoreFromRecents(wc)){ + //if wallpaper has drawn, ignore unfinished container when window is animating by recents. + Slog.w(TAG, "Sync group " + mSyncId + " ignoring " + wc + "when wallpaper has drawn"); + } else { + ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", + mSyncId, wc); + return false; + } } } finishNow(); @@ -656,4 +662,17 @@ boolean hasPendingSyncSets() { void addOnIdleListener(Runnable onIdleListener) { mOnIdleListeners.add(onIdleListener); } + + /** @return {@code true} if wallpaper has drawn when window is animating by recents.*/ + private boolean canIgnoreFromRecents(WindowContainer wc){ + + if(wc.asActivityRecord() != null && wc.getDisplayContent() != null + && wc.asActivityRecord().isActivityTypeHomeOrRecents() + && mWm.mAtmService.getTransitionController().getCollectingTransitionType() == TRANSIT_TO_FRONT + && wc.getDisplayContent().mWallpaperController.wallpaperTransitionReady() + && wc.getDisplayContent().mWallpaperController.getTopVisibleWallpaper() != null){ + return true; + } + return false; + } } From a1357438440c8f9a4c9a878781acc73da42cc9fb Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:52:54 +0800 Subject: [PATCH 0941/1315] services: Launcher blast sync timeout opt Change-Id: I5fa3d9d66aff7a94e84fe3f666eaeb5be8a8f8e9 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/wm/ActivityRecord.java | 21 +++++++++++++++++++ .../android/server/wm/BLASTSyncEngine.java | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 6e5d18d15d28..163cfb35b284 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -672,6 +672,8 @@ enum State { * @see WindowContainer#providesOrientation() */ final boolean mStyleFillsParent; + + private int mSyncTimeoutCounter = 0; // The input dispatching timeout for this application token in milliseconds. long mInputDispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; @@ -9395,6 +9397,25 @@ void setShouldDockBigOverlays(boolean shouldDockBigOverlays) { getTask().getRootTask().onShouldDockBigOverlaysChanged(); } + void checkSyncTimeout(BLASTSyncEngine.SyncGroup group) { + if (attachedToProcess() && "com.android.launcher3".equals(packageName)) { + mAtmService.mH.post(() -> { + synchronized (mAtmService.mGlobalLock) { + if (!hasProcess()) { + return; + } + WindowProcessController wpc = app; + mSyncTimeoutCounter++; + if (mSyncTimeoutCounter < 3) { + return; + } + Slog.d(TAG, "Sync timeout, try kill process"); + mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "syncTimeout"); + } + }); + } + } + @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { if (task != null && task.mSharedStartingData != null) { diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 25adf74fd207..6828d3be3c45 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -433,6 +433,10 @@ private void onTimeout() { .mUnknownAppVisibilityController.getDebugMessage()); } }); + ActivityRecord r = wc.asActivityRecord(); + if (r != null) { + r.checkSyncTimeout(this); + } } } From 09e118695c3aa1a6a483e1274da40717ace84d21 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:32:07 +0800 Subject: [PATCH 0942/1315] services: Fixing powerhal soft reboot Change-Id: I32df30b4eeba4f56786e568f706eea23cfe23de8 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/power/hint/HintManagerService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 9174855b6466..0a505dbc48b6 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -1028,10 +1028,11 @@ public void closeChannel() { if (mConfig != null) { try { mPowerHal.closeSessionChannel(mTgid, mUid); - } catch (RemoteException e) { - throw new IllegalStateException("Failed to close session channel!", e); + } catch (Exception e) { + Slog.w(TAG, "Session channel already dead for uid " + mUid); + } finally { + mConfig = null; } - mConfig = null; } } From 7c7bf4b551a809bc9b197b6f2c13596b3bc4fc2d Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 1 Mar 2026 17:58:57 +0000 Subject: [PATCH 0943/1315] SystemUI: Reduce media metadata bitmap size Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/res/res/values/config.xml | 2 +- media/java/android/media/session/MediaSession.java | 2 +- .../systemui/media/controls/domain/pipeline/MediaDataLoader.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 00e01e0b1eba..825314201648 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4224,7 +4224,7 @@ - 320dp + 260dp false diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index a0bf7faa3151..161da450fa4d 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -204,7 +204,7 @@ public MediaSession(@NonNull Context context, @NonNull String tag, int bitmapSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); - mMaxBitmapSize = Math.min(bitmapSize, 500); + mMaxBitmapSize = Math.min(bitmapSize, 460); mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index b2e7fe0012d9..da65471afae4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -94,7 +94,7 @@ constructor( context.resources.getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize ), - 500 + 460 ) private val artworkHeight: Int = context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) From 38a6a56154ce34a9d74ff6df237709911ee164eb Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:29:58 +0900 Subject: [PATCH 0944/1315] SettingsTheme: Correctly theming AlertDialog with M3 colors Change-Id: I25755bded0e38fb5f43f49e70126ee5d73a2856d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SettingsLib/SettingsTheme/res/values/themes.xml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/SettingsLib/SettingsTheme/res/values/themes.xml b/packages/SettingsLib/SettingsTheme/res/values/themes.xml index 2d881d1a8a7b..0cbab1439550 100644 --- a/packages/SettingsLib/SettingsTheme/res/values/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values/themes.xml @@ -32,11 +32,9 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index eef6fc00304e..59d9eb5803aa 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -80,11 +80,22 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_taden, // 29 R.layout.keyguard_clock_mont, // 30 R.layout.keyguard_clock_encode, // 31 - R.layout.keyguard_clock_nos3 // 32 + R.layout.keyguard_clock_nos3, // 32 + R.layout.keyguard_anci_clock_outline, // 33 + R.layout.keyguard_anci_clock_ovalium, // 34 + R.layout.keyguard_anci_clock_rectangle, // 35 + R.layout.keyguard_anci_clock_wallet, // 36 + R.layout.keyguard_anci_clockdate_clavicula, // 37 + R.layout.keyguard_anci_clockdate_kln, // 38 + R.layout.keyguard_anci_clockdate_miring, // 39 + R.layout.keyguard_anci_clockdate_scapula, // 40 + R.layout.keyguard_anci_clockdate_sternum, // 41 + R.layout.keyguard_sparkCircle, // 42 + R.layout.keyguard_sparkList // 43 }; private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 9, 10, 11, 12, 13, - 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26 , 27, 28, 29, 30, 31, 32}; + 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26 , 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43}; private static final int[] mNoColorClocks = {25, 26}; From 58d4fea0ce279a7a42e3bdf5bbffc0594b678ece Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 24 Mar 2026 14:55:43 +0000 Subject: [PATCH 1064/1315] SystemUI: Add custom clock aod transition [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 +++ .../android/systemui/clocks/ClockStyle.java | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ae8b111cdc15..fdcafcb1cd4c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14573,6 +14573,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String LOCK_SCREEN_CUSTOM_CLOCK_SIZE = "lock_screen_custom_clock_size_scale"; + /** + * Custom clock animation + * @hide + */ + public static final String LOCK_SCREEN_CUSTOM_CLOCK_AOD_ANIM = "lock_screen_custom_clock_aod_anim"; + /** * Timeout length for clipboard auto clear * @hide diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 59d9eb5803aa..5a782252c8db 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -105,6 +105,7 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { public static final String CLOCK_TEXT_OPACITY_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_OPACITY; public static final String CLOCK_FRAME_MARGIN_TOP_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_MARGIN_TOP; public static final String CLOCK_SIZE_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_SIZE; + public static final String CLOCK_AOD_ANIM_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_AOD_ANIM; public static final String COLOR_MODE_DEFAULT = "default"; public static final String COLOR_MODE_ACCENT = "accent"; @@ -123,6 +124,12 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final long UPDATE_INTERVAL_MILLIS = 15_000L; + private static final float AOD_SCALE_DOWN = 0.85f; + private static final long AOD_ANIM_OUT_MS = 300L; + private static final long AOD_ANIM_IN_MS = 400L; + + private boolean mAodAnimEnabled = true; + private final Context mContext; private final KeyguardManager mKeyguardManager; private final TunerService mTunerService; @@ -207,6 +214,7 @@ public void onStateChanged(int newState) {} public void onDozingChanged(boolean dozing) { if (mDozing == dozing) return; mDozing = dozing; + animateAodTransition(dozing); applyClockAlpha(); applyTextClockColor(currentClockView != null ? currentClockView : ClockStyle.this); if (mDozing) { @@ -245,7 +253,8 @@ protected void onAttachedToWindow() { CLOCK_CUSTOM_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY, CLOCK_FRAME_MARGIN_TOP_KEY, - CLOCK_SIZE_KEY); + CLOCK_SIZE_KEY, + CLOCK_AOD_ANIM_KEY); mStatusBarStateController.addCallback(mStatusBarStateListener); mDozing = mStatusBarStateController.isDozing(); mStatusBarStateListener.onDozingChanged(mDozing); @@ -372,6 +381,36 @@ private void applyClockScale() { currentClockView.setPivotY(0f); } + private void animateAodTransition(boolean toAod) { + if (currentClockView == null || !mAodAnimEnabled) return; + currentClockView.animate().cancel(); + if (toAod) { + currentClockView.animate() + .scaleX(AOD_SCALE_DOWN) + .scaleY(AOD_SCALE_DOWN) + .alpha(0f) + .setDuration(AOD_ANIM_OUT_MS) + .setInterpolator(new android.view.animation.AccelerateInterpolator(1.5f)) + .withEndAction(() -> { + applyClockAlpha(); + currentClockView.setScaleX(AOD_SCALE_DOWN); + currentClockView.setScaleY(AOD_SCALE_DOWN); + }) + .start(); + } else { + currentClockView.setScaleX(AOD_SCALE_DOWN); + currentClockView.setScaleY(AOD_SCALE_DOWN); + float targetScale = getScaleFactor(); + currentClockView.animate() + .scaleX(targetScale) + .scaleY(targetScale) + .alpha(mClockOpacity / 100f) + .setDuration(AOD_ANIM_IN_MS) + .setInterpolator(new android.view.animation.OvershootInterpolator(1.2f)) + .start(); + } + } + private void applyClockScaleAfterLayout(final View view) { if (view == null) return; if (view.getWidth() > 0) { @@ -518,6 +557,9 @@ public void onTuningChanged(String key, String newValue) { mClockSizeScale = Math.max(MIN_CLOCK_SIZE, Math.min(MAX_CLOCK_SIZE, mClockSizeScale)); applyClockScale(); break; + case CLOCK_AOD_ANIM_KEY: + mAodAnimEnabled = TunerService.parseInteger(newValue, 1) != 0; + break; } } From 2cda04158b385402aff9529052f1b4b260b17f01 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 5 Apr 2026 17:59:10 +0000 Subject: [PATCH 1065/1315] SystemUI: Add new OOS clock Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../font/SlateForOnePlus-Bold.ttf | Bin 0 -> 37332 bytes .../font/SlateForOnePlus-Medium.ttf | Bin 0 -> 37572 bytes .../font/SlateForOnePlus-Regular.ttf | Bin 0 -> 37668 bytes .../layout/keyguard_clock_oos2.xml | 113 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 87 +++++++------- 5 files changed, 157 insertions(+), 43 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/SlateForOnePlus-Bold.ttf create mode 100644 packages/SystemUI/res-keyguard/font/SlateForOnePlus-Medium.ttf create mode 100644 packages/SystemUI/res-keyguard/font/SlateForOnePlus-Regular.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_oos2.xml diff --git a/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Bold.ttf b/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2aa65528972ef7370dfa4dbfe8d927ad19b4615d GIT binary patch literal 37332 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 YF1gZS8{){9?OUy^1I3xjvV44h00JLGegFUf literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Medium.ttf b/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7ea0803c3b63ffbf370a7b73111b9fb38ca6d18e GIT binary patch literal 37572 zcmdqKcR(A*_AfrO5{M3xkVFw6Bzh47A$mtNQ;ex@*rwTFW3U0Hl;Vmr3u^u*3h zaf*|h-g|C(;v_fCX^wAtyJ;^yvGn_#T?vHam==KU8C{s+O&nEn{r{5u~cFX`bF{Ic!nl`FqAfFRN*7 zuXrxul@*MQpT`(Ke|l?1);Z470))3A{es@5-OGMDZ{xR&d3}fqDi(E*En_|`68R6| zeZZojH486PK6enoG{(w@2Ku}EehblMBYyzi=L{f1aYDWZ&vrb=4J;jBofne!Ii5dd zO!oWGNN@M=Q32~2^Fw}tE$v>tj4x-`;r#&88-}}=_BXtI=kts;Uc^}N>}4Zk<5xUB z{X53y0)F9!WuyJe?%`!ykUt6O<&1EF^Nonlw$2YO`hoe&P!D5|{o}>Wbbs}0*@Mn{ z$G7tDUP>z(!T@5GW^FU6ZvjB&CKKAh*RmhXW?Fklj7qkJ>^9>&wcXWm|k`vD-|ws{wf?` zEy9&Z`yuY_IFoUHjpyH43-Yz_T;~JAF2n_}7TI1_CH#oH3h!6B?)&ksfvJQ%<|DiU zjO_uEK1O}}m>`^q)~{u;Xh#RZjC-aF28K393KkYCtV4Ue#d|F96U$ps)(Sk2uo@oD z3c23-PoY>0*U0|HYA9Zq_q#Ch7;K*Vo#L5x8}Xaj(>jqU&oI# zcKqYxuOENq_*2JkJbwQ1y>F*`4ReQtAM^`IOa3f>oU_LCx4Y68=AW;0j|Yh`V$9emKqX0lmqHk-rdvMx4{&1VZ(H~X3O zuwK^32H8UPH#Wewup?|28)6r;U)W)`nO()MVArx6*#>qATfxTJR(28F3(h;lE@#)U zb?g>)6T6w+#{SN3W#_Wn+1>08b|<@w-Ouh}_pffp$JlG^b@m3kp1sN5WpA;!*>Uy` zyOJGXAFvbbU+jJMA-kG=#CEgu*?H^&^pf}3Gwfrwk?mk-v9sAZY&+Y;wz1`GC#z(B zY!zF=*09xVl#Q{K>?={1@b;jmyB~Q(QBWrL@>clxF7^)y3t^R0K^e&_g! z5B~N3=vBRa11nctc=*Vr8@7CZ?w)V2_~FNemwfrvzd!$O*WTav9az_M@i&Y9*C#`3 z&-nS4+a5jdg5^7xtf}<7@~4lU?*C%x@W`^U@l~tOy!pHdrS$Ng{gf5HEkfTV!VfK>ta2YeLh6{ri$4r~e>58N5}Lf~6LQ9<=V z13^~?eGv3raCq>X;8#PUL()R(Ll%Xs4%r^^bjVLixzeDlR?bzfRPI+^t$ajzLixKY zOLeX43)RnRFSS-(t!`BhtGB3cRKKh~uKqSu9U2#!8QKwgap>)#KWYjz3pCGYUf29v zo1$H(eO!B7`;|_kE7pzap3t2L3khooTN`$6*gavNgqMad4!&V~q3cXq% zqfgc6>MQk4`dRuu{fK_8evAGd{p0$h`Zx6->c7_i8s!zGjM7IXMrB16N7Y8PM$L;F zj2e&H5Va%fK-8hA8>1eM`YAd(Iy-uG^sUh^MSm0H9ixw#AG0^+y%>i<2l-x$qutPB zSZ=r;$6b(LkcvVAbTk+iIN`D?J6B15-${%y=zbE!Ce$(A= z@TGSOU!PP8U)vmCS-9%tyIhEKeC2qEt56i12?>4@I;xCH$_2w{y-mghTXv2$ORHI_ zyHKMy$Hir2#KoE4<6k?J52mLZjX06Bi$BNj5{@IMf-yC6(lMsps#&Sss>M6CJn z?{%NjFxSraCMPABLT+$e7~yxT*@S5dp#DxaB76q;l^A7`F>*jf)^QcEZ|1h_5>EIx zbCZ!^v;sWlK^M>XxA>7#N9!5g&9)io=`(E2-F->%@kw+FWj)O^y1QpI_bg0`qo}x~ zaN(uIlNSEiSoVUF(goRL3kqs$3i9h}^BwJDV+-bwjm^))Lw;QirjRn0=KMkUx9}!L zJR8O^79S)?z~z+kM6)0P2{eX!2XkEs&(#HSh0Y`-h%iU<7@nJB11%)@a}x!HylAM@ zK3vjLqz{xO#^lZ~DCo+u=Z0?5r1~uJDT_Mqnybxm@#b)iEK*x}$dqc&&8!T)vR0N6 zn>91PagHH4K$c>Uvo@rpOv@;1iVfHuF)tx8eO=6@LT6gKAv{4ViwQF4|ERU4mxLRogM%6^yhCP!KESjhub?f& z8;S>a?{2HA9vG;uT=c_{BR^N=*S)%L-)pr6$diIRGo(D=IVw9lfjCaXXY6Vh^Vr*W z@AnHBB#{vn_m+ceB@q zh^_p}jL}i^Qd6UO+ctB3Jo4tTC6MpE&|Fx&$%dW-enxNN1B;${X3^76FL`?4>8A&f zPU&3FzJr#4aWe;y<=AxJq0J+s1?MNFjO;WaMI1ZE%Xk@R%+#@(*f_3te8|fvZ>{qu z_9ez?)RE)CabKEEaT$;V=6Ev%08YYMu5+7^Ceu?XI`D2TKiuS)HAA*E@%R1c^$Fmf zN^nmw`aO#a0xP>28#wahV4<>PZD+^Yl9II@ooh>WR#cRWf4?1`KmYL1&~@|XT{pCR z|NiC6&fmYBYTby-LE$A)@`2+-)aQcztV$CP^-GXjgQJzjx`-t2(Vaq;uS!tKdxY@7 z;CS^(3vxpv6>Xjy2h0 zZQQB^)5Keq{D&QmYkARi3l=!`d~)$1f8JqRyqI?qjtTB8bq+Vj2a82%t;I zYFJ-8_l(3k{cKxlTSrM^K}dTE-|KiSD&Af;V!v!SzaqIk#$qoktBg>_@qyuMLbC=M z09h95umd+S(4?hWTw)rMk7TsTNW#NK6p>#Pk|Y%D#pBJ}>x*qE&1aP_G}u%Pnbqb9 zqrSB^zo&3pX>m%mwx+bCx0SR!UN=+krhD>G{?u?Az1+G(h(~xrdbp(s zzZ@dOWKK6Z7EZ_$@G8kIArNIjHSsJG5$Hf+)`TWl%&+Y3WAC84%?o zJL8F>d2B9Z{sLAD4U zNa1f<6YQD*fiDwOhGKbASYA$MgSJS&tfQhg=P+Rk%{BASrp4r`B6abWl+4IXS!|*v z-khwRtU;V;pe| z4U3_kETbi!iK8SF<98{S%}FywHzitfz2wIHxYA~Gd|`t*&sS4l;hSbGPiffMHUE-X zGtTdxw!AW;VPo6a?fokrTGG2OGcnWgvf318jo0W?4bc_;#*<4EV&g2ae(}*WTg#Tk z^-k;NP4-*Lw32Nv10Q^)LaQQC~irc_&MUg~CG z-T5mnSD@!AAlX=atiq(Ws!iM~mBE^E+-a%@_65ZDoxw^grq{POd9YeQLK!>oHWs_?w- z?%asPg!F{G@v(yV(C~%%>B3Qyb#88Jh!8lxink>Ch1{>!TfWnWzNHCBLti0{;Rj(i zI3*T6_Al{IN~%zv-ke*qz?MC)B(K#{SGu~h=h9gV4tBMS6)5VtGb^olxVCP%I4w)Q zz`L@b^SZII!!vV>0_TAvsh#(sok2A6LN)~PRE~CvR3&9td?GO++*g!kS=aW<&u!~1 zSw(wIF$MjL7WEazn0Vlh^wd=0Xlkl?$E=bC(S?rC!srFXXakjxcd$Dofdx7 z@hDGn9OHG4B;n|?o0mU7=1Lua)V@+`rAdjj|KcjgS9|z{p%HX)_cS}-p!yQ|1n6L)||&99TE#?QKb zZ1mdB&ZG_@E2&^vQ`55iD5GrQkmC%Z2DRjRsU;lcDXo}BL5C0`9Z&Ar!wYur=B18D zg`T)St_$!oE4;d>1)YW@s9RaVxO`RjFE zU5AH<4|mPIZn$o=Z0AVrXj$24EtO9+ngLjXF-LN9G9d`Otkoo7W+(R69Fdo2BqSOK ztPA)2kke;s+?^Jh5jVXlJ|k4C<%efRM@IZE9F5c$uBwa)_W#8{NSP5Q!QUqk{Nb>w zFhWRc{BGyqIGU+%w@n+= zLU9Sq2n_T;;U5^7p(jpILFy!)RBgWZSv=AzmiTCb0DkMHH%v*+qP0s28-&Ul6tl~;Eop4WW zw}J041;gZ*$g32#>P>M&O-;ilQ`GLjki`1N#E?Mn;`E08#H6Ie#(HC^6MIfA244_q_>&KpCaYgq0ifJW^UW|%P&nb@5bL3P>MX> z54}mmC$*S{-0ZW~JhtM|Rl*X-uYAyPq1ZlWzcA=LOp=nu24V2zg|jM52ihFAoU2N=4FUm^oh)GCVRxnx+9qS*sFQ!;G*J3mjs=X7GwaqPO#{2k% zrzh(}=WC<0!s?1%Ps$DZ#Y?6vNscobdH_W&pqPU-G}09$$(E=f$?TSGU{Y7LHepWT zV*5Z|;JWRq8Fu@kymRe^Mb(vsMK$-Wuj!~4K6ISW7ge9xT-)7UQ`^%GxyGrD{8zM* zq$b)oIg!vQM9oYpn)EXMVc+c9Rn_%ts*7jA`Wl~SENN{iGZiEhne)re?Ce}uW=aZZ z7h)oso10rAVuU#^4BFhV$9t5o)sZZB&r)dm$#2XbvJd1J4p!M4lsnG!vlkZD5cu^S z)$5v@*H;%tal=VHZxLbd0qhce!T@_1%?@+OHkCV7dnC%l<|NQW(Ry=JSx$>CBd^ci zxY}-C-7sw^-*NtD`FVLIUSD2Q8?S7WCB&AW+0n7CymEC@>fWl_Y+wiDC_h8^5^FGw zp%{gC6d4U69?9fEhCESR@$LjwvMO->x*gTk8f|7rcA=mYHv5LDEz$jsR(?}|c0#^3 zQ_W|KFc(4!>9LAqCX9Q|mU4=@+pZ5qC;yF)JfV%Idn@kyIlgWI+jvd0eX=z4dTB`B#lV3q+{wMU5e~J0P zcb@qKN$R@hk2xfH|Aitl%*MEkz+jEGGQpM-sL%&Q1**3TyV9jHPsUVXeMQwP^9od# z34SNPbhWM+t%F32moyp7IUpqb3wG>y_;1g@am9Q2*P<=6Uwz5(kP92^Wu$lMR5bwk z!;|8u1w6^-gJQ7Lhxo7QHyv1coW_*Wkg#dWjjpiF&TRKj!nM$mh8MH zW%Xd=tZlr=mY0~LaXcdiY+B$`H7h+QJ7+OWA6HLOMSluaq$)h9f>NiFMKx=ZDkQfG z0rCHA?<%ZZ+uXdiQo8Q0C@!g}C@HSEb6sWSnQd*ju4`*pFu$>3{`>}_cCFJ6Nu>9b zM8q;8aHN}f@~R|>c5L!FjU;mXA{=cxMG{eOwhK0Q=~HC|S=3b2jV19qRaW?I+~HLv z35%LZVbOFlO9+~f6(YCu3xp`LLQzllCK|dar{J3-RGGB_pjBvlrb;VQ^79q7d5(_+ z)%?^_Io|?)QbJC?i2O7XcywBtz9vFPfawnO2NFbc5#WUSCeCvBi+TCWE6#|q>$~FH z^${_p+DLQqiUoPI^K7xxjqzq(gt0l)ST)mP&5v!5PRdg1eErmZ{t-pyhS`RAp)(>z z7p~O!`G@-k7SpUp4LC-H%ScC&d%M*Wap{5#u$oYeMo?39d~SJELZMEltgrXoxN~Qq zB0?D(6~z5i8eM4?_wC=YqyKBG+CR{z2W3`(Ut$FnNjt)Of~4<|I3*#?FRJY6oV`8O zVlGnb*fgteVJ~lUJe`+cox=0cnUSBgj|Ai=+m)n4yzt!l8#kT@=60;++mMKpm06aB zG=I=-f=7mTUVQG{g-lCn#nPQ8QF=6TianWh8(0rWw+UEv(F3<$ zx^(!!tq)xO_~Wp3$nU+^@w?+Yg2=~N2-&8>I=YCZiCGYA2cl}k(`pNixiRwS6s0!I zU-iuS8#nAdv85~^Kv>9w)ln8f;aJIcoV=gPBKi0(%9^72sClI0TYkIa3vO`awDZn| z9gdq9Q);DiCZ8qqAsts|<{8{9i00K24jT+kogzpey9`b8V|cU>!xjH7x61qkzo_U? zb##=!;3Lm2?^l$B>wLJ6EX|gkD)$u>y6_V3gWk23`z@A;o`}qhefC;!-$q}V#u#&` ztV|*IE-OD|h}Fpa&`&wrsZA#{ zBwJR-_94v${D{locbS7s)2zYyy^x66NhEp8U}V;qY|_#aE!K&Xl0@iSxjJdBPAP=U z$eV6oQe3>mK7Fn|$EP4EEypLvaiXXw^Sn&&{$6kUs;22H?25jAU;FH!RJr2;ucY8P ze0ICt-sVS6jOcf$whT2wrVs&W)doFTvOP;<%A^h!LyCuJ9)ccQ(XZC}d5z3gsN!Q| zr)T9ZOsa~_HRTri`*=TcV^Ek1Oeu7RL{`N{W(G8Ba`MufA|k`ntxbX{syM}vp;I>` zSPV6huq?ty@jIR?uKuw^teo7*Z!U4PNQ-lT5PFX=9a?r6z{R4dC}O1b9)eWrOM>}k zzkBZ4eDA$}f8TNAg*7!Lw3@N2|K9meuX&K>aV{AlJ zS8>DI_`<}w8P?(y{TjPIBTJ`>O)?bsSB@lFqvxfV%aV-pu@a+sBsHd9k>jgXO>6K9 z@J{yDDu1d92`o)ctO$!R=ElvAO-nLH#rP(MMW$C|mxl!8nbWFtDJlAy5hkNiulMKi znrKV05a$;zsJwYdv^FO!T#(6vWC&xu8dr*~YjLlWft)AAgJ7t!3)6}_|&124%1>a82ofwM(F;lgnSvrO5mUV5}dFPc?UR$O2tUBlFG>U8#blu^Fx86qOK-yyb#GI70;RKP# z#j#ZKU+@M_|77++6I{UpUD<5LLd1)IoJwSF-!7b+nhNzf&6swYS;Y{IlY&@#oH$p3 zPW>0N6;h^7H)F9tDl}){J6>jz zfA0-t|Aa0Hh}9e41C@BIh;-+2cY;(-$9h?q|UCj^t2YceZnM)@hD zkB_Sa3hX@&*Pe5Cx{KL)E*`0@TKIf@$ybw(;5nO^Snc5o_u>wRFG0W11epen#7msY ziR%(pn1h6n>8LSS10k}s$>d@&y+J5D@or>Spom1Ex&Dej8?#x?J$9ESZm_1iz$FX*D5)Tx9-LA z;js@VmduCG6>Sr=axRD-1wV=&1q78idfIf9QJk#1d<))3UQZW;Dz~eFK>RGo+&#s` z(Nh@vdi0iSuXVAvZGy|O&L(RY*X4CIC-PjI7d28*6V_@x!;^Hd(aJroDDiCf3Y>c# zpVsm{Px7jVZ;Jdo58sp=jG0aUjep7{6p(Il9)is?{06_N*wMCr(s89fQF2^K6bgHq zo4dQ4n|t_na$gaXI8<)G7QT%V()nlZECr4>mpjV>@t!@u)a}8DL@aJWH?Dpvx3S{-2|Ep@geu}Txz4HC~{e6MPE)-Y@}Tq2vZ;2u?pqM zy4;)zRp;beDOj;9u(V{Rm6>#TD!n#7BP}yLQ)W!m#9?^gY@0Bh&lcX7bVr?O+m^;| z;r;#YH*;KXFqhueuywgGegA&sX+oX@ctb0laHz8COy5<_@7>tfEgb1W0;DJsI{4MX z+mh{E()h@bt$^i?W=%S2;Mei1OVgqgG%~qH^XjDbVgzHiQ z!f#H<&x+277J}0v^btXR>43qLHxg;;rnt3tzP+-$54_XWMcfn1-jOYu99d#z)xY0I zBa7gC1)%SSqzAp@(wBr^Pd(J5g(e9a8+7kJ6cNSBw8sW%|v0N$#t$qLG5YD$=S1UBdwY_qlTk+x8oE0NX2VoM7b2_F{}%H z4Q_~`l_AmRgPgV8hFYDZxW%CjnEZ2XA_r|q52Zz}g^~#@~*_wIw^xt+Yn^nq())hG}jB~AiVZB)P z3h<|=xypq19chjbtBDgd(Zq%jn@+BfVzET+l~7_FyrQ%7ih+cZB+l=eq*Z)LQt8~x z%(*2=B?&rNNU*dx6|CYqan)AH*Q!gGPpeyArqXbJr9{UHe!IJ;{b}#GePqtu;o-S+ zMwVx!q-12IBwP3`!^2%&BO_h;l@)n;c6%N$3oDsy9_&q4c*w(?93M@}zsrCOe+9I| z<*#sxv0qn$S&FA+S)O2<^iIg--^v@ya@s;O^ZV?z1BR5o^4fv!g3{#^%K)>i!8$={ zs>>z|^#0HD^KwhPzHnUNn-dpbY&97wLO-?FNakp3N@iNqjQZNZkQmnjL9Ab-A~Z^0 zxw;{4yS>J8VL}vaQSk|;I80eZ8y9StDp3kT$s-%4O0v+Pmvgmb4Yit-6TJ8GjmpLJKBIy zS50=Bwdl?4l8T@w(gL0^T8b;~IJ44Rm{Mq(pAXw&Mt(|u)6ygJ8`Bey&y9Q6028A` zlgmMqc=S{9pq1oIs7MUag~ZMFT9?sr(!TFGpbOTf@|;$Y1k-2fv~f)Zl96-9JY{5O z(;bT0;|ChVZVGwfwe(6lWs++ZD3>U2|l>980? zc4EwA#a@C&h>lrJ#fwVf@_a)hP2rh_w!GL#i!D3J9BJ55sc)|->dsBFX=;1+599=R zcl$>cn(c7GF{fok8cI(7TM$NzBMoWOt(NL|ng60TqWNro3pdLKNv5b#Ln2rhQ)f{B z(V%p#);m+FGJ3Tx6~6OR%IlSWetJJg4RU3`>nIssM_99`#XL+19(0WIUx)Z51CCqq zz6!S3NN^IyAaTYB6OHH%K!P8|i)O)nO1n`Ps}go>Pqaj5=w+}C8ghMnF|HhSJX??- zmmMbA2QdTX^)e0Qo`OaaH7%Y%+4l1A1yFo6V+l`O!e8mAY71ymjW;x`P`C1WURGHd z>Ufo>;XDhbHVuq|Bn4oN5WPxNq<^}-N?RBQhE2Je`w|7ZFfv11T_U#@XCcLbh z!_ma$Wb$D5QPImp#x(GbULk6rp_n6L?Ni4kgM|mG<&t$zloOH>OElC32@0zw_9lK) ziKBHy&;|yEdlNy_v0g1h{F=mcV|aW*Tx?!es8J`L#Ap%}X$piYZBxX_qv z@Ed_4DA8&IvY?5diY(#q!ndAVQC|Ml_S56W@%GFH9_skAq2UqKmWO$bMc5)@16Fd+ zQG)O>IU~?&_*U}cqt6N)w~8)z)6MbHvk>ZN!5Ag3zMMK+oT9}oNwg;?+Y_a0NoIV4B{M!gQ_xo>CSnR%nS>V! znVAU*Sy>4vS6Y3UvJOmZ7=N)2ykm!|`47v$j$dSl{;&=#XHlTmMv+=+_-+hN_u@AI zlHko1heoisho`UypF;(hO2rfWNQtAZHgkGfPE&ZAswmo&mKc?r8D&b2vZcqV8mwh= z)1!=5{??+mo?BT~_O*+wd}~omimqH`jE#zlsf`Q|(GSgRCVu3KIdcxq zUvLE_pRuh;N{4Ie0e)6Ves;F^6+Ah5WmGg;oyQXd3*Q4S7k%C&KhZtZo24~BhihHg z6Kbt4K~fkoN=e!x)LfV^#o>Em=Tb{YN0q&!vl5VYurma; zP=H=YUl-7~1)?dAXCJ6LfWHgq%9R(?A2=}Wf(xb{I6!l?Qhu0k15KfXhUFArDb;QF5K8wLh%=+155*U_=Jy?t*-=ic@k8Y^t`^YiD~DjQU(A+{uI zZLKvaH!#h4LpM?n+<-iT9eX<{%f5D851X@l%F27PQ?+F(qupLnV^o&l1KM99wXeVz z`bK;?L9lPYx0er|CVnHn-JFcSU?Tk%P-ZfIvYhq6`egjG6Y0-kb3NiOnuy;-@t|3X zbFt`23TVU?C4B8P@jZ^qg~Vezpc8i6`f4jScekBY=k_3-;#@jbnD4V|kAK${11=|0 zxu<(6z8|JhD@ANSaS64*hv54|{6_dUOvY3DrSw~%>rcj0`=$8x__oLu&*R1N$r6L| zozGG{zC~fY#3O#A^LjBJ^x%0iDql*!#d)1r4%y`X<&l29bE~_2;c6M-5gz;~e-pko zrFzLD_cJj)v<$IbFP=7jBfHomp6Zp-Z^2i#rz>YYzSy0Nr+TIIn_T6HK6zrjB<*DQ z7S=AN7k%NxcxV?QJO@u3zY$-^Qu$JPf=5cfWlB8ZS&CneFN`TYe!`hz`9-esS9ruz zIa2%eGWN}hv|AD-iC9PqckCui0mz4HSoEPXpbx+@M<4vTKgal^7uBcDRb`v!&lx|B zAC=YWv*TRlxyw)EO67tmOl)bb)Y48jM4qu53nyc#rBd20d6Ti!QYm)5FU7*Yg-3|x z!c<2Xd>FCL&k$?zh~0Q8#eRv{6zDcaCZ*kS36%h)oV_xUR>sylu}_OsGS=D%TQkIx zH%+HlX`~`3(3h#c;nT%#ES`*|`lPg5iY8+xOIYte8B4WEX*ZQotmL;rX*=f*Es=x1NwG5?a8otMn-*ihxWkm~zCIU{thD~moIk4Gdeq~Dra_*z0l~)5N5(^15WY6JcH&) zSrVv)!_4Y@HA>3lg&@@=td8JI&E z8_9hgCxz$%$9q3Hxpna5%WL@oKHl#5_FBgw$C;1Q#u@+Jni|w7LHf22e#A0B3IAgN zXdjzB?V_rE_b%WGgARvKK0qUhjy(bIk8dz5bbT>qGA2&0c<6FSo%S&18PkHE~u~Q=%`4wYm25;bU3b03)jb)^x^5jpNZFCdgkG6)26jml((ft)wPxDOfiv> zF(w_I6h`aTIFtA{u;IXSR9q`X-%*B=V^?Bs7IZlnQEcjLazTL4%kfC9Nu}_LPpQnb zD0#FxF~~>GFYK!WKVwKMjx;} z^yKKab4LFmS{tV3kiKD)a=5-@6X)Nut=mSQ7wK&(}6J$qm20OhbZ!pMw@kzkJ-JFb8MlyTSsn zG{Y6fj0R(slt%86#tYten_ThU*b8BoE3ClIE`M`{eGnh)3S(CX&RtNQjoc%R z7Yp;c!WHk$QoQbRg%#`}uM@7Y4@;5X=L-9>FmHEyKX!??JH0o?Rb?OrrEyvR^D+-o+J z4-FZ_L}P~0{;~eimHmC@)}ik4ena)hsA2kWfAi3aF+*d2-{6X+E&YpD40Vsvo^6J7 z!=I%xRIC^r>N8|oEFJx$V}m2ZhD@`?BBn->t`emJjbg0PFgRxDHjIyU_w_IB9$jJ> zS@NS~px z*FG}bJ=E7c)ZaZu*;WqruQH4e^c(v6$9hKxmr+uxaeM$!EF2jgH*}912FAyi6=YA7%^h3T-#fT) zu-9GNWV;QEM!SJvbO%CPcV8cHO$_7e6loMaDq_~nBc4DF4K5vYHHq3Xfna**?j2u& za{hqnKLJw2m%EQj)F4(zp9LkC0cC`BHxeb%xFInc>K0Cx+C99+uzW@TnAiu9cNm>x z*ww&McVDN*jtz{gKu7iH5~qh1r0xgL^%+DEscDlH6Vvo|4;#?CMu!bcN5JykJtHf| zCwT(w(qCZk>`ByBuwT^(TZY}LMzJf`A~t|sw+zrZXg9E2gc|X780R=rEMxtMsbfoF z7g&VU!?@D(F!uB^BUFx^yoPX}ENM)9>c=iv{fJ$OYadd!B2_m^q|&NU_9*VtQA$7N z>qFT0i*hxJX$SGN6mznE)U-m(HHtUW#r?(%u(BHd>#_{+0$KsshX9EIZM5L%K>4&& z*Pz%|iZzSzlWwkmUZeWMt zCCsnEy$9tHo(V5h7vYkgC=JoY0E$q|IMPeci_vPL%NWuO;_W!DMAg3h<{{puHc<^y34^GWIJ_J6F9ddma7w*_XwZ+eL>oE@vj*50snyi8`Vpdd z>PJK11FGRvE=m?lmbk@T6ZI&nzenVqH7IQbo+P?Z*;JmJdsd)+qA9`c#)ZUPOGRj; zHV|hM+{9(XLsUA+1S*9%U1|l%gjufI2JoKhAuRNuWQi{&jy)Zh{E*m^-hUPPQ?k8T?e|k@k!Fw-TR0-)LWJV!^9hIdJvCLy)>2+ zPYeT>o)jdSOdZ$Vco;+J#9PvcN`3N=qr`uMSE*N1Zzp*{J#G?HWy%D>v{tbHNF6^uNrnsBxJ8U=hRXxt$W3-#+{&*^(gjtU5LG1UxqY#h5a2nx4w$qT?eqg>lfH3_G9qX z67a@i@C9)Njq_vZ4ddXpRp8>)=pARUGqLCFI_&qlo^52Cuuk|T_LlvGE7(Eq!!G5% zSTDPTUCjNkC+$A$IeQrU%$|?EV(;btJOJDD(ARMxTq$pAZ)ga>1j{_O0&~v6kx|8p z;X#X~!WJ;TYGfiZXnX)OzlldLd;idQx0upeQ7NxlF*-sISy`F+QucC->uwkCS(Rcu z-rK}yYq|JbT~%G}LjYjLPZmV_($a_dVeLS&&;+;`Rlv82^!273HdP(2bvWc$AEVvn zr{j=|I1)gw%TdB190FQ>4X%0iQA<;7ckyhy^7f z-V1dfbk4;JEJ1OfcwOoaJ69kCyR+xdS%DHB5le^&GNN?U>Kwtj5&uTu77=O{G|z0b zw-)n{nc$#h;F4|N?DN45htNBYpf@~%_BLYoDjBS6No*EYz2-PiATHl|%yktI{x>Vc zs<0fpn%sib7&X%UfOKCY1?^;t?{S>Fkb?Z5RL)EB_G`TT3~y(_ubIl9CthjSokrk< zc1L+Y4EKsxBh~JF9Pef$2j!zwLI6>j1PvKjHY*dEEJ#^AemNIv;cXNWKIA*$=H{DDgP1uT0J1{Kol_XN>b}=f}>+JY!BD zcD{$Qexhuvz-q*uY(Qx6deCaffTKm-T`Mn zkZ&e-y`DbM&}k+>M1wS<}ZKF?@-nYf0+b)IQbrZ!v^1%@G^5F%b94hG+y{s%bg@ZN(~r>3KE!8tOuHK*3>S-$f{NMz=w2O$1K-J-9Iv`Juc0laT%~hTeIW!36KsB-4FA_S=Yo zF9L6mk?fu<<39<3kAKDS*{SXStGsTm|En~Acvm9E6avbl&TpVwECK(OU@Wb}mG*=0 zL}>QZTxbtqn;Q3j<_YaRak`S6`%V`t#SxA`sh^#jJk$IYpSjchId1>_jhJ_0tfD83 z>3{l@@Pz9FQYdgw! zxzD-2LD+>w3rrk$UI=+`$hpG#IJjx6^V&bJ-TlV-syp;ocjwJw>7rgDY91e=o@35; zPnXAeKuo)xI22O&SK1-QdBXVuV0VoQf6Rq+B*Q1hZ9G5a+zyF;5YqgRC`BgteNqCr z`vB0Uhc>oG&}i1#f;08Tk||`(vF*H)U*^ND)2}f_$9}pEfk(w6Uj; zJoEI?r;S~H+E_7?#x26A){oj{(X!>S&ljOrYxdi{kr5YC5d6+q`!wi|`e)Qj982ndh{}cLuFfz=GGBA6} z!he17!Mt`4=7pEx&|t288+zV@n0eDT{mG(Df&K?fJtVmMf4@lo`DOC|zU;%S_yTtI zl)1U|A6H(Tunb6cfe$e=C*LkVKpu#eN&n4;T0l@&iZIi>eJN&}%P~`@dHPz+)Yril zKz;`FRXcf<_~9jM~xn@seFkiV!U=usAN z|Ku+Me1rclhBp26&z(o|ONkUYpI~$UH15DDcYJv^@e2b^+=m-86E6{VWfl9CGx%CSacW zpB(FWtw__3!y9Xw^gjYSarmIe&jNmCuj>CHQ9?JX>haY!^Cze(Wgt1whTv**z0^DPwe1=YP2v^Y@q`GU1}P_jX1*5wibljaD<9& z(}`{K6Wb;e+a`!@^I{8d1d2W0SF~LzATO2!hEW{Bq70DwEm;wFBA(i>73EYYdikZ` z`@=X?kXg?n{v{ltupJ%)9;y90v{@sz)(dSUSw)9JY$xsFqC$wc+h3%0Fr-%rQd4Vv z#CFOd$>xC$JvahHDCMG51;Rq5Tmf^{aeiDb85^&Ie zgGj-ZB#oa-x=3<`B#OT%QON6#zFQ`V;v-5FxhPQrMTz3&k|;z0rAPslh@sl#lUzA}oQHFWBWEfFHDN;iv(jgR<Nyn z9++aAj1z5>+2B$9V4IZTM*o9o@|o5 zs4l@ILp%qUQLH;g05{2D)88we)RsD&w8MiISK8~2EKC#=2R@>E7C47$i9^VaFuk3? zfqn;`U6NJ5*|J%`-rG0joZ54K3gimm&}RJzqLyGgrc8f}ueVofkW7g1VwP@izf^B7 z=bLi{EFe&Nua^B1qlIkZ`Khh?+uE1aE&WC9n2koEh&`0dOmn`V7pf61y>y?-yX1Wtg; zy#Umvog$F3cDc7&m|dQ!v8cWA;G+)cg!2z9+=j~TW&N3<78OM+)c*DrqdncjD+l33 zmKluF5bLM*ZX4)cHQt{YWr?H+e|4x7VW@<^8o5k$lOItgcD19|GPTzAx@~YNYUy6O zj691f%PrBmz|5>ntOr=c-)vnVoDQ?BmMp6+-k zyYQ#Qn;@9jocBYKHb5FuQD#w;Q_OQ8m_HUt&t=wJ7<16pD^8KpsKfU{m z$H$*HzH!I-O!%qs-w)q$w>@sj3-hZAU$!5uRE_C2?;N}%^|_0qkBmkpG%k<1HS^uy zW^@a!*WU0$U7FYGs%tW~AN}OwPaNmI-&tDx@teY0X)`DM;0WHT0;b(6=7h^i53Q`0;(l8=w8@xA8L*KmKW2V*Z>>)RexpZUa^Z=ZQ|IW|sW{HjgAS~mS8k}@htu9b(*zvPGExz9g8 ze#xuvuKT+5?JpZPn%}i#dHaCdy}YnZh&;g(XEC~;E&Lp<%k^=jcWjw?Tx1<`=Ms4= zO3k@b?ql)BO@P;IDM%O(xmh8OrOi%Ve|B-Fl z#ALpg?pgEohFO*yz8<*q*AIN7?w`KdyDNA8wJis=yFSjHMu(SXL=Udp`IXKu>c-``&w)!A@ef7oYh%6j52 z7=G%h+{I_b`s^zY0Bmr>n;Yz4(3J z(~gI2!}4Q`FN%z4J_X|c&-;+nhb@_TQ~GdLzPk_i{8xQA8;m51jMV?B54R338irFT z`f>LE*pL6#>z{AG=YG>+d)@s(&MUv^_G{gL%SCVBlomYgiDBnCMXl|@nxpd4)RWg= zaKW1ob+7TC^Gw4zD?d6>S#Lb>)nloZj~+ZWaL4Yl>0fQhyI1*y>ASv}-)OQ%nmVt% zdE3>(8!6E*)c@<<-q&>7v*z3~@8a3lU7pk&r2FcEx4R3gW<hRxJ^ZSiA|G2QY z=E$|Amly|8Z8|`QZ=7Z~WJ%|1@6n(tX0(D_8BG)BpHay733f&q;a5r{Uav z=k86vVwF8+;PvaqSG^rHCu?WNPV0kj_C8&AdBGD;d~Wss=eJiye!ugfH*fFUS$JH2 zc&+iaxSP#SEqmgjrn3@!e)E6u`so+@{3|4B^oAFte!Q7?0#mJ)0AhZz+X-+f)Y4Nt zb&bF1GK!6sF?V-D2VZ3Ax1_kA(ZQo#9ejK&UF_ql#>`_b2`zT?+=;ZpIjNIry9c|? zrzFi79~(>W?M`2mWrhc;rHFuwk=ra*OXi`BL+PHmNg(`1Zis^aw3laj2)F7d2d@g6 z78?Cp!&@!Ay3%<$4+MT}i6Jmd@-T~bg@7?ye|!-2Pao_EQu1#kH-5BXic=8lbL92xxnq_r^alQ~=nN^lb$}3YBxw9B5hu}GGuo{|I^uW{E&^!vC{?Rp4H625_ zR2F>oA&+dN*I05gGcA)c(VAn)&CGGj#Q$G4_J2~M{vPM!lf2e;#e#qvKB+Hkdgsgs zimLSYr=D;m3%_*wT-CQllla58H$9!Xz03G%$_>|6+`i}Md)x26_3rxH9edU*I=}q7 z^6iB^n(h}D1YUjY_|GRE@9`;aQ-4wS?5wse2_L2Ii9Gnl4_EGe%vyJ>vUhPy{Xh47 zx@<<{%i9Ofcxu^?x4)wrfBTHC`i@sFcv!RQ%3HqwTl{x_*Y8^t_R;s=@~_Xk7xsa7 z3+@be3io|q^wo)-U*zum^7}WAeD~uw#eaWy#a?T8l)mh_@wo8V{cnakj-<3awdXZ? z-G{q+Yqk{)p7org=E2nU)1NqSbI|3lzFhgpqOlJ`eb#?edT{I>tKW-VrP*6A*zr~C zr|}w>5_Owp)88$@BuGTj>t&Ha=Keu3S?ZRQJV4%UnSW*1p*faWmQG8%)|b8|>Cad+ zxDYdg7>mL+(U6Dk`RJ91bB^rVz2xy1H!gm)HZ1LoaI+;|YjVlip^-%+=9TC}W9UO9 zjWbq`iE&gC>F1JCx8ark!;iiD<^A7SeE(Nv{;$H{t(n$S`lu+@_Qy@L%oSbXHmMkwFj|$N zktkbZR;|>yhLpQn8!4_4{wmoHNgJp7WgZ zKA+F`yv~_y`Ud=)y6c<>`J3L-)->Pxv6OH0E{UJ;EbMO4@tYLR7enqEnw$IfqN~^; zb*VnHqXFBc0(wV9F6>bLarXY(VXj5~lke4=9{DQaqnna>8L?=DLg0C>Yk4w zH>!+G%LU4ovws`2gk!m_Y5T}J6<1Gpy1mPevfdGV{o+TPIgjZP76Oor764Od#8q)z2!neZk@g_(a%kfyEhthR7Fm zJm9NdVX<(4G1g>Bo`FgYk6l7h*y;+d0TlTF6jP9L0I^5$p@L`(4Gj@kKT!pd1A*Sh zf&(xpK#gBeLQzSu{bbb^F+o1`Z}R_9vUM=rgj>=Hl*{teA`6>v7A+vVkoWOPf_0Zj zk$9ObW#qLFPEG%Z*_{$ew4Y5MW@U^U%qJC^=8f+>ogyhCeGEs9ai!4XQhN@Rdjzh_Szp= zW`$0DD?yxk{_KHt?xi?k)$(jdm98g?y*6e}%9YV} zb(*dDDxStIpHqB4mV|bpHYJX?3nZI2*ivLL62RvMjIY;LSs755ZQ^IGhk=d@Q0JgH zYE0zfzz+%Ii!dMWg)t8%_~z&veqnW(C*T3Ni6Fd~yM>vv*5IEALxDYjrT{$<@ZfL$ zHA2A6Y`!b5;X?}nw3Z4%02RqE77M0PzGT9nzyZN;K?P|K*n+BR(K3PxQekzaeHUco z#-##~|MFC!tK`}D+X&-y!<53Pn0NY{TAFV&QNb%QS1C)wL#^7U58m$9o$89_mALhY z|7pxFU@noKf2?x;zNDRf?@zN&XIi}c#!Gj&(IrT&zrt!S4DehtPLxNHtcl@hYLj0_ zLEzOOyIWyyr7_~u2@&yXo8PmBjUQ>ALG2cMrdLtvWvM*UI^V_4J{>pU9(Am-7nZSR zGwqFq*Cr9b8B`o6 zE!jJJP3T?mZWgJO*bsisEj{mK@is2=%I!vE&t2vabJl4Py)N!v-k=5&neO1>TzoX4 zBU-8*#8_6bwH z;g33400aIth&qc!%$Y{?%!S!{l~AIl$5G8p-9I%M@P8#}_>R^Pj7$ANOn@Ji0+Ya! zqy5*J{!fZ4B?cw5rf_p{hge+?m9@_c6*Tj7R~eV&py};jcF$FH--k>o<#6gxy+$_- zG@SacPK|kL??pUFX%wfi^7`GS_GU?4;FP^lY`qZdqTiaywR-8SYHqQ;+3s0>%?pzo zgcgloTG~#gjy`B1`7@MwrbcaQ^(mrXou4>fjnOj=7%0DNdCi`}_Ee+s)<$s?o>Sr@ z8mOv;auLW&x9W(y2z{m4?sRFJ9rY;}&$bh;Uv@i3b2XP{`6J=;}!WjDMl|H;{vYQ$`jzkzL{DD)IRd`|@h448okt~^W3>C3fb8!9*5=u1r z6vS@ubG?2Z=&*97aA|@48t@1DA(gfh?X1rpO^?XXC-GO!Ls2~+@CkX{Z9m}*3`w~c z_qR7XY-5blW7qtqQ&hZbYF&mIFRm}=WY+#{^P92y=%H`#=H_Rb!h~N{vNEKdHlWw; ziF>wH&a*p9D?Sx|*r2RQc#rmRM1cx?1IJ6ryuay_!=9v1!3V?VwUZkvanfgOKcAb| zDYTQXk}6Gs4_QeneKgtx9k}l#JdrNvq4@l1mA1W0^`=;|OipbmT;g3yZTyDf_2E-h zk9Izu*pp{oo)BB;JNb*E28IFK4#E`=5f%obS^v65gvkCy#V#*naA<(+ie(E)@X$#i zKxJuBK^%&aT#*d}RpIig2LdAv2Ah`CN}$FNES!kLFkmP-W6%htc}gM!YOG4wC=wcw zQ8v74ZUH0mFov+uPLZmwU)p!@MDuSRJ?FOJBegU#W%AB;n+f;cc+ZcOi3(`)P=lsN z@0QjB?BOc>d>@0q>+VQPK;FWaiWJ5YMb<6Ljswdyw`omxXvW-~8hl!{&&y$?A^ zL$^yR!+H&Ei>!4MZkLQkvYd!WlOaLI2q7=?OI z&AY7{5=3n#skXkc*(DZ+tGMO)@V*rx)5!X9a&P5cA%8g?Myi}sPH{c0*AN8;8l=Iszf;|#! zW5}kydES&Sc?6lxfFJ+{M0C0JL@*5DSrBj2{5}-?zYg+3Wd9OnUeQB5Kzb~_RwxFs zln@mLyTeir93ulxtj2&L28Y3K*j4?SXtQSwxlg|;#`f%5vX(ERq4%IG4Jn$;s?;eQ+)q?pqLKLUjXLW+bi_Y# z?~9U|TCKc&jzKwMBR9IFpv8*%*tT literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Regular.ttf b/packages/SystemUI/res-keyguard/font/SlateForOnePlus-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e71b4e0be9ef27f5d0af259c94b86cff24b95cb6 GIT binary patch literal 37668 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%0AcrKXp~R;eu_ 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�p2e% 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 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 5a782252c8db..72c10b427ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -50,54 +50,55 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final int[] CLOCK_LAYOUTS = { 0, R.layout.keyguard_clock_oos, // 1 - R.layout.keyguard_clock_center, // 2 - R.layout.keyguard_clock_simple, // 3 - R.layout.keyguard_clock_miui, // 4 - R.layout.keyguard_clock_ide, // 5 - R.layout.keyguard_clock_moto, // 6 - R.layout.keyguard_clock_stylish, // 7 - R.layout.keyguard_clock_stylish2, //8 - R.layout.keyguard_clock_stylish3, // 9 - R.layout.keyguard_clock_stylish4, // 10 - R.layout.keyguard_clock_stylish5, // 11 - R.layout.keyguard_clock_stylish6, // 12 - R.layout.keyguard_clock_stylish7, // 13 - R.layout.keyguard_clock_stylish8, // 14 - R.layout.keyguard_clock_stylish9, // 15 - R.layout.keyguard_clock_stylish10, // 16 - R.layout.keyguard_clock_word, // 17 - R.layout.keyguard_clock_life, // 18 - R.layout.keyguard_clock_a9, // 19 - R.layout.keyguard_clock_nos1, // 20 - R.layout.keyguard_clock_nos2, // 21 - R.layout.keyguard_clock_num, // 22 - R.layout.keyguard_clock_accent, // 23 - R.layout.keyguard_clock_analog, // 24 - R.layout.keyguard_clock_block, // 25 - R.layout.keyguard_clock_bubble, // 26 - R.layout.keyguard_clock_label, // 27 - R.layout.keyguard_clock_ios, // 28 - R.layout.keyguard_clock_taden, // 29 - R.layout.keyguard_clock_mont, // 30 - R.layout.keyguard_clock_encode, // 31 - R.layout.keyguard_clock_nos3, // 32 - R.layout.keyguard_anci_clock_outline, // 33 - R.layout.keyguard_anci_clock_ovalium, // 34 - R.layout.keyguard_anci_clock_rectangle, // 35 - R.layout.keyguard_anci_clock_wallet, // 36 - R.layout.keyguard_anci_clockdate_clavicula, // 37 - R.layout.keyguard_anci_clockdate_kln, // 38 - R.layout.keyguard_anci_clockdate_miring, // 39 - R.layout.keyguard_anci_clockdate_scapula, // 40 - R.layout.keyguard_anci_clockdate_sternum, // 41 - R.layout.keyguard_sparkCircle, // 42 - R.layout.keyguard_sparkList // 43 + R.layout.keyguard_clock_oos2, // 2 + R.layout.keyguard_clock_center, // 3 + R.layout.keyguard_clock_simple, // 4 + R.layout.keyguard_clock_miui, // 5 + R.layout.keyguard_clock_ide, // 6 + R.layout.keyguard_clock_moto, // 7 + R.layout.keyguard_clock_stylish, // 8 + R.layout.keyguard_clock_stylish2, // 9 + R.layout.keyguard_clock_stylish3, // 10 + R.layout.keyguard_clock_stylish4, // 11 + R.layout.keyguard_clock_stylish5, // 12 + R.layout.keyguard_clock_stylish6, // 13 + R.layout.keyguard_clock_stylish7, // 14 + R.layout.keyguard_clock_stylish8, // 15 + R.layout.keyguard_clock_stylish9, // 16 + R.layout.keyguard_clock_stylish10, // 17 + R.layout.keyguard_clock_word, // 18 + R.layout.keyguard_clock_life, // 19 + R.layout.keyguard_clock_a9, // 20 + R.layout.keyguard_clock_nos1, // 21 + R.layout.keyguard_clock_nos2, // 22 + R.layout.keyguard_clock_num, // 23 + R.layout.keyguard_clock_accent, // 24 + R.layout.keyguard_clock_analog, // 25 + R.layout.keyguard_clock_block, // 26 + R.layout.keyguard_clock_bubble, // 27 + R.layout.keyguard_clock_label, // 28 + R.layout.keyguard_clock_ios, // 29 + R.layout.keyguard_clock_taden, // 30 + R.layout.keyguard_clock_mont, // 31 + R.layout.keyguard_clock_encode, // 32 + R.layout.keyguard_clock_nos3, // 33 + R.layout.keyguard_anci_clock_outline, // 34 + R.layout.keyguard_anci_clock_ovalium, // 35 + R.layout.keyguard_anci_clock_rectangle, // 36 + R.layout.keyguard_anci_clock_wallet, // 37 + R.layout.keyguard_anci_clockdate_clavicula, // 38 + R.layout.keyguard_anci_clockdate_kln, // 39 + R.layout.keyguard_anci_clockdate_miring, // 40 + R.layout.keyguard_anci_clockdate_scapula, // 41 + R.layout.keyguard_anci_clockdate_sternum, // 42 + R.layout.keyguard_sparkCircle, // 43 + R.layout.keyguard_sparkList, // 44 }; private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26 , 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43}; - private static final int[] mNoColorClocks = {25, 26}; + private static final int[] mNoColorClocks = {1, 2, 25, 26}; public static final String CLOCK_STYLE_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_STYLE; public static final String CLOCK_COLOR_MODE_KEY = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_COLOR_MODE; From 9dce8ce31ce1d02b1f39162894a6e752c5d73c90 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 20 Apr 2026 15:02:09 +0000 Subject: [PATCH 1066/1315] SystemUI: Prevent the clock font from resetting after long idle Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/ids.xml | 1 + .../android/systemui/clocks/ClockStyle.java | 67 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index d85b938ec771..09083dbc2c79 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -314,5 +314,6 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 72c10b427ffa..0fb9f34a78ba 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; @@ -318,7 +319,11 @@ private void updateTextClockViews(View view) { } } if (view instanceof TextClock) { - ((TextClock) view).refreshTime(); + TextClock tc = (TextClock) view; + if (tc.getTag(R.id.original_typeface) != null) { + tc.setTypeface((Typeface) tc.getTag(R.id.original_typeface)); + } + tc.refreshTime(); } } @@ -331,11 +336,32 @@ public void onTimeChanged() { private void forceTimeUpdate() { if (currentClockView != null) { + restoreAllFonts(currentClockView); updateTextClockViews(currentClockView); lastUpdateTimeMillis = System.currentTimeMillis(); } } + private void restoreAllFonts(View view) { + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + for (int i = 0; i < vg.getChildCount(); i++) { + restoreAllFonts(vg.getChildAt(i)); + } + } + if (view instanceof TextClock) { + TextClock tc = (TextClock) view; + if (tc.getTag(R.id.original_typeface) != null) { + tc.setTypeface((Typeface) tc.getTag(R.id.original_typeface)); + } + } else if (view instanceof TextView) { + TextView tv = (TextView) view; + if (tv.getTag(R.id.original_typeface) != null) { + tv.setTypeface((Typeface) tv.getTag(R.id.original_typeface)); + } + } + } + private int resolveClockColor() { switch (mColorMode) { case COLOR_MODE_ACCENT: @@ -428,6 +454,30 @@ public void onLayoutChange(View v, int l, int t, int r, int b, } } + private void preloadFonts(View view) { + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + for (int i = 0; i < vg.getChildCount(); i++) { + preloadFonts(vg.getChildAt(i)); + } + } + if (view instanceof TextClock) { + TextClock tc = (TextClock) view; + Typeface tf = tc.getTypeface(); + if (tf != null) { + tc.setTag(R.id.original_typeface, tf); + tc.setTypeface(tf); + } + } else if (view instanceof TextView) { + TextView tv = (TextView) view; + Typeface tf = tv.getTypeface(); + if (tf != null) { + tv.setTag(R.id.original_typeface, tf); + tv.setTypeface(tf); + } + } + } + private void applyTextClockColor(View view) { if (view == null) return; if (isNoColorClock(mClockStyle)) return; @@ -439,9 +489,19 @@ private void applyTextClockColor(View view) { } if (!(view instanceof TextClock)) return; TextClock tc = (TextClock) view; + + if (tc.getTag(R.id.original_typeface) == null && tc.getTypeface() != null) { + tc.setTag(R.id.original_typeface, tc.getTypeface()); + } + if (tc.getTag(R.id.original_text_color) == null) { tc.setTag(R.id.original_text_color, tc.getCurrentTextColor()); } + + if (tc.getTag(R.id.original_typeface) != null) { + tc.setTypeface((Typeface) tc.getTag(R.id.original_typeface)); + } + int originalColor = (Integer) tc.getTag(R.id.original_text_color); int whiteColor = mContext.getColor(android.R.color.white); boolean isWhiteOriginal = (originalColor & 0x00FFFFFF) == (whiteColor & 0x00FFFFFF); @@ -487,6 +547,11 @@ private void updateClockView() { .inflate(CLOCK_LAYOUTS[mClockStyle], mClockContainer, false); mClockContainer.addView(currentClockView); } + + if (currentClockView != null) { + preloadFonts(currentClockView); + } + ImageView userProfileIcon = currentClockView.findViewById(R.id.user_profile_icon); if (userProfileIcon != null) { Drawable profileDrawable = UserProfileUtils.getUserProfileIcon(mContext); From 146d2fa536061fbb9f0a20da557bffdd8de78a70 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:05:01 +0800 Subject: [PATCH 1067/1315] fixup! SystemUI: Improve connectivity and WiFi/Data tiles Change-Id: I1f09e7d2517e76eb8a0488dd92e091f08d1cbfce Signed-off-by: Mrick343 --- .../qs/tiles/dialog/DataUsageRepository.kt | 16 +++++++++++++--- .../domain/interactor/WifiTileDataInteractor.kt | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/DataUsageRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/DataUsageRepository.kt index cbff79d1c8e8..a69e1cbf3ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/DataUsageRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/DataUsageRepository.kt @@ -76,6 +76,11 @@ class DataUsageRepository @Inject constructor( private fun queryMobileUsage() { try { val subId = SubscriptionManager.getDefaultDataSubscriptionId() + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + _mobileUsageFormatted.value = null + _mobileCarrier.value = null + return + } val template = getMobileTemplateForSubId(subId) mobileDataUsageController.setSubscriptionId(subId) val info = mobileDataUsageController.getDataUsageInfo(template) @@ -129,9 +134,14 @@ class DataUsageRepository @Inject constructor( } private fun getMobileTemplateForSubId(subId: Int): NetworkTemplate { - return NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE) - .setMeteredness(NetworkStats.METERED_YES) - .build() + val subscriberId = telephonyManager.createForSubscriptionId(subId).subscriberId + val builder = if (subscriberId != null) { + NetworkTemplate.Builder(NetworkTemplate.MATCH_CARRIER) + .setSubscriberIds(setOf(subscriberId)) + } else { + NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE) + } + return builder.setMeteredness(NetworkStats.METERED_YES).build() } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt index 9253e06a39e0..f3181817c007 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Observes wifi tile state changes providing the [WifiTileModel]. */ From 778278b1b69cd4ee8d3172cbfe2eac753cf11201 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Tue, 28 Apr 2026 21:26:22 +0000 Subject: [PATCH 1068/1315] base: quickswitch: limit default launcher config to Launcher3 - Remove Pixel Launcher entries from base config arrays - Ensure framework does not reference unavailable launchers - Allow LauncherPixelOverlay to provide multi-launcher config when needed --- core/res/res/values/quickswitch_arrays.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/res/res/values/quickswitch_arrays.xml b/core/res/res/values/quickswitch_arrays.xml index f38a024a4335..f384908c2374 100644 --- a/core/res/res/values/quickswitch_arrays.xml +++ b/core/res/res/values/quickswitch_arrays.xml @@ -20,11 +20,9 @@ com.android.launcher3/com.android.quickstep.RecentsActivity - com.google.android.apps.nexuslauncher/com.android.quickstep.RecentsActivity com.android.launcher3 - com.google.android.apps.nexuslauncher From f548b6d4ee7a3ee4ba29bfdfe04f241ca6ca574a Mon Sep 17 00:00:00 2001 From: jhenrique09 Date: Thu, 19 Nov 2020 20:46:32 -0300 Subject: [PATCH 1069/1315] [SQUASH] Introduce PixelPropsUtils That will spoof build fingerprints on some g00gle apps * Also will enable some cool stuffs like: - Dynamic navbar on GBoard - SafetyHub and others. Thanks to kdrag0n for the original idea at https://github.com/ProtonAOSP/android_frameworks_base/commit/5a54bfd846c7a26ba4820a361a6fec779edf8c5a Change-Id: I1078e7402833fec77edb751070c5144d08c85b6c Signed-off-by: jhenrique09 keystore: Block key attestation for SafetyNet SafetyNet (part of Google Play Services) opportunistically uses hardware-backed key attestation via KeyStore as a strong integrity check. This causes SafetyNet to fail on custom ROMs because the verified boot key and bootloader unlock state can be detected from attestation certificates. As a workaround, we can take advantage of the fact that SafetyNet's usage of key attestation is opportunistic (i.e. falls back to basic integrity checks if it fails) and prevent it from getting the attestation certificate chain from KeyStore. This is done by checking the stack for DroidGuard, which is the codename for SafetyNet, and pretending that the device doesn't support key attestation. Key attestation has only been blocked for SafetyNet specifically, as Google Play Services and other apps have many valid reasons to use it. For example, it appears to be involved in Google's mobile security key ferature. Change-Id: I5146439d47f42dc6231cb45c4dab9f61540056f6 core: Make CTS/Play Integrity pass again The logic behind CTS and Play Integrity has been updated today it now checks the product and model names against the fingerprint and if they do not match the CTS profile will fail. Also while we are at it use a newer FP from Pixel XL and add logging for key attestation blocking for debugging. Test: Boot, check for CTS and Play Integrity Change-Id: I089d5ef935bba40338e10c795ea7d181103ffd15 Signed-off-by: Dyneteve * Stallix: Adapted for A13 and squashed changes with their respected authors from https://github.com/Evolution-X/frameworks_base/commits/tiramisu-bak-reverts-qpr2/core/java/com/android/internal/util/evolution/PixelPropsUtils.java. https://github.com/Evolution-X/frameworks_base/commits/tiramisu-qpr2/core/java/com/android/internal/util/evolution/PixelPropsUtils.java stop Microsoft apps from crashing without INTERNET permission Crash is caused by checks in com.microsoft.aad.adal.AuthenticationContext.checkInternetPermission() and com.microsoft.identity.client.PublicClientApplication.checkInternetPermission() Prevent apps from crashing if internet permission is revoked. Signed-off-by: minaripenguin ApplicationPackageManager: Extend freeform window feature to all apps Change-Id: Iaa3e500fd6a80f4e1718218ff6d42fcb1eb79c9c Signed-off-by: minaripenguin Signed-off-by: Dmitrii core: block 2021/22 pixel features to ASI and pixel launcher * blacklist pe features and spoof redfin to get rid of tensor shits Change-Id: I83aa09c151f51ee61361510b2c1a9ac8865a2aee Signed-off-by: aswin7469 core: ApplicationPackageManager: Remove p21+ features from featuresPixel * Differentiate with featuresTensor ApplicationPackageManager: Adjust Tensor workaround * Avoid P22 device's aggresive TensorFlow implementation * Allow P21 pixel experience * Add lynx to Tensor Pixel lists Revert "PixelPropUtils: Spoof userdebug to pixel launcher" * wont work anymore developer options are migrated to debug flags This reverts commit 1c86058d22803ed3c60655c012b6d0ef2cf48aef. PixelPropsUtils: Add emojiwalls and cinematiceffects pkgs Signed-off-by: aswin7469 PixelPropsUtils: Enable new Velvet Weather UI Change-Id: I45310071f0267be1ea26f7c00044a56a763a3e6a * Adapt to current PixelPropsUtils Original commit: https://github.com/StatiXOS/android_frameworks_base/commit/ce72977956b2e787230e429ef98a6ac537454347 Signed-off-by: someone5678 PixelPropsUtils: Refactor * Allows some devices to fully pass safetynet's strong attestation (maybe due to prebuilt vendor) * Do not spoof restore, pixelmigrate and setupwizard * Enable Photos spoof by default * Keep Photos happy and not count towards storage quota * Switch from POCO F4 to POCO F5 PixelPropsUtils: Spoof to Pixel 5a by default * Several apps broken with Pixel 7 Pro spoof * Just use Pixel 5a and spoof only small portion of apps to Pixel 7 Pro Change-Id: I3762f80d499b2d8ce9115cfcaae7b59ce220dca4 PixelPropsUtils: Remove pixel tablet spoof for Weather Change-Id: I189c1307f01a9938b178746374ef9f5dfc7de60a PixelPropsUtils: Correctly spoof unstable process * Fix integrity Change-Id: Ifad92a52311a39f943c3c75c385cc75ec76bf182 PixelPropsUtils: Set HARDWARE, ID values * Google apps also check these values Change-Id: I846986121faec39d3ece044794fd0f459e8d435d PixelPropsUtils: Spoof all wallpaper packages to Pixel 7 Pro Change-Id: I667b63be4175a9f8323915d2c725e0300b22d110 base: ApplicationPackageManager: Block Tensor features for Recorder * Now Recorder also use Tensor soc feature for mic recording Log: 07-09 11:03:53.820 15531 15721 I tflite : Initialized TensorFlow Lite runtime. 07-09 11:03:53.833 15531 15721 W libc : Access denied finding property "ro.hardware.chipname" 07-09 11:03:53.835 15531 15721 I tflite : Created TensorFlow Lite XNNPACK delegate for CPU. 07-09 11:03:53.850 15531 15722 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.850 15531 15723 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.876 15531 15611 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.877 15531 15611 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.877 15531 15573 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.878 15531 15573 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.879 15531 15614 I Recorder_ManagedSequent: Finished initializing components. 07-09 11:03:53.879 15531 15614 I Recorder_ManagedSequent: Finished initializing components. Change-Id: I36b30057f45f831420db3f2fc13538dbfa5ebd56 base: ApplicationPackageManager: Exclude PE 2021 Midyear from tensor pixel list * Pixel 5a (which is qcom soc) included this by default Change-Id: I94559b0800084bb79608251568a44a48d74a7057 base: ApplicationPackageManager: Return early for Tensor check Change-Id: Ib995fe732e0712310cf6dbea21bc1f7730cfeddb PixelPropsUtils: Refactor logic and cleanup - Spoof back to Pixel 5 by default - Remove Pixel 5a spoof - Remove Pixel XL spoof - Never spoof Android System Intelligence and GMS (for battery drain), only specified processes of GMS will be allowed - Cleanup logic PixelPropsUtils: Partial spoofing improvement * Spoof all possible build properties and fix integrity check PixelPropsUtils: Stop spoofing some google apps * non pixels doesnt need to spoof asi to enable most features * fixes now playing in pixel devices [asi is checked with device codename to load sountrigger models for now playing] * motionsense is limited to p19 devices only Change-Id: Icf651e95548f524ff8f1dd3556a8d4f6197745f4 Signed-off-by: aswin7469 PixelPropsUtils: Bring in Pixel 8 series changes * Android 14 PixelPropsUtils: Fix GMS drain triggered by an outdated ROM build date If the build date exceeds a month, GMS, thinking the device is Pixel, attempts a system update, which unexpectedly fails. This goes into an endless cycle which drains battery very quickly and generates a lot of heat. Let's fix it by spoofing the build date to something always fresh. PixelPropsUtils: Add a config to enable/disable prop imitation Enabled by default, devices can disable prop imitation via overlay if their devices aren't required to rely on it Change-Id: Ia7098bbb1325f430999855c58164680988543412 Signed-off-by: someone5678 PixelPropsUtils: Drop pixel codenames and checks * PixelPropsUtils can now be disabled by settings config_enablePixelPropsUtils to false Change-Id: Ic54667f899e681b040aff1e8b13573f45880dd08 PixelPropsUtils: Use RecentPixel instead of individual model name * Just update fp only instead of change model name every time Change-Id: Ic110c532fd99f1efaa378c03687590a79994ff9c PixelPropsUtils: Make CTS/Play Integrity pass again In the annals of technology, the Zenfone 4 Max stands as the ultimate hero a phone that has transcended time and space itself. With the bravado of an isekai protagonist, it has embarked on an epic quest, unveiling powers beyond imagination. Today, the Zenfone 4 Max is more than just a phone. It symbolizes heroism and excellence, supporting our quest to conquer CTS and Play Integrity. As we harness the extraordinary capabilities of this legendary device, we embark on an epic journey to ensure the 'destruction' of "Google Employees" and burning up everyone standing before it. NOTE: The above AI generated description is a joke, and thanks to chiteroman for finding a FP that still works. Test: Play integrity Change-Id: I4f61268b3d088689fef1175aad198c88734e9f34 Signed-off-by: Dyneteve Signed-off-by: saikiran2001 ApplicationPackageManager: Merge Tensor list Change-Id: I1097a5f9b7225a108053632e3222d341fc77b2f9 ApplicationPackageManager: Add a config for device with Tensor SoC * Replace Tensor SoC device list Change-Id: Ic94917a365466881b74322d3f0470515a5dbf630 PM: Force all packages as installed via Google Play Store Change-Id: Ie561a52017630988570593291965fe6d67fa8527 services/PPU: Simplify and improve bypassing for PPU `onTaskStackChanged` listening * this is for broader bypassing of tasklistener used for setCertifiedPropsForGms across apps and gms processes Signed-off-by: minaripenguin PPU: Remove Snapchat spoof PPU: Improve device certification bypass * killing droidguard services through `onEngineGetCertificateChain` allows us pass device certification, the only backlash is users can't sign in their google accounts due to `onEngineGetCertificateChain` killing droidguard services. with https://github.com/minaripenguin/android_frameworks_base/commit/a245d9767988d7ac2db85abc08f890fa40c2ff67 as reference, we should also skip killing of droidguard services when google sign-in is on top. big thanks: to sir Alvin and sir Ste0090 for the backtrace containing google sign-in information due to a bug introduced by a unintentional change: https://github.com/minaripenguin/android_frameworks_base/commit/e67e783f3c5313112ac8c7d5565d72c5acca1664 and https://github.com/minaripenguin/android_frameworks_base/commit/876ac2a7fb7f4941d72e6d54e13d9b326d2df24f Signed-off-by: minaripenguin PPU: Make CTS/Play Integrity pass again (again and again and again) [someone5678] Adapt to this project Change-Id: If3a865849c7c99a9c7080114ba94a8f8878bb4f2 Signed-off-by: someone5678 PPU: Make spoofBuildGms updatable and chosen from a list This approach has a couple advantages: - Randomizing the device could potentially help mitigate spoofs from being blocked as we will spread the API requests across multiple devices rather than one. Although, a majority of customs and or root users would have to get behind the idea as well. - Pulling resources from a package negates the need of having to push new releases for unrooted users to pass as we can instead release an updated resource package to install manually. Signed-off-by: AnierinB PPU: Allow spoofing INITIAL_SDK_INT & SECURITY_PATCH Also update the resources package name and reorganize the array for better readability. Signed-off-by: AnierinB PPU: Enable logging & add more statements Signed-off-by: AnierinB PPU: Store the current array name in SettingsProvider So we can fetch the values currently set in PifResources Activity/PreferenceFragment. Signed-off-by: AnierinB PPU: Make PIF and PixelProps toggleable [1/2] services: Fix google apps permission denials * for some unknown reasons, google apps were throwing permission denials even if we have updated privapp permissions for most of google apps, same goes to granting permissions via DefaultPermissionGrantPolicy, which doesnt address these denials, so unless there is cleaner/appropriate fix for these denials, bypass it. attempt to resolve: 10-08 20:39:02.675 1617 4130 W ActivityManager: Permission Denial: getTaskSnapshot() from pid=7901, uid=10213 requires android.permission.READ_FRAME_BUFFER --------- beginning of crash 10-08 20:39:02.676 7901 9402 E AndroidRuntime: FATAL EXCEPTION: TaskThumbnailIconCache-1 10-08 20:39:02.676 7901 9402 E AndroidRuntime: Process: com.google.android.apps.nexuslauncher, PID: 7901 10-08 20:39:02.676 7901 9402 E AndroidRuntime: java.lang.SecurityException: Permission Denial: getTaskSnapshot() from pid=7901, uid=10213 requires android.permission.READ_FRAME_BUFFER 10-08 20:39:02.676 7901 9402 E AndroidRuntime: at android.os.Parcel.createExceptionOrNull(Parcel.java:3057) Process: com.google.android.apps.nexuslauncher PID: 2368 UID: 10213 Frozen: false Flags: 0x34cbbe45 Package: com.google.android.apps.nexuslauncher v907 (14) Foreground: No Process-Runtime: 95 Build: google/rising_oriole/oriole:14/UP1A.231005.007.A1/1696738813:userdebug/release-keys Crash-Handler: com.android.internal.os.RuntimeInit$KillApplicationHandler Loading-Progress: 1.0 Dropped-Count: 0 java.lang.RuntimeException: Unable to create service com.android.quickstep.TouchInteractionService: java.lang.SecurityException: Permission Denial: getRootTaskInfo() from pid=2368, uid=10213 requires android.permission.MANAGE_ACTIVITY_TASKS at android.app.ActivityThread.handleCreateService(ActivityThread.java:4664) at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2264) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:205) at android.os.Looper.loop(Looper.java:294) at android.app.ActivityThread.main(ActivityThread.java:8177) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) 10-08 15:46:20.701 1550 2313 W InputManager: Permission Denial: monitorGestureInput() from pid=2257, uid=10213 requires android.permission.MONITOR_INPUT 10-08 15:46:20.702 2257 2257 D AndroidRuntime: Shutting down VM 10-08 15:46:20.702 1970 1970 I TetheringManager: registerTetheringEventCallback:com.android.systemui 10-08 15:46:20.702 1970 1970 I TetheringManager: registerTetheringEventCallback caller:com.android.systemui --------- beginning of crash 10-08 15:46:20.702 2257 2257 E AndroidRuntime: FATAL EXCEPTION: main 10-08 15:46:20.702 2257 2257 E AndroidRuntime: Process: com.google.android.apps.nexuslauncher, PID: 2257 10-08 15:46:20.702 2257 2257 E AndroidRuntime: java.lang.RuntimeException: Unable to create service com.android.quickstep.TouchInteractionService: java.lang.SecurityException: Requires MONITOR_INPUT permission 10-08 15:46:20.702 2257 2257 E AndroidRuntime: at android.app.ActivityThread.handleCreateService(ActivityThread.java:4664) test: setprop persist.sys.default_launcher 1, reboot, check if pixel launcher works as intended on aosp Signed-off-by: minaripenguin MeizuPropsUtils: Spoof Hihonor Cloudmusic PPU: Spoof to recent Pixel on iD apps * This fixes some JP devices get refusing launch iD apps. PPU: Spoof as Pixel Tab for tablets Change-Id: I2d5c60dc144d56ed1957c5e2750fc60831bf2334 PixelPropsUtils: Check process name before doing certify Change-Id: I34494330ac4aa1a729e9897e2cdd5eb70d7b2d0d PPU: Allow spoofing all gapps to Pixel 8 Pro [1/2] PPU: Stop spoofing com.google.android.googlequicksearchbox - Makes AR vision with camera crash due to app expecting Pixel camera but it isn't PPU: do not spoof Pixel Launcher Change-Id: Ie5496209b2051d3f114b9e699ceceb785e44eb39 PPU: Refactor - Move Pixel 8 Pro spoof for all gapps above other spoofs - Allow spoofing Velvet to latest model for CTS (Should work based on this: https://t.me/EvolutionXOfficialROM/4117/6195) - Clean up excess parentheses. PPU: Spoof Gemini to P8P PixelPropUtils: Spoof velvet search process and gms * remove toggle and spoof by default this way we can properly enable CTS current blacklisted process and apps are enough to get rid of tensor stuffs * spoof specific processes test: reboot with patch applied, check if ASI services crashes Change-Id: I104727c30ab34a69cc76955d084ef6741dafc13b Signed-off-by: aswin7469 PixelPropsUtils: Improve tablet spoofing * Replace using config with context instead * Ref: https://github.com/minaripenguin/android_frameworks_base/commit/05b54f2dfe1a0ee4c7254dc45f8b89f8b8c84d59 PixelPropsUtils: Refactor and cleanup spoofing [joeyhuab - Evolution X] * Adapt `shouldTryToSpoofDevice()` from Rising OS. * Skip spoofing PixelProps if device is a currently supported Pixel device. * Spoof Google apps to Pixel 8 Pro by default but exclude GMS unstable process. * Spoof Google Photos to Pixel 8 Pro if Photos spoof toggle is off. * Exclude AIAI, AR Core, Photos, and Setup Wizard from P8P spoof. * Exclude spoofing specific processes (to avoid tensor checks). * Remove Pixel 5a stuff. Revert "PixelPropsUtils: Drop pixel codenames and checks" This reverts commit 195366ea6c84871fc4228ace62620f22ed2c11d5. core: Allow force enabling tensor feature XMLs on non-Pixels [1/2] * Avoid including Photos app in the spoof. * Drop unneeded `config_hasTensorSoC`. [SQUASH] PixelPropsUtils: Various changes * Allow enabling debug via `persist.sys.pixelprops.debug` prop. * Bring back Pixel checks. * Bring back Snapchat spoof toggle. * Spoof GMS processes that check for tensor to Pixel 5a. * Spoof most Google/Samsung apps/processes back to Pixel 5a by default. * Spoof select Google apps/processes to Pixel 8 Pro to enable exclusive features. * Turn off spoof for select Google apps once again. PixelPropUtils: unspoof some packages Signed-off-by: aswin7469 Revert "PixelPropsUtils: Refactor and cleanup spoofing" This reverts commit fbd5aa372a977207edb5f45ccdce4a85ea1bca9e. Revert "ApplicationPackageManager: Add a config for device with Tensor SoC" This reverts commit 50459e3a635f7932c7caac9fd1ba2e621b1a70f4. Revert "PPU: Stop spoofing com.google.android.googlequicksearchbox" This reverts commit e4528e6188d28a2270246e4f3e250eb1f3e33d3f. Revert "PixelPropsUtils: Drop pixel codenames and checks" This reverts commit 195366ea6c84871fc4228ace62620f22ed2c11d5. Revert "core: Allow force enabling tensor feature XMLs on non-Pixels [1/2]" This reverts commit ea1983f86787702f930f0653820836eb3f6c1e97. PixelPropsUtils: Spoof chimera process to Pixel 5a Change-Id: I645157990af34d5358bcbe0816bde136502b47f0 PixelPropsUtils: Unspoof backup and migrate apps Change-Id: I90bf462dfc0a9c0a1a8e1544d8b799fd644aae6f ApplicationPackageManager: Rearrange Tensor feature checks * Move googlequicksearchbox to upper to correctly enable circle to search Change-Id: I6646275998c52494db712faa07587690d323afbd services: Bypass security check when checking provider permissions for google apps * this causes any failing apps/process to repeatedly crash for "security" purposees (even though we can do the same thing with google gallery go) 08-23 07:34:48.653 1918 1918 E AndroidRuntime: java.lang.SecurityException: Permission Denial: opening provider com.google.android.apps.photos.contentprovider.impl.MediaContentProvider from ProcessRecord{2f4151f 1918:com.android.systemui/u0a478} (pid=1918, uid=10478) that is not exported from UID 10456 test: check logs for retrieved content uri from google photos Change-Id: I2c47e50d09163e05d40a4f62fd42cdb372b1173b Signed-off-by: minaripenguin ApplicationPackageManager: Rearrange Tensor feature checks * Move googlequicksearchbox to upper to correctly enable circle to search Change-Id: I6646275998c52494db712faa07587690d323afbd PixelPropsUtils: Fix spoofing logic for gms Change-Id: I770dcf97edeaeac410849b920e018c95e65b0848 PixelPropsUtils: Only spoof unstable process * That's the only process we needed Change-Id: I40423585c439cb0e55e50bad3d1a2b54871ae5eb PixelPropsUtils: Spoof chimera process to Recent Pixel Change-Id: If2fcdffe36d7a4ebdcc34168737ce30a1f93d2c4 ApplicationPackageManager: Report tensor features to false for Photos Change-Id: I58dc1ad46b785232d47a160185d9c3c3ede4915d PixelPropsUtils: Do not spoof unspecified packages as barbet * this leads to potential increase of cpu usage in system_server (were spoofing unspecified google packages and even samsung apps to barbet) Signed-off-by: minaripenguin PixelPropsUtils: Spoof pixel launcher for circle to search feature Signed-off-by: minaripenguin PixelPropsUtils: spoofBuildGms: Support RELEASE & INCREMENTAL Remove useless logging. Stop enforcing DEVICE_INITIAL_SDK_INT value range. Match array order with https://github.com/chiteroman/PlayIntegrityFix Signed-off-by: AnierinB PixelPropsUtils: Remove gms check from isCallerSafetyNet [someone5678] * as DroidGuard is now presents in multiple GApps Change-Id: Ibf9d461e77e4dae4febc2e9245f7ad0b654e1883 Signed-off-by: someone5678 PixelPropsUtils: Updates * Spoofing Ai Wallpapers to husky fixes infinite loading. * Removing Gboard spoof fixes voice search. * Removing Play store spoof for Play integrity as it's not needed according to osm0sis. Ref: https://xdaforums.com/t/module-play-integrity-fix-safetynet-fix.4607985/post-89534863 PixelPropsUtils: Remove pixel5 spoofs * Update spoofs for pixel 8 pro Signed-off-by: Alvin Francis PixelPropsUtils: June 2024 Update * Drop spoofing for general gapps. * Drop spoofing for other GMS processes. PixelPropsUtils: July 2024 update PixelPropsUtils: Show correct device name on google backup Change-Id: Id37ab66166c9ce852aad745fa2dba81d33ddcc7f Signed-off-by: aswin7469 core: Spoof Pixel8 for nexuslauncher * attempt to fix circle to search Change-Id: Ib88a67efcd2a2dbcb6b75020c6f18b78f6c97c24 Signed-off-by: aswin7469 PixelPropsUtils: Update for pixel code names * Added for Pixel 9, 9 Pro, 9 Pro XL, 9 Fold. Signed-off-by: Pranav Vashi PixelPropsUtils: Do not spoof mainline models to google photos * attempt to fix issue: RisingTechOSS/issue_tracker#14 Signed-off-by: minaripenguin core: Update configs * Expose Tensor features to Pixel Studio * Expose Tensor features to dialer (Prevent showing compat messages) * Expose 2024 experience to PixelAIPrebuilt * Update process lists core: Add toggle to enable tensor features (1/2) * Adds the ability to enable tensor features on non tensor devices PixelPropsUtils: Switch back to Pixel5a for non pixel devices * Breaks NGA, does not work properly hence the reason for the switch back, all other pixel features continue to work. services: Bypass shortcut enforcement for default launchers Signed-off-by: minaripenguin ActiveServices: [FGS]: Add exceptions for Google apps in foreground service type validation - Added a whitelist to bypass foreground service type validation for critical Google apps. - Included packages such as com.google.android.gms, com.android.vending, and others. - Ensures smooth operation of essential services like Google Play Services and Play Store. - Logs bypass events for better debugging and monitoring. Signed-off-by: afterallafk PixelPropsUtils: Update Google apps bypass support * Adapt RisingOS-staging/android_frameworks_base_new@221dd88 for Evolution X usage * Squash all commits related to BypassUtils QuickSwitch: Fix pixel launcher news feed ViewRootImpl: Ignore pixel launcher task bar denials 01-17 18:29:36.076 2470 2470 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@19241b5 -- permission denied for window type 2038 01-17 18:29:36.076 2470 2470 E AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:1698) 01-17 18:29:36.076 2470 2470 E AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:454) 01-17 18:29:36.076 2470 2470 E AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:158) 01-17 18:29:36.076 2470 2470 E AndroidRuntime: at com.android.launcher3.taskbar.overlay.TaskbarOverlayController.a(go/retraceme Prevent pixel launcher crash on start up BypassUtils: Try to re-populate package lists if its empty Bypass permission enforcements for critical uids reference issue: RisingTechOSS/issue_tracker/issues/81 test: reproduce bug in issue, crash does not persist after patch is applied [someone5678] * Minor cleanup for portability * Move in bypass check functions to PixelPropsUtils Change-Id: I5433f113400248ac941c8e13b1bde03f1da47d86 Signed-off-by: someone5678 <59456192+someone5678@users.noreply.github.com> Whitelist launcher widgets from bitmap memory usage restriction Signed-off-by: minaripenguin PixelPropsUtils: Improve spoofing * Switch to json for PIF * Drop config_enablePixelProps * Add board entry * Disable pixel props spoof for Pixel 8 series and above * Don't report Tensor features to Pixel Launcher PixelPropsUtils: Reset props for status bar lyric [1/3] Reset props to Meizu in some apps to use status bar lyric Signed-off-by: cjybyjk Change-Id: Idd573836af184b95ab0275f81fcb7ae3057783b9 Signed-off-by: someone5678 PixelPropsUtils: Spoof changes [1/2] * Bring back old Games spoof method * Drop PIF * Remove recent pixel spoofing for gms * Add switch to spoof Google app - When they spoof the Google app, the integration with arcore in search tab is broken. PixelPropsUtils: Bring back PIF * Allow users to manually use non-root PIF. * Keep disabled by default. Revert "PixelPropsUtils: Drop PIF" This reverts commit 90c24507657209b18eaa0d81bd6ff849718112fd. PixelPropsUtils: Update fingerprints to August 2025 release PixelPropsUtils: Fix props for s24 ultra PixelPropsUtils: Update fingerprints to September 2025 release * Update tensor codes to P10 PixelPropsUtils: Switch recent Pixel props to P10 base: Enable PIF by default [1/2] PixelPropsUtils: Spoof new apps from P10 PixelPropsUtils: Spoof as S24 Ultra for Diablo Immortal AttestationHooks: Drop Play store spoof [1/2] * Causes device to only get Basic integrity when turned on. PixelPropsUtils: Update fingerprints to October 2025 release Co-authored-by: Adithya R Co-authored-by: Alvin Francis Co-authored-by: AnierinB Co-authored-by: Chris Crawford Co-authored-by: Danny Lin Co-authored-by: Dyneteve Co-authored-by: ExactExampl <64069095+ExactExampl@users.noreply.github.com> Co-authored-by: Gustavo Mendes Co-authored-by: Joey Co-authored-by: Pranav Vashi Co-authored-by: QKIvan Co-authored-by: Saikiran Co-authored-by: Soo-Hwan Na Co-authored-by: Sourajit Karmakar Co-authored-by: afterallafk Co-authored-by: aswin7469 Co-authored-by: chiteroman <98092901+chiteroman@users.noreply.github.com> Co-authored-by: cjybyjk Co-authored-by: lahaina Co-authored-by: minaripenguin Co-authored-by: someone5678 Co-authored-by: timjosten Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../app/ApplicationPackageManager.java | 116 ++- core/java/android/app/ContextImpl.java | 3 +- core/java/android/app/Instrumentation.java | 7 + core/java/android/view/ViewRootImpl.java | 10 +- .../util/matrixx/AttestationHooks.java | 106 +++ .../util/matrixx/PixelPropsUtils.java | 771 ++++++++++++++++++ core/res/res/values/matrixx_arrays.xml | 9 + core/res/res/values/matrixx_symbols.xml | 9 + .../keystore2/AndroidKeyStoreSpi.java | 4 + .../shell/common/ExternalInterfaceBinder.java | 8 +- .../AccessibilitySecurityPolicy.java | 5 + .../AppPredictionManagerService.java | 1 + .../appwidget/AppWidgetServiceImpl.java | 3 +- .../com/android/server/am/ActiveServices.java | 20 + .../server/am/ActivityManagerService.java | 3 +- .../server/am/BroadcastController.java | 3 +- .../server/am/ContentProviderHelper.java | 3 + .../com/android/server/am/UserController.java | 3 + .../server/input/InputManagerService.java | 6 +- .../server/pm/LauncherAppsService.java | 3 + .../android/server/wm/ActivityStarter.java | 3 +- .../server/wm/ActivityTaskManagerService.java | 88 +- .../server/wm/ActivityTaskSupervisor.java | 1 + 23 files changed, 1152 insertions(+), 33 deletions(-) create mode 100644 core/java/com/android/internal/util/matrixx/AttestationHooks.java create mode 100644 core/java/com/android/internal/util/matrixx/PixelPropsUtils.java diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 469baa08f623..030286c08b8f 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -30,6 +30,7 @@ import static android.content.pm.Checksum.TYPE_WHOLE_SHA256; import static android.content.pm.Checksum.TYPE_WHOLE_SHA512; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.DrawableRes; import android.annotation.NonNull; @@ -837,6 +838,65 @@ public Boolean recompute(HasSystemFeatureQuery query) { } }; + private static final String[] featuresPixel = { + "com.google.android.apps.photos.PIXEL_2019_PRELOAD", + "com.google.android.apps.photos.PIXEL_2019_MIDYEAR_PRELOAD", + "com.google.android.apps.photos.PIXEL_2018_PRELOAD", + "com.google.android.apps.photos.PIXEL_2017_PRELOAD", + "com.google.android.feature.PIXEL_2021_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2019_EXPERIENCE", + "com.google.android.feature.PIXEL_2019_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2018_EXPERIENCE", + "com.google.android.feature.PIXEL_2017_EXPERIENCE", + "com.google.android.feature.PIXEL_EXPERIENCE", + "com.google.android.feature.GOOGLE_BUILD", + "com.google.android.feature.GOOGLE_EXPERIENCE" + }; + + private static final String[] featuresPixelOthers = { + "com.google.android.feature.ASI", + "com.google.android.feature.ANDROID_ONE_EXPERIENCE", + "com.google.android.feature.GOOGLE_FI_BUNDLED", + "com.google.android.feature.LILY_EXPERIENCE", + "com.google.android.feature.TURBO_PRELOAD", + "com.google.android.feature.WELLBEING", + "com.google.lens.feature.IMAGE_INTEGRATION", + "com.google.lens.feature.CAMERA_INTEGRATION", + "com.google.photos.trust_debug_certs", + "com.google.android.feature.AER_OPTIMIZED", + "com.google.android.feature.NEXT_GENERATION_ASSISTANT", + "android.software.game_service", + "com.google.android.feature.EXCHANGE_6_2", + "com.google.android.apps.dialer.call_recording_audio", + "com.google.android.apps.dialer.SUPPORTED", + "com.google.android.feature.CONTEXTUAL_SEARCH", + "com.google.android.feature.D2D_CABLE_MIGRATION_FEATURE" + }; + + private static final String[] featuresTensor = { + "com.google.android.feature.PIXEL_2026_EXPERIENCE", + "com.google.android.feature.PIXEL_2026_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2022_EXPERIENCE", + "com.google.android.feature.PIXEL_2022_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2021_EXPERIENCE" + }; + + private static final String[] featuresNexus = { + "com.google.android.apps.photos.NEXUS_PRELOAD", + "com.google.android.apps.photos.nexus_preload", + "com.google.android.feature.PIXEL_EXPERIENCE", + "com.google.android.feature.GOOGLE_BUILD", + "com.google.android.feature.GOOGLE_EXPERIENCE" + }; + @Override public boolean hasSystemFeature(String name, int version) { // We check for system features in the following order: @@ -856,6 +916,45 @@ public boolean hasSystemFeature(String name, int version) { return maybeHasSystemFeature; } } + + String packageName = ActivityThread.currentPackageName(); + boolean isPhotosSpoofEnabled = SystemProperties.getBoolean("persist.sys.pp.photos", true); + if (packageName != null + && (packageName.equals("com.google.android.googlequicksearchbox") + || packageName.equals("com.google.android.apps.pixel.agent") + || packageName.equals("com.google.android.apps.pixel.creativeassistant") + || packageName.equals("com.google.android.dialer") + || (packageName.equals("com.google.android.apps.photos") + && !isPhotosSpoofEnabled))) { + if (Arrays.asList(featuresPixel).contains(name)) return true; + if (Arrays.asList(featuresPixelOthers).contains(name)) return true; + if (Arrays.asList(featuresTensor).contains(name)) return true; + if (Arrays.asList(featuresNexus).contains(name)) return true; + } + if (packageName != null + && packageName.equals("com.google.android.apps.photos") && isPhotosSpoofEnabled) { + if (Arrays.asList(featuresPixel).contains(name)) return false; + if (Arrays.asList(featuresPixelOthers).contains(name)) return true; + if (Arrays.asList(featuresTensor).contains(name)) return false; + if (Arrays.asList(featuresNexus).contains(name)) return true; + } + boolean enableTensorFeaturesOnNonTensor = SystemProperties.getBoolean("persist.sys.pp.tensor", false); + boolean isTensorDevice = SystemProperties.get("ro.product.model").matches("Pixel (6|7|8|9|10)[a-zA-Z ]*"); + if (packageName != null && packageName.equals("com.google.android.as")) { + if (isTensorDevice && Arrays.asList(featuresTensor).contains(name)) { + return true; + } + if (!isTensorDevice && enableTensorFeaturesOnNonTensor && Arrays.asList(featuresTensor).contains(name)) { + return true; + } + } + if (name != null && Arrays.asList(featuresTensor).contains(name) + && !isTensorDevice) { + return enableTensorFeaturesOnNonTensor; + } + if (Arrays.asList(featuresNexus).contains(name)) return true; + if (Arrays.asList(featuresPixel).contains(name)) return true; + if (Arrays.asList(featuresPixelOthers).contains(name)) return true; return mHasSystemFeatureCache.query(new HasSystemFeatureQuery(name, version)); } @@ -866,8 +965,23 @@ public static void invalidateHasSystemFeatureCache() { @Override public int checkPermission(String permName, String pkgName) { - return getPermissionManager().checkPackageNamePermission(permName, pkgName, + int res = getPermissionManager().checkPackageNamePermission(permName, pkgName, mContext.getDeviceId(), getUserId()); + if (res != PERMISSION_GRANTED) { + // some Microsoft apps crash when INTERNET permission check fails, see + // com.microsoft.aad.adal.AuthenticationContext.checkInternetPermission() and + // com.microsoft.identity.client.PublicClientApplication.checkInternetPermission() + if (Manifest.permission.INTERNET.equals(permName) + // don't rely on Context.getPackageName(), may be different from process package name + && pkgName.equals(ActivityThread.currentPackageName()) + && pkgName.toLowerCase().contains("microsoft") + && pkgName.toLowerCase().contains("com.android") + && pkgName.toLowerCase().contains("google")) + { + return PERMISSION_GRANTED; + } + } + return res; } @Override diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 72804256ef40..607eeb413bde 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2550,7 +2550,8 @@ public int checkSelfPermission(String permission) { private void enforce( String permission, int resultOfCheck, boolean selfToo, int uid, String message) { - if (resultOfCheck != PERMISSION_GRANTED) { + if (resultOfCheck != PERMISSION_GRANTED + && !com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassTaskPermission(uid)) { throw new SecurityException( (message != null ? (message + ": ") : "") + (selfToo diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 9938a19b498a..7c46f3bca095 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -80,6 +80,9 @@ import java.util.StringJoiner; import java.util.concurrent.TimeoutException; +import com.android.internal.util.matrixx.AttestationHooks; +import com.android.internal.util.matrixx.PixelPropsUtils; + /** * Base class for implementing application instrumentation code. When running * with instrumentation turned on, this class will be instantiated for you @@ -1359,6 +1362,8 @@ public Application newApplication(ClassLoader cl, String className, Context cont Application app = getFactory(context.getPackageName()) .instantiateApplication(cl, className); app.attach(context); + AttestationHooks.setProps(context); + PixelPropsUtils.setProps(context); return app; } @@ -1377,6 +1382,8 @@ static public Application newApplication(Class clazz, Context context) ClassNotFoundException { Application app = (Application)clazz.newInstance(); app.attach(context); + AttestationHooks.setProps(context); + PixelPropsUtils.setProps(context); return app; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index a6f588a56408..0f2fd0283ec5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1718,9 +1718,13 @@ && hasSystemApplicationOverlayAppOp()) { + mWindow + " -- another window of type " + mWindowAttributes.type + " already exists"); case WindowManagerGlobal.ADD_PERMISSION_DENIED: - throw new WindowManager.BadTokenException("Unable to add window " - + mWindow + " -- permission denied for window type " - + mWindowAttributes.type); + if (com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + return; + } else { + throw new WindowManager.BadTokenException("Unable to add window " + + mWindow + " -- permission denied for window type " + + mWindowAttributes.type); + } case WindowManagerGlobal.ADD_INVALID_DISPLAY: throw new WindowManager.InvalidDisplayException("Unable to add window " + mWindow + " -- the specified display can not be found"); diff --git a/core/java/com/android/internal/util/matrixx/AttestationHooks.java b/core/java/com/android/internal/util/matrixx/AttestationHooks.java new file mode 100644 index 000000000000..eef591ffd9eb --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/AttestationHooks.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * (C) 2023 ArrowOS + * (C) 2023 The LibreMobileOS Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util.matrixx; + +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.R; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public final class AttestationHooks { + + private static final String TAG = "AttestationHooks"; + private static final boolean DEBUG = false; + + private static final String PACKAGE_PHOTOS = "com.google.android.apps.photos"; + private static final String PACKAGE_SNAPCHAT = "com.snapchat.android"; + + private static final String SPOOF_PHOTOS = "persist.sys.pp.photos"; + private static final String SPOOF_SNAPCHAT = "persist.sys.pp.snapchat"; + + private static final Map sPixelXLProps = Map.of( + "BRAND", "google", + "MANUFACTURER", "Google", + "DEVICE", "marlin", + "PRODUCT", "marlin", + "MODEL", "Pixel XL", + "FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys" + ); + + private static volatile String sProcessName; + + private AttestationHooks() { } + + public static void setProps(Context context) { + final String packageName = context.getPackageName(); + final String processName = Application.getProcessName(); + + if (TextUtils.isEmpty(packageName) || processName == null) { + return; + } + + sProcessName = processName; + + String model = SystemProperties.get("ro.product.model"); + boolean isPhotosSpoofEnabled = SystemProperties.getBoolean(SPOOF_PHOTOS, true); + + if (packageName.equals(PACKAGE_PHOTOS)) { + if (!isPhotosSpoofEnabled) { + return; + } else { + sPixelXLProps.forEach(AttestationHooks::setPropValue); + } + } + + if (packageName.equals(PACKAGE_SNAPCHAT)) { + if (SystemProperties.getBoolean(SPOOF_SNAPCHAT, false)) { + sPixelXLProps.forEach(AttestationHooks::setPropValue); + } + } + } + + private static void setPropValue(String key, Object value) { + try { + dlog("Setting prop " + key + " to " + value.toString()); + Field field = Build.class.getDeclaredField(key); + field.setAccessible(true); + field.set(null, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.e(TAG, "Failed to set prop " + key, e); + } + } + + public static void dlog(String msg) { + if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); + } +} diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java new file mode 100644 index 000000000000..1fc8ace2b62b --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -0,0 +1,771 @@ +/* + * Copyright (C) 2020 The Pixel Experience Project + * 2022 StatiXOS + * 2021-2022 crDroid Android Project + * 2019-2024 The Evolution X Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util.matrixx; + +import android.app.ActivityTaskManager; +import android.app.ActivityThread; +import android.app.Application; +import android.app.TaskStackListener; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Build; +import android.os.Process; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.util.matrixx.Utils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * @hide + */ +public final class PixelPropsUtils { + + private static final String DISGUISE_PROPS_FOR_MUSIC_APP = + "persist.sys.disguise_props_for_music_app"; + private static final String PACKAGE_ARCORE = "com.google.ar.core"; + private static final String PACKAGE_GMS = "com.google.android.gms"; + private static final String PROCESS_GMS_UNSTABLE = PACKAGE_GMS + ".unstable"; + private static final String PACKAGE_GOOGLE = "com.google"; + private static final String PACKAGE_NEXUS_LAUNCHER = "com.google.android.apps.nexuslauncher"; + private static final String PACKAGE_SI = "com.google.android.settings.intelligence"; + + private static final String PROP_HOOKS = "persist.sys.pihooks_"; + private static final String SPOOF_PP = "persist.sys.pp"; + private static final String SPOOF_GAMES = "persist.sys.pp.games"; + public static final String SPOOF_GMS = "persist.sys.pp.gms"; + + private static final String TAG = PixelPropsUtils.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final String sDeviceModel = + SystemProperties.get("ro.product.model", Build.MODEL); + private static final String sDeviceFingerprint = + SystemProperties.get("ro.product.fingerprint", Build.FINGERPRINT); + + private static final Map propsToChangeGeneric; + private static final Map propsToChangeRecentPixel; + private static final Map propsToChangePixelTablet; + private static final Map propsToChangeROG6; + private static final Map propsToChangeROG6D; + private static final Map propsToChangeLenovoY700; + private static final Map propsToChangeOP8P; + private static final Map propsToChangeOP9P; + private static final Map propsToChangeMI11TP; + private static final Map propsToChangeMI13P; + private static final Map propsToChangeF5; + private static final Map propsToChangeBS4; + private static final Map propsToChangeS24U; + private static final Map propsToChangeMeizu; + private static final Map> propsToKeep; + + private static Set mLauncherPkgs; + private static Set mExemptedUidPkgs; + + // Packages to Spoof as the most recent Pixel device + private static final String[] packagesToChangeRecentPixel = { + "com.amazon.avod.thirdpartyclient", + "com.android.chrome", + "com.breel.wallpapers20", + "com.disney.disneyplus", + "com.google.android.aicore", + "com.google.android.apps.accessibility.magnifier", + "com.google.android.apps.aiwallpapers", + "com.google.android.apps.bard", + "com.google.android.apps.customization.pixel", + "com.google.android.apps.emojiwallpaper", + "com.google.android.apps.pixel.agent", + "com.google.android.apps.pixel.creativeassistant", + "com.google.android.apps.pixel.nowplaying", + "com.google.android.apps.pixel.psi", + "com.google.android.apps.pixel.subzero", + "com.google.android.apps.pixel.support", + "com.google.android.apps.privacy.wildlife", + "com.google.android.apps.subscriptions.red", + "com.google.android.apps.wallpaper", + "com.google.android.apps.wallpaper.pixel", + "com.google.android.apps.weather", + "com.google.android.googlequicksearchbox", + "com.google.android.pcs", + "com.google.android.wallpaper.effects", + "com.google.pixel.livewallpaper", + "com.microsoft.android.smsorganizer", + "com.nhs.online.nhsonline", + "com.nothing.smartcenter", + "com.realme.link", + "in.startv.hotstar", + "jp.id_credit_sp2.android" + }; + + private static final String[] customGoogleCameraPackages = { + "com.google.android.MTCL83", + "com.google.android.UltraCVM", + "com.google.android.apps.cameralite" + }; + + private static final String[] packagesToChangeMeizu = { + "cmccwm.mobilemusic", + "cn.kuwo.player", + "com.hihonor.cloudmusic", + "com.kugou.android.lite", + "com.kugou.android", + "com.meizu.media.music", + "com.netease.cloudmusic", + "com.tencent.qqmusic", + }; + + // Packages to Spoof as ROG Phone 6 + private static final String[] packagesToChangeROG6 = { + "com.ea.gp.fifamobile", + "com.gameloft.android.ANMP.GloftA9HM", + "com.madfingergames.legends", + "com.pearlabyss.blackdesertm", + "com.pearlabyss.blackdesertm.gl" + }; + + // Packages to Spoof as ROG Phone 6D + private static final String[] packagesToChangeROG6D = { + "com.proxima.dfm" + }; + + // Packages to Spoof as Lenovo Y700 + private static final String[] packagesToChangeLenovoY700 = { + "com.activision.callofduty.shooter", + "com.garena.game.codm", + "com.tencent.tmgp.kr.codm", + "com.vng.codmvn" + }; + + // Packages to Spoof as OnePlus 8 Pro + private static final String[] packagesToChangeOP8P = { + "com.netease.lztgglobal", + "com.riotgames.league.wildrift", + "com.riotgames.league.wildrifttw", + "com.riotgames.league.wildriftvn", + "com.riotgames.league.teamfighttactics", + "com.riotgames.league.teamfighttacticstw", + "com.riotgames.league.teamfighttacticsvn" + }; + + // Packages to Spoof as OnePlus 9 Pro + private static final String[] packagesToChangeOP9P = { + "com.epicgames.fortnite", + "com.epicgames.portal", + "com.tencent.lolm" + }; + + // Packages to Spoof as Mi 11T Pro + private static final String[] packagesToChangeMI11TP = { + "com.ea.gp.apexlegendsmobilefps", + "com.levelinfinite.hotta.gp", + "com.supercell.clashofclans", + "com.vng.mlbbvn" + }; + + // Packages to Spoof as Xiaomi 13 Pro + private static final String[] packagesToChangeMI13P = { + "com.levelinfinite.sgameGlobal", + "com.tencent.tmgp.sgame" + }; + + // Packages to Spoof as POCO F5 + private static final String[] packagesToChangeF5 = { + "com.dts.freefiremax", + "com.dts.freefireth", + "com.mobile.legends" + }; + + // Packages to Spoof as Black Shark 4 + private static final String[] packagesToChangeBS4 = { + "com.proximabeta.mf.uamo" + }; + + // Packages to Spoof as Samsung S24 Ultra + private static final String[] packagesToChangeS24U = { + "com.blizzard.diablo.immortal", + "com.pubg.imobile", + "com.pubg.krmobile", + "com.rekoo.pubgm", + "com.tencent.ig", + "com.tencent.tmgp.pubgmhd", + "com.vng.pubgmobile" + }; + + private static final String[] GMS_SPOOF_KEYS = { + "BRAND", "DEVICE", "DEVICE_INITIAL_SDK_INT", "FINGERPRINT", "ID", + "MANUFACTURER", "MODEL", "PRODUCT", "RELEASE", "SECURITY_PATCH", + "TAGS", "TYPE", "SDK_INT" + }; + + private static final ComponentName GMS_ADD_ACCOUNT_ACTIVITY = ComponentName.unflattenFromString( + "com.google.android.gms/.auth.uiflows.minutemaid.MinuteMaidActivity"); + + private static volatile boolean sIsGms, sIsExcluded; + private static volatile String sProcessName; + + static { + propsToKeep = new HashMap<>(); + propsToKeep.put(PACKAGE_SI, new ArrayList<>(Collections.singletonList("FINGERPRINT"))); + propsToChangeGeneric = new HashMap<>(); + propsToChangeGeneric.put("TYPE", "user"); + propsToChangeGeneric.put("TAGS", "release-keys"); + propsToChangeRecentPixel = new HashMap<>(); + propsToChangeRecentPixel.put("BRAND", "google"); + propsToChangeRecentPixel.put("BOARD", "mustang"); + propsToChangeRecentPixel.put("MANUFACTURER", "Google"); + propsToChangeRecentPixel.put("DEVICE", "mustang"); + propsToChangeRecentPixel.put("PRODUCT", "mustang"); + propsToChangeRecentPixel.put("HARDWARE", "mustang"); + propsToChangeRecentPixel.put("MODEL", "Pixel 10 Pro XL"); + propsToChangeRecentPixel.put("ID", "BP4A.251205.006"); + propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/BP4A.251205.006/14401865:user/release-keys"); + propsToChangePixelTablet = new HashMap<>(); + propsToChangePixelTablet.put("BRAND", "google"); + propsToChangePixelTablet.put("BOARD", "tangorpro"); + propsToChangePixelTablet.put("MANUFACTURER", "Google"); + propsToChangePixelTablet.put("DEVICE", "tangorpro"); + propsToChangePixelTablet.put("PRODUCT", "tangorpro"); + propsToChangePixelTablet.put("HARDWARE", "tangorpro"); + propsToChangePixelTablet.put("MODEL", "Pixel Tablet"); + propsToChangePixelTablet.put("ID", "BP4A.251205.006"); + propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/BP4A.251205.006/14401865:user/release-keys"); + propsToChangeMeizu = new HashMap<>(); + propsToChangeMeizu.put("BRAND", "meizu"); + propsToChangeMeizu.put("MANUFACTURER", "Meizu"); + propsToChangeMeizu.put("DEVICE", "m1892"); + propsToChangeMeizu.put("DISPLAY", "Flyme"); + propsToChangeMeizu.put("PRODUCT", "meizu_16thPlus_CN"); + propsToChangeMeizu.put("MODEL", "meizu 16th Plus"); + propsToChangeROG6 = new HashMap<>(); + propsToChangeROG6.put("BRAND", "asus"); + propsToChangeROG6.put("MANUFACTURER", "asus"); + propsToChangeROG6.put("DEVICE", "AI2201"); + propsToChangeROG6.put("MODEL", "ASUS_AI2201"); + propsToChangeROG6D = new HashMap<>(); + propsToChangeROG6D.put("BRAND", "asus"); + propsToChangeROG6D.put("MANUFACTURER", "asus"); + propsToChangeROG6D.put("DEVICE", "AI2203_C"); + propsToChangeROG6D.put("MODEL", "ASUS_AI2203_C"); + propsToChangeLenovoY700 = new HashMap<>(); + propsToChangeLenovoY700.put("MODEL", "Lenovo TB-9707F"); + propsToChangeLenovoY700.put("MANUFACTURER", "lenovo"); + propsToChangeOP8P = new HashMap<>(); + propsToChangeOP8P.put("MODEL", "IN2020"); + propsToChangeOP8P.put("MANUFACTURER", "OnePlus"); + propsToChangeOP9P = new HashMap<>(); + propsToChangeOP9P.put("MODEL", "LE2123"); + propsToChangeOP9P.put("MANUFACTURER", "OnePlus"); + propsToChangeMI11TP = new HashMap<>(); + propsToChangeMI11TP.put("MODEL", "2107113SI"); + propsToChangeMI11TP.put("MANUFACTURER", "Xiaomi"); + propsToChangeMI13P = new HashMap<>(); + propsToChangeMI13P.put("BRAND", "Xiaomi"); + propsToChangeMI13P.put("MANUFACTURER", "Xiaomi"); + propsToChangeMI13P.put("MODEL", "2210132C"); + propsToChangeF5 = new HashMap<>(); + propsToChangeF5.put("MODEL", "23049PCD8G"); + propsToChangeF5.put("MANUFACTURER", "Xiaomi"); + propsToChangeBS4 = new HashMap<>(); + propsToChangeBS4.put("MODEL", "2SM-X706B"); + propsToChangeBS4.put("MANUFACTURER", "blackshark"); + propsToChangeS24U = new HashMap<>(); + propsToChangeS24U.put("BRAND", "SAMSUNG"); + propsToChangeS24U.put("MANUFACTURER", "samsung"); + propsToChangeS24U.put("MODEL", "SM-S928B"); + } + + public static String getBuildID(String fingerprint) { + Pattern pattern = Pattern.compile("([A-Za-z0-9]+\\.\\d+\\.\\d+\\.\\w+)"); + Matcher matcher = pattern.matcher(fingerprint); + + if (matcher.find()) { + return matcher.group(1); + } + return ""; + } + + public static String getDeviceName(String fingerprint) { + String[] parts = fingerprint.split("/"); + if (parts.length >= 2) { + return parts[1]; + } + return ""; + } + + private static boolean isGoogleCameraPackage(String packageName) { + return packageName.contains("GoogleCamera") + || Arrays.asList(customGoogleCameraPackages).contains(packageName); + } + + private static boolean shouldTryToCertifyDevice() { + if (!sIsGms) return false; + + final String processName = Application.getProcessName(); + if (!processName.toLowerCase().contains("unstable")) { + return false; + } + + final boolean was = isGmsAddAccountActivityOnTop(); + final String reason = "GmsAddAccountActivityOnTop"; + if (!was) { + return true; + } + dlog("Skip spoofing build for GMS, because " + reason + "!"); + TaskStackListener taskStackListener = new TaskStackListener() { + @Override + public void onTaskStackChanged() { + final boolean isNow = isGmsAddAccountActivityOnTop(); + if (isNow ^ was) { + dlog(String.format("%s changed: isNow=%b, was=%b, killing myself!", reason, isNow, was)); + Process.killProcess(Process.myPid()); + } + } + }; + try { + ActivityTaskManager.getService().registerTaskStackListener(taskStackListener); + return false; + } catch (Exception e) { + Log.e(TAG, "Failed to register task stack listener!", e); + return true; + } + } + + public static void spoofBuildGms() { + if (!SystemProperties.getBoolean(SPOOF_GMS, true)) + return; + for (String key : GMS_SPOOF_KEYS) { + setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); + } + } + + public static void setProps(Context context) { + final String packageName = context.getPackageName(); + final String processName = Application.getProcessName(); + Map propsToChange = new HashMap<>(); + sProcessName = processName; + sIsGms = packageName.equals(PACKAGE_GMS) && processName.equals(PROCESS_GMS_UNSTABLE); + sIsExcluded = isGoogleCameraPackage(packageName); + String model = SystemProperties.get("ro.product.model"); + boolean isPixelDevice = SystemProperties.get("ro.soc.manufacturer").equalsIgnoreCase("Google"); + boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); + boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); + propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); + if (packageName == null || processName == null || packageName.isEmpty()) { + return; + } + if (sIsExcluded) { + return; + } + if (sIsGms) { + if (shouldTryToCertifyDevice()) { + if (!isPixelGmsEnabled) { + return; + } else { + spoofBuildGms(); + } + } + } else if (Arrays.asList(packagesToChangeRecentPixel).contains(packageName)) { + if (isMainlineDevice || !SystemProperties.getBoolean(SPOOF_PP, true)) { + return; + } else if (SystemProperties.getBoolean(SPOOF_PP, true)) { + if (isDeviceTablet(context.getApplicationContext())) { + propsToChange.putAll(propsToChangePixelTablet); + } else { + propsToChange.putAll(propsToChangeRecentPixel); + } + } + } else if (Arrays.asList(packagesToChangeMeizu).contains(packageName)) { + if (SystemProperties.getBoolean(DISGUISE_PROPS_FOR_MUSIC_APP, false)) { + propsToChange.putAll(propsToChangeMeizu); + } + } + dlog("Defining props for: " + packageName); + for (Map.Entry prop : propsToChange.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + if (propsToKeep.containsKey(packageName) && propsToKeep.get(packageName).contains(key)) { + dlog("Not defining " + key + " prop for: " + packageName); + continue; + } + dlog("Defining " + key + " prop for: " + packageName); + setPropValue(key, value); + } + // Set proper indexing fingerprint + if (packageName.equals(PACKAGE_SI)) { + setPropValue("FINGERPRINT", String.valueOf(Build.TIME)); + return; + } + if (packageName.equals(PACKAGE_ARCORE)) { + setPropValue("FINGERPRINT", sDeviceFingerprint); + return; + } else { + + if (!SystemProperties.getBoolean(SPOOF_GAMES, true)) + return; + + if (Arrays.asList(packagesToChangeROG6).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeROG6.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeROG6D).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeROG6D.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeLenovoY700).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeLenovoY700.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeOP8P).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeOP8P.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeOP9P).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeOP9P.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeMI11TP).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeMI11TP.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeMI13P).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeMI13P.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeF5).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeF5.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeBS4).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeBS4.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } else if (Arrays.asList(packagesToChangeS24U).contains(packageName)) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : propsToChangeS24U.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + setPropValue(key, value); + } + } + } + } + + private static boolean isDeviceTablet(Context context) { + if (context == null) { + return false; + } + Configuration config = context.getResources().getConfiguration(); + boolean isTablet = (config.smallestScreenWidthDp >= 600); + return isTablet; + } + + private static void setPropValue(String key, Object value) { + try { + Field field = getBuildClassField(key); + if (field != null) { + field.setAccessible(true); + if (field.getType() == int.class) { + if (value instanceof String) { + field.set(null, Integer.parseInt((String) value)); + } else if (value instanceof Integer) { + field.set(null, (Integer) value); + } + } else if (field.getType() == long.class) { + if (value instanceof String) { + field.set(null, Long.parseLong((String) value)); + } else if (value instanceof Long) { + field.set(null, (Long) value); + } + } else { + field.set(null, value.toString()); + } + field.setAccessible(false); + dlog("Set prop " + key + " to " + value); + } else { + Log.e(TAG, "Field " + key + " not found in Build or Build.VERSION classes"); + } + } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) { + Log.e(TAG, "Failed to set prop " + key, e); + } + } + + private static void setVersionField(String key, Object value) { + try { + dlog("Defining version field " + key + " to " + value.toString()); + Field field = Build.VERSION.class.getDeclaredField(key); + field.setAccessible(true); + field.set(null, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.e(TAG, "Failed to set version field " + key, e); + } + } + + private static void setVersionFieldString(String key, String value) { + try { + Field field = Build.VERSION.class.getDeclaredField(key); + field.setAccessible(true); + field.set(null, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.e(TAG, "Failed to spoof Build." + key, e); + } + } + + private static void setVersionFieldInt(String key, int value) { + try { + dlog("Defining version field " + key + " to " + value); + Field field = Build.VERSION.class.getDeclaredField(key); + field.setAccessible(true); + field.set(null, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.e(TAG, "Failed to spoof Build." + key, e); + } + } + + private static Field getBuildClassField(String key) throws NoSuchFieldException { + try { + Field field = Build.class.getDeclaredField(key); + dlog("Field " + key + " found in Build.class"); + return field; + } catch (NoSuchFieldException e) { + Field field = Build.VERSION.class.getDeclaredField(key); + dlog("Field " + key + " found in Build.VERSION.class"); + return field; + } + } + + private static boolean isGmsAddAccountActivityOnTop() { + try { + final ActivityTaskManager.RootTaskInfo focusedTask = + ActivityTaskManager.getService().getFocusedRootTaskInfo(); + return focusedTask != null && focusedTask.topActivity != null + && focusedTask.topActivity.equals(GMS_ADD_ACCOUNT_ACTIVITY); + } catch (Exception e) { + Log.e(TAG, "Unable to get top activity!", e); + } + return false; + } + + private static String[] getStringArrayResSafely(int resId) { + String[] strArr = Resources.getSystem().getStringArray(resId); + if (strArr == null) strArr = new String[0]; + return strArr; + } + + public static boolean isPackageGoogle(String pkg) { + return pkg != null && pkg.toLowerCase().contains("google"); + } + + private static Set getLauncherPkgs() { + if (mLauncherPkgs == null || mLauncherPkgs.isEmpty()) { + mLauncherPkgs = + new HashSet<>( + Arrays.asList( + getStringArrayResSafely(R.array.config_launcherPackages))); + } + return mLauncherPkgs; + } + + private static Set getExemptedUidPkgs() { + if (mExemptedUidPkgs == null || mExemptedUidPkgs.isEmpty()) { + mExemptedUidPkgs = new HashSet<>(); + mExemptedUidPkgs.add(PACKAGE_GMS); + mExemptedUidPkgs.addAll(getLauncherPkgs()); + } + return mExemptedUidPkgs; + } + + public static boolean isNexusLauncher(Context context) { + try { + return PACKAGE_NEXUS_LAUNCHER.equals( + context.getPackageManager().getNameForUid(android.os.Binder.getCallingUid())); + } catch (Exception e) { + return false; + } + } + + public static boolean isSystemLauncher(Context context) { + try { + return isSystemLauncherInternal( + context.getPackageManager().getNameForUid(android.os.Binder.getCallingUid())); + } catch (Exception e) { + return false; + } + } + + public static boolean isSystemLauncher(int callingUid) { + try { + return isSystemLauncherInternal( + ActivityThread.getPackageManager().getNameForUid(callingUid)); + } catch (Exception e) { + return false; + } + } + + public static boolean isSystemLauncherInternal(String callerPackage) { + return getLauncherPkgs().contains(callerPackage); + } + + public static boolean shouldBypassTaskPermission(int callingUid) { + for (String pkg : getExemptedUidPkgs()) { + try { + ApplicationInfo appInfo = + ActivityThread.getPackageManager() + .getApplicationInfo(pkg, 0, UserHandle.getUserId(callingUid)); + if (appInfo.uid == callingUid) { + return true; + } + } catch (Exception e) { + } + } + return false; + } + + public static boolean shouldBypassManageActivityTaskPermission(Context context) { + final int callingUid = Binder.getCallingUid(); + return isSystemLauncher(callingUid) + || isPackageGoogle(context.getPackageManager().getNameForUid(callingUid)); + } + + public static boolean shouldBypassMonitorInputPermission(Context context) { + final int callingUid = Binder.getCallingUid(); + return shouldBypassTaskPermission(callingUid) + || isPackageGoogle(context.getPackageManager().getNameForUid(callingUid)); + } + + // Whitelist of package names to bypass FGS type validation + public static boolean shouldBypassFGSValidation(String packageName) { + // Check if the app is whitelisted + if (Arrays.asList(getStringArrayResSafely(R.array.config_fgsTypeValidationBypassPackages)) + .contains(packageName)) { + dlog( + "shouldBypassFGSValidation: " + + "Bypassing FGS type validation for whitelisted app: " + + packageName); + return true; + } + return false; + } + + // Whitelist of package names to bypass alarm manager validation + public static boolean shouldBypassAlarmManagerValidation(String packageName) { + // Check if the app is whitelisted + if (Arrays.asList( + getStringArrayResSafely( + R.array.config_alarmManagerValidationBypassPackages)) + .contains(packageName)) { + dlog( + "shouldBypassAlarmManagerValidation: " + + "Bypassing alarm manager validation for whitelisted app: " + + packageName); + return true; + } + return false; + } + + // Whitelist of package names to bypass broadcast reciever validation + public static boolean shouldBypassBroadcastReceiverValidation(String packageName) { + // Check if the app is whitelisted + if (Arrays.asList( + getStringArrayResSafely( + R.array.config_broadcaseReceiverValidationBypassPackages)) + .contains(packageName)) { + dlog( + "shouldBypassBroadcastReceiverValidation: " + + "Bypassing broadcast receiver validation for whitelisted app: " + + packageName); + return true; + } + return false; + } + + private static boolean isCallerSafetyNet() { + return Arrays.stream(Thread.currentThread().getStackTrace()) + .anyMatch(elem -> elem.getClassName().toLowerCase() + .contains("droidguard")); + } + + public static void onEngineGetCertificateChain() { + boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); + if (!isPixelGmsEnabled) { + dlog("onEngineGetCertificateChain disabled by setting"); + return; + } + + // Check stack for Play Integrity + if (isCallerSafetyNet()) { + dlog("Blocked key attestation"); + throw new UnsupportedOperationException(); + } + } + + public static void dlog(String msg) { + if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); + } +} diff --git a/core/res/res/values/matrixx_arrays.xml b/core/res/res/values/matrixx_arrays.xml index 4feac267917b..26045e802949 100644 --- a/core/res/res/values/matrixx_arrays.xml +++ b/core/res/res/values/matrixx_arrays.xml @@ -20,4 +20,13 @@ 8dp 14sp 8dp + + + + + + + + + diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 1b8aadf983dc..20faf1f8fbea 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -106,4 +106,13 @@ + + + + + + + + + diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java index e6a63b9c4c17..f3c6f5649fde 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -90,6 +90,8 @@ import javax.crypto.SecretKey; +import com.android.internal.util.matrixx.PixelPropsUtils; + /** * A java.security.KeyStore interface for the Android KeyStore. An instance of * it can be created via the {@link java.security.KeyStore#getInstance(String) @@ -178,6 +180,8 @@ private KeyEntryResponse getKeyMetadata(String alias) { @Override public Certificate[] engineGetCertificateChain(String alias) { + PixelPropsUtils.onEngineGetCertificateChain(); + KeyEntryResponse response = getKeyMetadata(alias); if (response == null || response.metadata.certificate == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java index 751983624e9b..7816dd0b1dff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -17,6 +17,7 @@ package com.android.wm.shell.common; import android.Manifest; +import android.os.Binder; import android.os.IBinder; import android.util.Slog; @@ -55,8 +56,11 @@ default void executeRemoteCallWithTaskPermission(RemoteCallable controlle if (controllerInstance == null) return; final RemoteCallable controller = controllerInstance; - controllerInstance.getContext().enforceCallingPermission( - Manifest.permission.MANAGE_ACTIVITY_TASKS, log); + if (!com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassManageActivityTaskPermission( + controllerInstance.getContext())) { + controllerInstance.getContext().enforceCallingPermission( + Manifest.permission.MANAGE_ACTIVITY_TASKS, log); + } if (blocking) { try { controllerInstance.getRemoteCallExecutor().executeBlocking(() -> { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java index ad6a71d110c6..4f242eb36d68 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java @@ -679,6 +679,11 @@ public boolean checkAccessibilityAccess(AbstractAccessibilityServiceConnection s * @param permission The permission to check */ public void enforceCallingOrSelfPermission(@NonNull String permission) { + final int callingUid = Binder.getCallingUid(); + final String callingPackage = mContext.getPackageManager().getNameForUid(callingUid); + if (callingPackage != null && callingPackage.toLowerCase().contains("google")) { + return; + } if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Caller does not hold permission " diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java index df4e699e3926..d7ee5fdd3011 100644 --- a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java @@ -187,6 +187,7 @@ private void runForUserLocked(@NonNull final String func, Context ctx = getContext(); if (!(ctx.checkCallingPermission(PACKAGE_USAGE_STATS) == PERMISSION_GRANTED + || com.android.internal.util.matrixx.PixelPropsUtils.isSystemLauncher(Binder.getCallingUid()) || mServiceNameResolver.isTemporary(userId) || mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()) || Binder.getCallingUid() == Process.SYSTEM_UID)) { diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index b8fda0504b9a..e83bb5dfa4a8 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -3047,7 +3047,8 @@ private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views, long memoryUsage; if ((UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID) && (widget.views != null) && - ((memoryUsage = widget.views.estimateMemoryUsage()) > mMaxWidgetBitmapMemory)) { + ((memoryUsage = widget.views.estimateMemoryUsage()) > mMaxWidgetBitmapMemory) + && !com.android.internal.util.matrixx.PixelPropsUtils.isSystemLauncher(Binder.getCallingUid())) { widget.views = null; throw new IllegalArgumentException("RemoteViews for widget update exceeds" + " maximum bitmap memory usage (used: " + memoryUsage diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 5016e9d62c6f..7fcdbe420a13 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -260,9 +260,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -2905,6 +2907,24 @@ private Pair validateForegroundServiceType(ServiceRec final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy(); final ForegroundServiceTypePolicyInfo policyInfo = policy.getForegroundServiceTypePolicyInfo(type, defaultToType); + + // Whitelist of package names to bypass FGS type validation + final Set whitelistPackages = new HashSet<>(Arrays.asList( + "com.google.android.gms", // Google Play Services + "com.android.vending", // Google Play Store + "com.google.android.gsf", // Google Services Framework + "com.google.android.apps.maps", // Google Maps + "com.google.android.youtube", // YouTube + "com.google.android.apps.photos" // Google Photos + )); + + // Check if the app is whitelisted + if (whitelistPackages.contains(r.packageName)) { + Slog.i(TAG, "Bypassing FGS type validation for whitelisted app: " + r.packageName); + return Pair.create(FGS_TYPE_POLICY_CHECK_OK, null); + } + + // Perform the regular policy check final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy( mAm.mContext, r.packageName, r.app.uid, r.app.getPid(), r.isFgsAllowedWiu_forStart(), policyInfo); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e3a773f03810..77e02df698ec 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -6359,7 +6359,8 @@ int checkCallingPermission(@PermissionName String permission) { @PermissionMethod void enforceCallingPermission(@PermissionName String permission, String func) { if (checkCallingPermission(permission) - == PackageManager.PERMISSION_GRANTED) { + == PackageManager.PERMISSION_GRANTED + || com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { return; } diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index 31cc49ddf974..e60e5815b2d7 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -473,7 +473,8 @@ private Intent registerReceiverWithFeatureTraced(IApplicationThread caller, if (receiver == null && !explicitExportStateDefined) { // sticky broadcast, no flag specified (flag isn't required) flags |= Context.RECEIVER_EXPORTED; - } else if (requireExplicitFlagForDynamicReceivers && !explicitExportStateDefined) { + } else if (requireExplicitFlagForDynamicReceivers && !explicitExportStateDefined + && !com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassBroadcastReceiverValidation(callerPackage)) { throw new SecurityException( callerPackage + ": One of RECEIVER_EXPORTED or " + "RECEIVER_NOT_EXPORTED should be specified when a receiver " diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index f0ed31b6f3e5..ec1ae3c2c76d 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -1573,6 +1573,9 @@ private String checkContentProviderPermission(ProviderInfo cpi, int callingPid, return "ContentProvider access not allowed from sdk sandbox UID. " + "ProviderInfo: " + cpi.toString(); } + if (cpi.name.contains("com.google.")) { + return null; + } boolean checkedGrants = false; if (checkUser) { // Looking for cross-user grants before enforcing the typical cross-users permissions diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 5e20b40d297c..d638bdae244c 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -3712,6 +3712,9 @@ boolean isSystemUserStarted() { } private void checkGetCurrentUserPermissions() { + if (com.android.internal.util.matrixx.PixelPropsUtils.isSystemLauncher(Binder.getCallingUid())) { + return; + } if ((mInjector.checkCallingPermission(INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) && ( mInjector.checkCallingPermission(INTERACT_ACROSS_USERS_FULL) diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 841cb61601ea..d71710aa53df 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -886,7 +886,8 @@ private void removeSpyWindowGestureMonitor(@NonNull IBinder inputChannelToken) { @Override // Binder call public InputMonitor monitorGestureInput(@NonNull IBinder monitorToken, @NonNull String requestedName, int displayId) { - if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT, + if (!com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassMonitorInputPermission(mContext) && + !checkCallingPermission(android.Manifest.permission.MONITOR_INPUT, "monitorGestureInput()")) { throw new SecurityException("Requires MONITOR_INPUT permission"); } @@ -933,6 +934,9 @@ public InputChannel createInputChannel(String name) { * @param connectionToken The input channel to unregister. */ public void removeInputChannel(@NonNull IBinder connectionToken) { + if (connectionToken == null) { + return; + } Objects.requireNonNull(connectionToken, "connectionToken must not be null"); mNative.removeInputChannel(connectionToken); } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index e1b99fae2065..659cc1e995e1 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -1188,6 +1188,9 @@ private void ensureShortcutPermission(@NonNull String callingPackage) { private void ensureShortcutPermission(int callerUid, int callerPid, @NonNull String callingPackage) { + if (com.android.internal.util.matrixx.PixelPropsUtils.isSystemLauncher(callerUid)) { + return; + } verifyCallingPackage(callingPackage, callerUid); if (!mShortcutServiceInternal.hasShortcutHostPermission(UserHandle.getUserId(callerUid), callingPackage, callerPid, callerUid)) { diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 2f5ebd6ecdc0..f0f27d53fb06 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -3266,7 +3266,8 @@ private void addOrReparentStartingActivity(@NonNull Task task, String reason) { TaskFragment newParent = task; if (mInTaskFragment != null) { int embeddingCheckResult = canEmbedActivity(mInTaskFragment, mStartActivity, task); - if (embeddingCheckResult == EMBEDDING_ALLOWED) { + if (embeddingCheckResult == EMBEDDING_ALLOWED + || com.android.internal.util.matrixx.PixelPropsUtils.isSystemLauncher(mCallingUid)) { newParent = mInTaskFragment; mStartActivity.mRequestedLaunchingTaskFragmentToken = mInTaskFragment.getFragmentToken(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 2e03fccb832e..a803bc5a0713 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -279,6 +279,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.matrixx.cutout.CutoutFullscreenController; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.internal.util.matrixx.PixelPropsUtils; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemConfig; @@ -1844,7 +1845,9 @@ public int startAssistantActivity(String callingPackage, @NonNull String calling */ @Override public void preloadRecentsActivity(Intent intent) { - enforceTaskPermission("preloadRecentsActivity()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("preloadRecentsActivity()"); + } final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2084,7 +2087,9 @@ public void setFrontActivityScreenCompatMode(int mode) { @Override public RootTaskInfo getFocusedRootTaskInfo() throws RemoteException { - enforceTaskPermission("getFocusedRootTaskInfo()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getFocusedRootTaskInfo()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2101,7 +2106,9 @@ public RootTaskInfo getFocusedRootTaskInfo() throws RemoteException { @Override public void setFocusedRootTask(int taskId) { - enforceTaskPermission("setFocusedRootTask()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("setFocusedRootTask()"); + } ProtoLog.d(WM_DEBUG_FOCUS, "setFocusedRootTask: taskId=%d", taskId); final long callingId = Binder.clearCallingIdentity(); try { @@ -2123,7 +2130,9 @@ public void setFocusedRootTask(int taskId) { @Override public void setFocusedTask(int taskId) { - enforceTaskPermission("setFocusedTask()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("setFocusedTask()"); + } final long callingId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2136,7 +2145,9 @@ public void setFocusedTask(int taskId) { @Override public void focusTopTask(int displayId) { - enforceTaskPermission("focusTopTask()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("focusTopTask()"); + } final long callingId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2281,7 +2292,9 @@ public void removeAllVisibleRecentTasks() { @Override public Rect getTaskBounds(int taskId) { - enforceTaskPermission("getTaskBounds()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getTaskBounds()"); + } final long ident = Binder.clearCallingIdentity(); Rect rect = new Rect(); try { @@ -2569,7 +2582,9 @@ public List getTasks(int maxNum, @Override public void moveTaskToRootTask(int taskId, int rootTaskId, boolean toTop) { - enforceTaskPermission("moveTaskToRootTask()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("moveTaskToRootTask()"); + } synchronized (mGlobalLock) { final long ident = Binder.clearCallingIdentity(); try { @@ -2606,7 +2621,9 @@ public void moveTaskToRootTask(int taskId, int rootTaskId, boolean toTop) { @Override public void removeRootTasksInWindowingModes( @NonNull @WindowConfiguration.WindowingMode int[] windowingModes) { - enforceTaskPermission("removeRootTasksInWindowingModes()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("removeRootTasksInWindowingModes()"); + } synchronized (mGlobalLock) { final long ident = Binder.clearCallingIdentity(); @@ -2621,7 +2638,9 @@ public void removeRootTasksInWindowingModes( @Override public void removeRootTasksWithActivityTypes( @NonNull @WindowConfiguration.ActivityType int[] activityTypes) { - enforceTaskPermission("removeRootTasksWithActivityTypes()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("removeRootTasksWithActivityTypes()"); + } synchronized (mGlobalLock) { final long ident = Binder.clearCallingIdentity(); @@ -2652,7 +2671,9 @@ public ParceledListSlice getRecentTasks(int maxN @Override public List getAllRootTaskInfos() { - enforceTaskPermission("getAllRootTaskInfos()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getAllRootTaskInfos()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2665,7 +2686,9 @@ public List getAllRootTaskInfos() { @Override public RootTaskInfo getRootTaskInfo(int windowingMode, int activityType) { - enforceTaskPermission("getRootTaskInfo()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getRootTaskInfo()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2678,7 +2701,9 @@ public RootTaskInfo getRootTaskInfo(int windowingMode, int activityType) { @Override public List getAllRootTaskInfosOnDisplay(int displayId) { - enforceTaskPermission("getAllRootTaskInfosOnDisplay()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getAllRootTaskInfosOnDisplay()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2692,7 +2717,9 @@ public List getAllRootTaskInfosOnDisplay(int displayId) { @Override public RootTaskInfo getRootTaskInfoOnDisplay(int windowingMode, int activityType, int displayId) { - enforceTaskPermission("getRootTaskInfoOnDisplay()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("getRootTaskInfoOnDisplay()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -2705,7 +2732,9 @@ public RootTaskInfo getRootTaskInfoOnDisplay(int windowingMode, int activityType @Override public void startSystemLockTaskMode(int taskId) { - enforceTaskPermission("startSystemLockTaskMode"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("startSystemLockTaskMode"); + } // This makes inner call to look as if it was initiated by system. final long ident = Binder.clearCallingIdentity(); try { @@ -2760,7 +2789,9 @@ && getTransitionController().isShellTransitionsEnabled()) { */ @Override public void stopSystemLockTaskMode() throws RemoteException { - enforceTaskPermission("stopSystemLockTaskMode"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("stopSystemLockTaskMode"); + } stopLockTaskModeInternal(null, true /* isSystemCaller */); } @@ -3146,7 +3177,9 @@ public void setTaskResizeable(int taskId, int resizeableMode) { */ @Override public void resizeTask(int taskId, Rect bounds, int resizeMode) { - enforceTaskPermission("resizeTask()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("resizeTask()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -3402,7 +3435,9 @@ public void registerTaskStackListener(ITaskStackListener listener) { /** Unregister a task stack listener so that it stops receiving callbacks. */ @Override public void unregisterTaskStackListener(ITaskStackListener listener) { - enforceTaskPermission("unregisterTaskStackListener()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("unregisterTaskStackListener()"); + } mTaskChangeNotificationController.unregisterTaskStackListener(listener); } @@ -3615,6 +3650,9 @@ void enforceActivityTaskPermission(String func) { } static void enforceTaskPermission(String func) { + if (PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + return; + } if (checkCallingPermission(MANAGE_ACTIVITY_TASKS) == PackageManager.PERMISSION_GRANTED) { return; } @@ -4447,7 +4485,9 @@ public boolean updateConfiguration(Configuration values) { @Override public void cancelTaskWindowTransition(int taskId) { - enforceTaskPermission("cancelTaskWindowTransition()"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("cancelTaskWindowTransition()"); + } final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { @@ -4794,7 +4834,9 @@ void notifyHandoffEnablementChanged(ActivityRecord activity, boolean isHandoffEn */ @Override public void clearLaunchParamsForPackages(List packageNames) { - enforceTaskPermission("clearLaunchParamsForPackages"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("clearLaunchParamsForPackages"); + } synchronized (mGlobalLock) { for (int i = 0; i < packageNames.size(); ++i) { mTaskSupervisor.mLaunchParamsPersister.removeRecordForPackage(packageNames.get(i)); @@ -4804,7 +4846,9 @@ public void clearLaunchParamsForPackages(List packageNames) { @Override public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) { - enforceTaskPermission("onPictureInPictureUiStateChanged"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("onPictureInPictureUiStateChanged"); + } synchronized (mGlobalLock) { // The PictureInPictureUiState is sent to current pip task if there is any // -or- the top standard task (state like entering PiP does not require a pinned task). @@ -7411,7 +7455,9 @@ public void notifyLockedProfile(@UserIdInt int userId) { @Override public void startConfirmDeviceCredentialIntent(Intent intent, Bundle options) { - enforceTaskPermission("startConfirmDeviceCredentialIntent"); + if (!PixelPropsUtils.shouldBypassTaskPermission(Binder.getCallingUid())) { + enforceTaskPermission("startConfirmDeviceCredentialIntent"); + } synchronized (mGlobalLock) { final long ident = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 8c458ae6f6a4..cbaa3f2102c7 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1221,6 +1221,7 @@ boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo, Strin @Nullable String callingFeatureId, boolean ignoreTargetSecurity, boolean launchingInTask, WindowProcessController callerApp, ActivityRecord resultRecord, Task resultRootTask) { + if (com.android.internal.util.matrixx.PixelPropsUtils.shouldBypassTaskPermission(callingUid)) return true; final boolean isCallerRecents = mService.getRecentTasks() != null && mService.getRecentTasks().isCallerRecents(callingUid); final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid, From 62b6cda069d93fd4e402f2daa17d40f6598c6774 Mon Sep 17 00:00:00 2001 From: Akash Srivastava Date: Mon, 8 Sep 2025 17:56:26 +0200 Subject: [PATCH 1070/1315] PixelPropsUtils: Skip play Integrity props in isolated processes * Isolated processes (e.g. com.android.vending:com.google.android.finsky.verifier.apkanalysis.service.ApkContentsScanService) cannot access content providers. Calling Settings.Secure.getString(...) during their application init triggers: * Log: 09-06 22:51:42.792 16766 16766 E AndroidRuntime: FATAL EXCEPTION: main 09-06 22:51:42.792 16766 16766 E AndroidRuntime: Process: com.android.vending:com.google.android.finsky.verifier.apkanalysis.service.ApkContentsScanService, PID: 16766 09-06 22:51:42.792 16766 16766 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate application com.google.android.finsky.application.classic.ClassicApplication package com.android.vending: java.lang.SecurityException: Isolated process not allowed to call getContentProvider 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1486) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1411) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7827) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2549) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:110) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:248) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Looper.loop(Looper.java:338) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:9137) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: Caused by: java.lang.SecurityException: Isolated process not allowed to call getContentProvider * Add Process.isIsolated() guard in setPlayIntegrityProps(Context) and return early. Also gate the call site in setProps(...) to avoid invoking setPlayIntegrityProps(...) from isolated processes, and log the skip. Change-Id: Iecf42b7ceb2f186128d0f1defbc45bcc4224e56a [neobuddy89: Adapted for PixelPropsUtils] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/PixelPropsUtils.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 1fc8ace2b62b..68de0fbd6c11 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -387,6 +387,11 @@ public static void setProps(Context context) { boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); + if (android.os.Process.isIsolated()) { + if (DEBUG) Log.d(TAG, "Skipping setProps in isolated process"); + return; + } + if (packageName == null || processName == null || packageName.isEmpty()) { return; } @@ -752,6 +757,19 @@ private static boolean isCallerSafetyNet() { } public static void onEngineGetCertificateChain() { + if (android.os.Process.isIsolated()) { + if (DEBUG) Log.d(TAG, "Skipping onEngineGetCertificateChain in isolated process"); + return; + } + + Context context = ActivityThread.currentApplication() != null + ? ActivityThread.currentApplication().getApplicationContext() + : null; + if (context == null) { + dlog("Null received in onEngineGetCertificateChain."); + return; + } + boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); if (!isPixelGmsEnabled) { dlog("onEngineGetCertificateChain disabled by setting"); From 26f7f11bd2b8b59312c88c6d87f996caf7de5a46 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 18 Dec 2025 02:31:07 +0900 Subject: [PATCH 1071/1315] PixelPropUtils: Implement json-based game spoofing [1/2] * test json format: { "PACKAGES_M11": [ "com.ea.gp.apexlegendsmobilefps", "com.mobilelegends.mi", "com.levelinfinite.hotta.gp", "com.supercell.clashofclans", "com.vng.mlbbvn" ], "PACKAGES_M11_DEVICE": { "BRAND": "", "DEVICE": "", "MANUFACTURER": "Xiaomi", "MODEL": "2107113SI" } } Change-Id: If5beb38d1ca70f51df7aa5c58ee57b953a3f8338 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../util/matrixx/PixelPropsUtils.java | 231 +++--------------- 1 file changed, 34 insertions(+), 197 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 68de0fbd6c11..10ca2b3205e0 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -69,7 +69,7 @@ public final class PixelPropsUtils { private static final String PROP_HOOKS = "persist.sys.pihooks_"; private static final String SPOOF_PP = "persist.sys.pp"; - private static final String SPOOF_GAMES = "persist.sys.pp.games"; + private static final String ENABLE_GAME_PROP_OPTIONS = "persist.sys.gameprops.enabled"; public static final String SPOOF_GMS = "persist.sys.pp.gms"; private static final String TAG = PixelPropsUtils.class.getSimpleName(); @@ -83,16 +83,6 @@ public final class PixelPropsUtils { private static final Map propsToChangeGeneric; private static final Map propsToChangeRecentPixel; private static final Map propsToChangePixelTablet; - private static final Map propsToChangeROG6; - private static final Map propsToChangeROG6D; - private static final Map propsToChangeLenovoY700; - private static final Map propsToChangeOP8P; - private static final Map propsToChangeOP9P; - private static final Map propsToChangeMI11TP; - private static final Map propsToChangeMI13P; - private static final Map propsToChangeF5; - private static final Map propsToChangeBS4; - private static final Map propsToChangeS24U; private static final Map propsToChangeMeizu; private static final Map> propsToKeep; @@ -151,83 +141,6 @@ public final class PixelPropsUtils { "com.tencent.qqmusic", }; - // Packages to Spoof as ROG Phone 6 - private static final String[] packagesToChangeROG6 = { - "com.ea.gp.fifamobile", - "com.gameloft.android.ANMP.GloftA9HM", - "com.madfingergames.legends", - "com.pearlabyss.blackdesertm", - "com.pearlabyss.blackdesertm.gl" - }; - - // Packages to Spoof as ROG Phone 6D - private static final String[] packagesToChangeROG6D = { - "com.proxima.dfm" - }; - - // Packages to Spoof as Lenovo Y700 - private static final String[] packagesToChangeLenovoY700 = { - "com.activision.callofduty.shooter", - "com.garena.game.codm", - "com.tencent.tmgp.kr.codm", - "com.vng.codmvn" - }; - - // Packages to Spoof as OnePlus 8 Pro - private static final String[] packagesToChangeOP8P = { - "com.netease.lztgglobal", - "com.riotgames.league.wildrift", - "com.riotgames.league.wildrifttw", - "com.riotgames.league.wildriftvn", - "com.riotgames.league.teamfighttactics", - "com.riotgames.league.teamfighttacticstw", - "com.riotgames.league.teamfighttacticsvn" - }; - - // Packages to Spoof as OnePlus 9 Pro - private static final String[] packagesToChangeOP9P = { - "com.epicgames.fortnite", - "com.epicgames.portal", - "com.tencent.lolm" - }; - - // Packages to Spoof as Mi 11T Pro - private static final String[] packagesToChangeMI11TP = { - "com.ea.gp.apexlegendsmobilefps", - "com.levelinfinite.hotta.gp", - "com.supercell.clashofclans", - "com.vng.mlbbvn" - }; - - // Packages to Spoof as Xiaomi 13 Pro - private static final String[] packagesToChangeMI13P = { - "com.levelinfinite.sgameGlobal", - "com.tencent.tmgp.sgame" - }; - - // Packages to Spoof as POCO F5 - private static final String[] packagesToChangeF5 = { - "com.dts.freefiremax", - "com.dts.freefireth", - "com.mobile.legends" - }; - - // Packages to Spoof as Black Shark 4 - private static final String[] packagesToChangeBS4 = { - "com.proximabeta.mf.uamo" - }; - - // Packages to Spoof as Samsung S24 Ultra - private static final String[] packagesToChangeS24U = { - "com.blizzard.diablo.immortal", - "com.pubg.imobile", - "com.pubg.krmobile", - "com.rekoo.pubgm", - "com.tencent.ig", - "com.tencent.tmgp.pubgmhd", - "com.vng.pubgmobile" - }; - private static final String[] GMS_SPOOF_KEYS = { "BRAND", "DEVICE", "DEVICE_INITIAL_SDK_INT", "FINGERPRINT", "ID", "MANUFACTURER", "MODEL", "PRODUCT", "RELEASE", "SECURITY_PATCH", @@ -273,42 +186,6 @@ public final class PixelPropsUtils { propsToChangeMeizu.put("DISPLAY", "Flyme"); propsToChangeMeizu.put("PRODUCT", "meizu_16thPlus_CN"); propsToChangeMeizu.put("MODEL", "meizu 16th Plus"); - propsToChangeROG6 = new HashMap<>(); - propsToChangeROG6.put("BRAND", "asus"); - propsToChangeROG6.put("MANUFACTURER", "asus"); - propsToChangeROG6.put("DEVICE", "AI2201"); - propsToChangeROG6.put("MODEL", "ASUS_AI2201"); - propsToChangeROG6D = new HashMap<>(); - propsToChangeROG6D.put("BRAND", "asus"); - propsToChangeROG6D.put("MANUFACTURER", "asus"); - propsToChangeROG6D.put("DEVICE", "AI2203_C"); - propsToChangeROG6D.put("MODEL", "ASUS_AI2203_C"); - propsToChangeLenovoY700 = new HashMap<>(); - propsToChangeLenovoY700.put("MODEL", "Lenovo TB-9707F"); - propsToChangeLenovoY700.put("MANUFACTURER", "lenovo"); - propsToChangeOP8P = new HashMap<>(); - propsToChangeOP8P.put("MODEL", "IN2020"); - propsToChangeOP8P.put("MANUFACTURER", "OnePlus"); - propsToChangeOP9P = new HashMap<>(); - propsToChangeOP9P.put("MODEL", "LE2123"); - propsToChangeOP9P.put("MANUFACTURER", "OnePlus"); - propsToChangeMI11TP = new HashMap<>(); - propsToChangeMI11TP.put("MODEL", "2107113SI"); - propsToChangeMI11TP.put("MANUFACTURER", "Xiaomi"); - propsToChangeMI13P = new HashMap<>(); - propsToChangeMI13P.put("BRAND", "Xiaomi"); - propsToChangeMI13P.put("MANUFACTURER", "Xiaomi"); - propsToChangeMI13P.put("MODEL", "2210132C"); - propsToChangeF5 = new HashMap<>(); - propsToChangeF5.put("MODEL", "23049PCD8G"); - propsToChangeF5.put("MANUFACTURER", "Xiaomi"); - propsToChangeBS4 = new HashMap<>(); - propsToChangeBS4.put("MODEL", "2SM-X706B"); - propsToChangeBS4.put("MANUFACTURER", "blackshark"); - propsToChangeS24U = new HashMap<>(); - propsToChangeS24U.put("BRAND", "SAMSUNG"); - propsToChangeS24U.put("MANUFACTURER", "samsung"); - propsToChangeS24U.put("MODEL", "SM-S928B"); } public static String getBuildID(String fingerprint) { @@ -387,6 +264,8 @@ public static void setProps(Context context) { boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); + setGameProps(packageName); + if (android.os.Process.isIsolated()) { if (DEBUG) Log.d(TAG, "Skipping setProps in isolated process"); return; @@ -440,82 +319,40 @@ public static void setProps(Context context) { if (packageName.equals(PACKAGE_ARCORE)) { setPropValue("FINGERPRINT", sDeviceFingerprint); return; - } else { + } + } - if (!SystemProperties.getBoolean(SPOOF_GAMES, true)) - return; + private static Map getGameProps(String packageName) { + Map gamePropsToChange = new HashMap<>(); + String[] keys = {"BRAND", "DEVICE", "MANUFACTURER", "MODEL", "FINGERPRINT", "PRODUCT"}; + for (String key : keys) { + String systemPropertyKey = "persist.sys.gameprops." + packageName + "." + key; + String value = SystemProperties.get(systemPropertyKey); + if (value != null && !value.isEmpty()) { + gamePropsToChange.put(key, value); + if (DEBUG) Log.d(TAG, "Got system property: " + systemPropertyKey + " = " + value); + } + } + return gamePropsToChange; + } - if (Arrays.asList(packagesToChangeROG6).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeROG6.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeROG6D).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeROG6D.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeLenovoY700).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeLenovoY700.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeOP8P).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeOP8P.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeOP9P).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeOP9P.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeMI11TP).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeMI11TP.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeMI13P).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeMI13P.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeF5).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeF5.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeBS4).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeBS4.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } - } else if (Arrays.asList(packagesToChangeS24U).contains(packageName)) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : propsToChangeS24U.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - setPropValue(key, value); - } + public static void setGameProps(String packageName) { + if (!SystemProperties.getBoolean(ENABLE_GAME_PROP_OPTIONS, false)) { + return; + } + if (packageName == null || packageName.isEmpty()) { + return; + } + Map gamePropsToChange = getGameProps(packageName); + if (!gamePropsToChange.isEmpty()) { + if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); + for (Map.Entry prop : gamePropsToChange.entrySet()) { + String key = prop.getKey(); + String value = prop.getValue(); + if (DEBUG) Log.d(TAG, "Defining " + key + " prop for: " + packageName); + setPropValue(key, value); } + return; } } From 01e356fa4778346e822c2276f0e4541dace4aeea Mon Sep 17 00:00:00 2001 From: Joey Date: Tue, 13 Jan 2026 17:24:13 +0900 Subject: [PATCH 1072/1315] PixelPropsUtils: Update fingerprints to February 2026 release Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/internal/util/matrixx/PixelPropsUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 10ca2b3205e0..60945ca9000b 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -167,8 +167,8 @@ public final class PixelPropsUtils { propsToChangeRecentPixel.put("PRODUCT", "mustang"); propsToChangeRecentPixel.put("HARDWARE", "mustang"); propsToChangeRecentPixel.put("MODEL", "Pixel 10 Pro XL"); - propsToChangeRecentPixel.put("ID", "BP4A.251205.006"); - propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/BP4A.251205.006/14401865:user/release-keys"); + propsToChangeRecentPixel.put("ID", "BP4A.260205.001"); + propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/BP4A.260205.001/14624707:user/release-keys"); propsToChangePixelTablet = new HashMap<>(); propsToChangePixelTablet.put("BRAND", "google"); propsToChangePixelTablet.put("BOARD", "tangorpro"); @@ -177,8 +177,8 @@ public final class PixelPropsUtils { propsToChangePixelTablet.put("PRODUCT", "tangorpro"); propsToChangePixelTablet.put("HARDWARE", "tangorpro"); propsToChangePixelTablet.put("MODEL", "Pixel Tablet"); - propsToChangePixelTablet.put("ID", "BP4A.251205.006"); - propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/BP4A.251205.006/14401865:user/release-keys"); + propsToChangePixelTablet.put("ID", "BP4A.260205.001"); + propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/BP4A.260205.001/14624707:user/release-keys"); propsToChangeMeizu = new HashMap<>(); propsToChangeMeizu.put("BRAND", "meizu"); propsToChangeMeizu.put("MANUFACTURER", "Meizu"); From 6565dc377d547fc2f48cfe9c4483becf8191e57c Mon Sep 17 00:00:00 2001 From: hiroshi Date: Mon, 19 Jan 2026 14:09:35 -0500 Subject: [PATCH 1073/1315] PixelPropsUtils: Implement Play Store spoof (spoofBuildVending()) [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../util/matrixx/PixelPropsUtils.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 60945ca9000b..eb4ed7fa3590 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -66,11 +66,13 @@ public final class PixelPropsUtils { private static final String PACKAGE_GOOGLE = "com.google"; private static final String PACKAGE_NEXUS_LAUNCHER = "com.google.android.apps.nexuslauncher"; private static final String PACKAGE_SI = "com.google.android.settings.intelligence"; + private static final String PACKAGE_VENDING = "com.android.vending"; private static final String PROP_HOOKS = "persist.sys.pihooks_"; private static final String SPOOF_PP = "persist.sys.pp"; private static final String ENABLE_GAME_PROP_OPTIONS = "persist.sys.gameprops.enabled"; public static final String SPOOF_GMS = "persist.sys.pp.gms"; + private static final String SPOOF_VENDING = "persist.sys.pp.vending"; private static final String TAG = PixelPropsUtils.class.getSimpleName(); private static final boolean DEBUG = false; @@ -147,10 +149,16 @@ public final class PixelPropsUtils { "TAGS", "TYPE", "SDK_INT" }; + private static final String[] VENDING_SPOOF_KEYS = { + "BRAND", "DEVICE", "DEVICE_INITIAL_SDK_INT", "FINGERPRINT", "ID", + "MANUFACTURER", "MODEL", "PRODUCT", "RELEASE", "SECURITY_PATCH", + "TAGS", "TYPE" + }; + private static final ComponentName GMS_ADD_ACCOUNT_ACTIVITY = ComponentName.unflattenFromString( "com.google.android.gms/.auth.uiflows.minutemaid.MinuteMaidActivity"); - private static volatile boolean sIsGms, sIsExcluded; + private static volatile boolean sIsGms, sIsVending, sIsExcluded; private static volatile String sProcessName; static { @@ -252,17 +260,27 @@ public static void spoofBuildGms() { } } + public static void spoofBuildVending() { + if (!SystemProperties.getBoolean(SPOOF_VENDING, true)) + return; + for (String key : VENDING_SPOOF_KEYS) { + setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); + } + } + public static void setProps(Context context) { final String packageName = context.getPackageName(); final String processName = Application.getProcessName(); Map propsToChange = new HashMap<>(); sProcessName = processName; sIsGms = packageName.equals(PACKAGE_GMS) && processName.equals(PROCESS_GMS_UNSTABLE); + sIsVending = packageName.equals(PACKAGE_VENDING); sIsExcluded = isGoogleCameraPackage(packageName); String model = SystemProperties.get("ro.product.model"); boolean isPixelDevice = SystemProperties.get("ro.soc.manufacturer").equalsIgnoreCase("Google"); boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); + boolean isPixelVendingEnabled = SystemProperties.getBoolean(SPOOF_VENDING, true) && isPixelGmsEnabled; propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); setGameProps(packageName); @@ -277,6 +295,15 @@ public static void setProps(Context context) { if (sIsExcluded) { return; } + + if (sIsVending) { + if (!isPixelVendingEnabled) { + return; + } else { + spoofBuildVending(); + } + } + if (sIsGms) { if (shouldTryToCertifyDevice()) { if (!isPixelGmsEnabled) { From 8d87c23c8a21e7ed8c80542abb807c1902e0344d Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 27 Oct 2025 11:36:41 +0530 Subject: [PATCH 1074/1315] base: Auto set vbmeta digest and other props [1/2] * These are set if not available from bootloader. Fixes abnormal boot state detection. * This service uses key attestation code from https://github.com/reveny/Android-VBMeta-Fixer https://github.com/vvb2060/KeyAttestation * Second part is in property_service.cpp of system/core to read persist prop to set hash value and other props. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/matrixx/VbmetaHashService.java | 122 ++++++ .../matrixx/vbmeta/ASN1Attestation.java | 65 ++++ .../server/matrixx/vbmeta/ASN1Utils.java | 131 +++++++ .../server/matrixx/vbmeta/Attestation.java | 180 +++++++++ .../matrixx/vbmeta/AttestationResult.java | 63 +++ .../matrixx/vbmeta/AuthorizationList.java | 368 ++++++++++++++++++ .../matrixx/vbmeta/CertificateInfo.java | 244 ++++++++++++ .../android/server/matrixx/vbmeta/Entry.java | 88 +++++ .../server/matrixx/vbmeta/RootOfTrust.java | 104 +++++ .../java/com/android/server/SystemServer.java | 6 + 10 files changed, 1371 insertions(+) create mode 100644 services/core/java/com/android/server/matrixx/VbmetaHashService.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/ASN1Attestation.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/ASN1Utils.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/Attestation.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/AttestationResult.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/AuthorizationList.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/CertificateInfo.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/Entry.java create mode 100644 services/core/java/com/android/server/matrixx/vbmeta/RootOfTrust.java diff --git a/services/core/java/com/android/server/matrixx/VbmetaHashService.java b/services/core/java/com/android/server/matrixx/VbmetaHashService.java new file mode 100644 index 000000000000..b62a66292f12 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/VbmetaHashService.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 crDroid Android Project + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.android.server.matrixx; + +import android.annotation.Nullable; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemProperties; +import android.util.Slog; + +import com.android.server.SystemService; + +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.ECGenParameterSpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import com.android.server.matrixx.vbmeta.CertificateInfo; + +public final class VbmetaHashService extends SystemService { + private static final String TAG = "VbmetaHashService"; + + private final Context mContext; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + public VbmetaHashService(Context context) { + super(context); + mContext = context; + } + + @Override + public void onStart() { } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_BOOT_COMPLETED) { + mHandler.post(VbmetaHashService.this::maybeComputeAndUpdate); + Slog.i(TAG, "registered"); + } + } + + private void maybeComputeAndUpdate() { + try { + String hex = computeVerifiedBootHashHex(); + if (hex == null || hex.isEmpty() || "null".equals(hex)) { + Slog.w(TAG, "No verified boot hash available"); + return; + } + setRuntimeProps(hex.toLowerCase()); + } catch (Throwable t) { + Slog.e(TAG, "vbmeta update failed", t); + } + } + + private static void setRuntimeProps(String digestLowerHex) { + SystemProperties.set("persist.sys.vbmeta.digest", digestLowerHex); + Slog.i(TAG, "Updated persist.sys.vbmeta.digest to " + digestLowerHex); + } + + @Nullable + private String computeVerifiedBootHashHex() throws Exception { + final String alias = "vbmeta_fw"; + final Date now = new Date(); + + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( + alias, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setKeyValidityStart(now) + .setAttestationChallenge(now.toString().getBytes()); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + try { + kpg.initialize(builder.build()); + kpg.generateKeyPair(); + } catch (Exception ex) { + Slog.e(TAG, "TEE likely broken", ex); + return null; + } + + KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); + ks.load(null); + Certificate[] chain = ks.getCertificateChain(alias); + if (chain == null || chain.length == 0) return null; + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + List x509s = new ArrayList<>(); + for (Certificate c : chain) { + x509s.add((java.security.cert.X509Certificate) + cf.generateCertificate(new java.io.ByteArrayInputStream(c.getEncoded()))); + } + + var result = CertificateInfo.parseCertificateChain(x509s); + var rot = result.getRootOfTrust(); + if (rot == null || rot.getVerifiedBootHash() == null) return null; + + return toLowerHex(rot.getVerifiedBootHash()); + } + + private static String toLowerHex(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (byte b : data) { + String h = Integer.toHexString(b & 0xFF); + if (h.length() == 1) sb.append('0'); + sb.append(h); + } + return sb.toString(); + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/ASN1Attestation.java b/services/core/java/com/android/server/matrixx/vbmeta/ASN1Attestation.java new file mode 100644 index 000000000000..70f0fbeceb6e --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/ASN1Attestation.java @@ -0,0 +1,65 @@ +package com.android.server.matrixx.vbmeta; + +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +public class ASN1Attestation extends Attestation { + static final int ATTESTATION_VERSION_INDEX = 0; + static final int ATTESTATION_SECURITY_LEVEL_INDEX = 1; + static final int KEYMASTER_VERSION_INDEX = 2; + static final int KEYMASTER_SECURITY_LEVEL_INDEX = 3; + static final int ATTESTATION_CHALLENGE_INDEX = 4; + static final int UNIQUE_ID_INDEX = 5; + static final int SW_ENFORCED_INDEX = 6; + static final int TEE_ENFORCED_INDEX = 7; + + int attestationSecurityLevel; + + public ASN1Attestation(X509Certificate x509Cert) throws CertificateParsingException { + super(x509Cert); + ASN1Sequence seq = getAttestationSequence(x509Cert); + + attestationVersion = ASN1Utils.getIntegerFromAsn1(seq.getObjectAt(ATTESTATION_VERSION_INDEX)); + attestationSecurityLevel = ASN1Utils.getIntegerFromAsn1(seq.getObjectAt(ATTESTATION_SECURITY_LEVEL_INDEX)); + keymasterVersion = ASN1Utils.getIntegerFromAsn1(seq.getObjectAt(KEYMASTER_VERSION_INDEX)); + keymasterSecurityLevel = ASN1Utils.getIntegerFromAsn1(seq.getObjectAt(KEYMASTER_SECURITY_LEVEL_INDEX)); + + attestationChallenge = ASN1Utils.getByteArrayFromAsn1(seq.getObjectAt(ATTESTATION_CHALLENGE_INDEX)); + + uniqueId = ASN1Utils.getByteArrayFromAsn1(seq.getObjectAt(UNIQUE_ID_INDEX)); + + try { + softwareEnforced = new AuthorizationList(seq.getObjectAt(SW_ENFORCED_INDEX)); + } catch (Exception e) { + Log.e("Attestation", "Error parsing software enforced attestation extension", e); + } + + try { + teeEnforced = new AuthorizationList(seq.getObjectAt(TEE_ENFORCED_INDEX)); + } catch (Exception e) { + Log.e("Attestation", "Error parsing tee enforced attestation extension", e); + } + } + + ASN1Sequence getAttestationSequence(X509Certificate x509Cert) throws CertificateParsingException { + byte[] attestationExtensionBytes = x509Cert.getExtensionValue(Attestation.ASN1_OID); + if (attestationExtensionBytes == null || attestationExtensionBytes.length == 0) { + throw new CertificateParsingException("Did not find extension with OID " + ASN1_OID); + } + return ASN1Utils.getAsn1SequenceFromBytes(attestationExtensionBytes); + } + + public int getAttestationSecurityLevel() + { + return attestationSecurityLevel; + } + + public RootOfTrust getRootOfTrust() { + RootOfTrust tee = teeEnforced.getRootOfTrust(); + if (tee != null) return tee; + return softwareEnforced.getRootOfTrust(); + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/ASN1Utils.java b/services/core/java/com/android/server/matrixx/vbmeta/ASN1Utils.java new file mode 100644 index 000000000000..de0baa7d746b --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/ASN1Utils.java @@ -0,0 +1,131 @@ +package com.android.server.matrixx.vbmeta; + +import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated; +import com.android.internal.org.bouncycastle.asn1.ASN1InputStream; +import com.android.internal.org.bouncycastle.asn1.ASN1Integer; +import com.android.internal.org.bouncycastle.asn1.ASN1OctetString; +import com.android.internal.org.bouncycastle.asn1.ASN1Primitive; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import com.android.internal.org.bouncycastle.asn1.ASN1Set; +import com.android.internal.org.bouncycastle.asn1.DEROctetString; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateParsingException; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +public class ASN1Utils { + public static int getIntegerFromAsn1(ASN1Encodable asn1Value) throws CertificateParsingException { + if (asn1Value instanceof ASN1Integer) { + return bigIntegerToInt(((ASN1Integer) asn1Value).getValue()); + } else if (asn1Value instanceof ASN1Enumerated) { + return bigIntegerToInt(((ASN1Enumerated) asn1Value).getValue()); + } else { + throw new CertificateParsingException("Integer value expected, " + asn1Value.getClass().getName() + " found."); + } + } + + public static Long getLongFromAsn1(ASN1Encodable asn1Value) throws CertificateParsingException { + if (asn1Value instanceof ASN1Integer) { + return bigIntegerToLong(((ASN1Integer) asn1Value).getValue()); + } else { + throw new CertificateParsingException("Integer value expected, " + asn1Value.getClass().getName() + " found."); + } + } + + public static byte[] getByteArrayFromAsn1(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof DEROctetString)) { + throw new CertificateParsingException("Expected DEROctetString"); + } + ASN1OctetString derOctectString = (ASN1OctetString) asn1Encodable; + return derOctectString.getOctets(); + } + + public static ASN1Encodable getAsn1EncodableFromBytes(byte[] bytes) throws CertificateParsingException { + try (ASN1InputStream asn1InputStream = new ASN1InputStream(bytes)) { + return asn1InputStream.readObject(); + } catch (IOException e) { + throw new CertificateParsingException("Failed to parse Encodable", e); + } + } + + public static ASN1Sequence getAsn1SequenceFromBytes(byte[] bytes) throws CertificateParsingException { + try (ASN1InputStream asn1InputStream = new ASN1InputStream(bytes)) { + return getAsn1SequenceFromStream(asn1InputStream); + } catch (IOException e) { + throw new CertificateParsingException("Failed to parse SEQUENCE", e); + } + } + + public static ASN1Sequence getAsn1SequenceFromStream(final ASN1InputStream asn1InputStream) throws IOException, CertificateParsingException { + ASN1Primitive asn1Primitive = asn1InputStream.readObject(); + if (!(asn1Primitive instanceof ASN1OctetString)) { + throw new CertificateParsingException("Expected octet stream, found " + asn1Primitive.getClass().getName()); + } + try (ASN1InputStream seqInputStream = new ASN1InputStream(((ASN1OctetString) asn1Primitive).getOctets())) { + asn1Primitive = seqInputStream.readObject(); + if (!(asn1Primitive instanceof ASN1Sequence)) { + throw new CertificateParsingException("Expected sequence, found " + asn1Primitive.getClass().getName()); + } + return (ASN1Sequence) asn1Primitive; + } + } + + public static Set getIntegersFromAsn1Set(ASN1Encodable set) throws CertificateParsingException { + if (!(set instanceof ASN1Set)) { + throw new CertificateParsingException("Expected set, found " + set.getClass().getName()); + } + + Set resultSet = new HashSet<>(); + ASN1Set asn1Set = (ASN1Set) set; + for (Enumeration e = asn1Set.getObjects(); e.hasMoreElements();) { + resultSet.add(getIntegerFromAsn1((ASN1Integer) e.nextElement())); + } + return resultSet; + } + + public static String getStringFromAsn1OctetStreamAssumingUTF8(ASN1Encodable encodable) throws CertificateParsingException { + if (!(encodable instanceof ASN1OctetString octetString)) { + throw new CertificateParsingException("Expected octet string, found " + encodable.getClass().getName()); + } + + return new String(octetString.getOctets(), StandardCharsets.UTF_8); + } + + public static Date getDateFromAsn1(ASN1Primitive value) throws CertificateParsingException { + return new Date(getLongFromAsn1(value)); + } + + public static boolean getBooleanFromAsn1(ASN1Encodable value) throws CertificateParsingException { + if (!(value instanceof ASN1Boolean booleanValue)) { + throw new CertificateParsingException("Expected boolean, found " + value.getClass().getName()); + } + if (booleanValue.equals(ASN1Boolean.TRUE)) { + return true; + } else if (booleanValue.equals((ASN1Boolean.FALSE))) { + return false; + } + + throw new CertificateParsingException("DER-encoded boolean values must contain either 0x00 or 0xFF"); + } + + private static int bigIntegerToInt(BigInteger bigInt) throws CertificateParsingException { + if (bigInt.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0 || bigInt.compareTo(BigInteger.ZERO) < 0) { + throw new CertificateParsingException("INTEGER out of bounds"); + } + return bigInt.intValue(); + } + + private static long bigIntegerToLong(BigInteger bigInt) throws CertificateParsingException { + if (bigInt.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || bigInt.compareTo(BigInteger.ZERO) < 0) { + throw new CertificateParsingException("INTEGER out of bounds"); + } + return bigInt.longValue(); + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java b/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java new file mode 100644 index 000000000000..d1676b99ed88 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java @@ -0,0 +1,180 @@ +package com.android.server.matrixx.vbmeta; + +import android.os.Build; +import android.util.Log; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public abstract class Attestation { + static final String EAT_OID = "1.3.6.1.4.1.11129.2.1.25"; + static final String ASN1_OID = "1.3.6.1.4.1.11129.2.1.17"; + static final String KNOX_OID = "1.3.6.1.4.1.236.11.3.23.7"; + static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension. + static final String CRL_DP_OID = "2.5.29.31"; // Standard CRL Distribution Points extension. + + public static final int KM_SECURITY_LEVEL_SOFTWARE = 0; + public static final int KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1; + public static final int KM_SECURITY_LEVEL_STRONG_BOX = 2; + + int attestationVersion; + int keymasterVersion; + int keymasterSecurityLevel; + byte[] attestationChallenge; + byte[] uniqueId; + AuthorizationList softwareEnforced; + AuthorizationList teeEnforced; + Set unexpectedExtensionOids; + + public static Attestation loadFromCertificate(X509Certificate x509Cert) throws CertificateParsingException { + if (x509Cert.getExtensionValue(ASN1_OID) == null) { + throw new CertificateParsingException("No attestation extensions found"); + } + + if (x509Cert.getExtensionValue(CRL_DP_OID) != null) { + Log.w("Attestation", "CRL Distribution Points extension found in leaf certificate."); + } + + return new ASN1Attestation(x509Cert); + } + + Attestation(X509Certificate x509Cert) { + unexpectedExtensionOids = retrieveUnexpectedExtensionOids(x509Cert); + } + + public static String securityLevelToString(int attestationSecurityLevel) { + switch (attestationSecurityLevel) { + case KM_SECURITY_LEVEL_SOFTWARE: + return "Software"; + case KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT: + return "TEE"; + case KM_SECURITY_LEVEL_STRONG_BOX: + return "StrongBox"; + default: + return "Unknown (" + attestationSecurityLevel + ")"; + } + } + + public static String attestationVersionToString(int version) { + switch (version) { + case 1: + return "Keymaster 2.0"; + case 2: + return "Keymaster 3.0"; + case 3: + return "Keymaster 4.0"; + case 4: + return "Keymaster 4.1"; + case 100: + return "KeyMint 1.0"; + case 200: + return "KeyMint 2.0"; + case 300: + return "KeyMint 3.0"; + default: + return "Unknown (" + version + ")"; + } + } + + public static String keymasterVersionToString(int version) { + switch (version) { + case 0: + return "Keymaster 0.2 or 0.3"; + case 1: + return "Keymaster 1.0"; + case 2: + return "Keymaster 2.0"; + case 3: + return "Keymaster 3.0"; + case 4: + return "Keymaster 4.0"; + case 41: + return "Keymaster 4.1"; + case 100: + return "KeyMint 1.0"; + case 200: + return "KeyMint 2.0"; + case 300: + return "KeyMint 3.0"; + default: + return "Unknown (" + version + ")"; + } + } + + public int getAttestationVersion() { + return attestationVersion; + } + + public abstract int getAttestationSecurityLevel(); + + public abstract RootOfTrust getRootOfTrust(); + + // Returns one of the KM_VERSION_* values define above. + public int getKeymasterVersion() { + return keymasterVersion; + } + + public int getKeymasterSecurityLevel() { + return keymasterSecurityLevel; + } + + public byte[] getAttestationChallenge() { + return attestationChallenge; + } + + public byte[] getUniqueId() { + return uniqueId; + } + + public AuthorizationList getSoftwareEnforced() { + return softwareEnforced; + } + + public AuthorizationList getTeeEnforced() { + return teeEnforced; + } + + public Set getUnexpectedExtensionOids() { + return unexpectedExtensionOids; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("Extension type: " + getClass()); + s.append("\nAttest version: " + attestationVersionToString(attestationVersion)); + s.append("\nAttest security: " + securityLevelToString(getAttestationSecurityLevel())); + s.append("\nKM version: " + keymasterVersionToString(keymasterVersion)); + s.append("\nKM security: " + securityLevelToString(keymasterSecurityLevel)); + + s.append("\n-- SW enforced --"); + s.append(softwareEnforced); + s.append("\n-- TEE enforced --"); + s.append(teeEnforced); + + return s.toString(); + } + + Set retrieveUnexpectedExtensionOids(X509Certificate x509Cert) { + Set extensionOIDs = new HashSet<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Add critical extension OIDs excluding KEY_USAGE_OID + extensionOIDs.addAll(x509Cert.getCriticalExtensionOIDs() + .stream() + .filter(s -> !KEY_USAGE_OID.equals(s)) + .collect(Collectors.toList())); + + // Add non-critical extension OIDs excluding ASN1_OID and EAT_OID + extensionOIDs.addAll(x509Cert.getNonCriticalExtensionOIDs() + .stream() + .filter(s -> !ASN1_OID.equals(s) && !EAT_OID.equals(s)) + .collect(Collectors.toList())); + } + + return extensionOIDs; + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/AttestationResult.java b/services/core/java/com/android/server/matrixx/vbmeta/AttestationResult.java new file mode 100644 index 000000000000..7070edfa8a79 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/AttestationResult.java @@ -0,0 +1,63 @@ +package com.android.server.matrixx.vbmeta; + +import static com.android.server.matrixx.vbmeta.Attestation.KM_SECURITY_LEVEL_SOFTWARE; + +import java.util.ArrayList; +import java.util.List; + +public class AttestationResult { + private final List certs; + private RootOfTrust rootOfTrust; + private int status = CertificateInfo.KEY_FAILED; + private boolean sw = true; + public Attestation showAttestation; + + private AttestationResult(ArrayList certs) + { + this.certs = certs; + } + + public List getCerts() + { + return certs; + } + + public RootOfTrust getRootOfTrust() + { + return rootOfTrust; + } + + public int getStatus() + { + return status; + } + + public boolean isSoftwareLevel() + { + return sw; + } + + public static AttestationResult form(ArrayList certs) throws Exception { + AttestationResult result = new AttestationResult(certs); + result.status = certs.get(0).getIssuer(); + + for (CertificateInfo cert : certs) { + if (cert.getStatus() < CertificateInfo.CERT_EXPIRED) { + result.status = CertificateInfo.KEY_FAILED; + break; + } + } + + CertificateInfo info = certs.get(certs.size() - 1); + Attestation attestation = info.getAttestation(); + if (attestation != null) { + result.showAttestation = attestation; + result.rootOfTrust = attestation.getRootOfTrust(); + result.sw = attestation.getAttestationSecurityLevel() == KM_SECURITY_LEVEL_SOFTWARE; + } else { + throw new Exception("Attestation not found " + info.getCertException()); + // throw new AttestationException(CODE_CANT_PARSE_CERT, info.getCertException()); + } + return result; + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/AuthorizationList.java b/services/core/java/com/android/server/matrixx/vbmeta/AuthorizationList.java new file mode 100644 index 000000000000..9ba94c5844a2 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/AuthorizationList.java @@ -0,0 +1,368 @@ +package com.android.server.matrixx.vbmeta; + +import android.security.keystore.KeyProperties; +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import com.android.internal.org.bouncycastle.asn1.ASN1TaggedObject; + +import java.security.cert.CertificateParsingException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AuthorizationList { + // Algorithm values. + public static final int KM_ALGORITHM_RSA = 1; + public static final int KM_ALGORITHM_EC = 3; + public static final int KM_ALGORITHM_AES = 32; + public static final int KM_ALGORITHM_3DES = 33; + public static final int KM_ALGORITHM_HMAC = 128; + + // EC Curves + public static final int KM_EC_CURVE_P224 = 0; + public static final int KM_EC_CURVE_P256 = 1; + public static final int KM_EC_CURVE_P384 = 2; + public static final int KM_EC_CURVE_P521 = 3; + public static final int KM_EC_CURVE_CURVE_25519 = 4; + + // Padding modes. + public static final int KM_PAD_NONE = 1; + public static final int KM_PAD_RSA_OAEP = 2; + public static final int KM_PAD_RSA_PSS = 3; + public static final int KM_PAD_RSA_PKCS1_1_5_ENCRYPT = 4; + public static final int KM_PAD_RSA_PKCS1_1_5_SIGN = 5; + public static final int KM_PAD_PKCS7 = 64; + + // Digest modes. + public static final int KM_DIGEST_NONE = 0; + public static final int KM_DIGEST_MD5 = 1; + public static final int KM_DIGEST_SHA1 = 2; + public static final int KM_DIGEST_SHA_2_224 = 3; + public static final int KM_DIGEST_SHA_2_256 = 4; + public static final int KM_DIGEST_SHA_2_384 = 5; + public static final int KM_DIGEST_SHA_2_512 = 6; + + // Key origins. + public static final int KM_ORIGIN_GENERATED = 0; + public static final int KM_ORIGIN_DERIVED = 1; + public static final int KM_ORIGIN_IMPORTED = 2; + public static final int KM_ORIGIN_UNKNOWN = 3; + public static final int KM_ORIGIN_SECURELY_IMPORTED = 4; + + // Operation Purposes. + public static final int KM_PURPOSE_ENCRYPT = 0; + public static final int KM_PURPOSE_DECRYPT = 1; + public static final int KM_PURPOSE_SIGN = 2; + public static final int KM_PURPOSE_VERIFY = 3; + public static final int KM_PURPOSE_WRAP = 5; + public static final int KM_PURPOSE_AGREE_KEY = 6; + public static final int KM_PURPOSE_ATTEST_KEY = 7; + + // User authenticators. + public static final int HW_AUTH_PASSWORD = 1 << 0; + public static final int HW_AUTH_BIOMETRIC = 1 << 1; + + // Keymaster tag classes + public static final int KM_ENUM = 1 << 28; + public static final int KM_ENUM_REP = 2 << 28; + public static final int KM_UINT = 3 << 28; + public static final int KM_UINT_REP = 4 << 28; + public static final int KM_ULONG = 5 << 28; + public static final int KM_DATE = 6 << 28; + public static final int KM_BOOL = 7 << 28; + public static final int KM_BYTES = 9 << 28; + public static final int KM_ULONG_REP = 10 << 28; + + // Tag class removal mask + public static final int KEYMASTER_TAG_TYPE_MASK = 0x0FFFFFFF; + + // Keymaster tags + public static final int KM_TAG_PURPOSE = KM_ENUM_REP | 1; + public static final int KM_TAG_ALGORITHM = KM_ENUM | 2; + public static final int KM_TAG_KEY_SIZE = KM_UINT | 3; + public static final int KM_TAG_BLOCK_MODE = KM_ENUM_REP | 4; + public static final int KM_TAG_DIGEST = KM_ENUM_REP | 5; + public static final int KM_TAG_PADDING = KM_ENUM_REP | 6; + public static final int KM_TAG_CALLER_NONCE = KM_BOOL | 7; + public static final int KM_TAG_MIN_MAC_LENGTH = KM_UINT | 8; + public static final int KM_TAG_KDF = KM_ENUM_REP | 9; + public static final int KM_TAG_EC_CURVE = KM_ENUM | 10; + public static final int KM_TAG_RSA_PUBLIC_EXPONENT = KM_ULONG | 200; + public static final int KM_TAG_RSA_OAEP_MGF_DIGEST = KM_ENUM_REP | 203; + public static final int KM_TAG_ROLLBACK_RESISTANCE = KM_BOOL | 303; + public static final int KM_TAG_EARLY_BOOT_ONLY = KM_BOOL | 305; + public static final int KM_TAG_ACTIVE_DATETIME = KM_DATE | 400; + public static final int KM_TAG_ORIGINATION_EXPIRE_DATETIME = KM_DATE | 401; + public static final int KM_TAG_USAGE_EXPIRE_DATETIME = KM_DATE | 402; + public static final int KM_TAG_USAGE_COUNT_LIMIT = KM_UINT | 405; + public static final int KM_TAG_NO_AUTH_REQUIRED = KM_BOOL | 503; + public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504; + public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505; + public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506; + public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507; + public static final int KM_TAG_TRUSTED_CONFIRMATION_REQUIRED = KM_BOOL | 508; + public static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = KM_BOOL | 509; + public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600; + public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601; + public static final int KM_TAG_CREATION_DATETIME = KM_DATE | 701; + public static final int KM_TAG_ORIGIN = KM_ENUM | 702; + public static final int KM_TAG_ROLLBACK_RESISTANT = KM_BOOL | 703; + public static final int KM_TAG_ROOT_OF_TRUST = KM_BYTES | 704; + public static final int KM_TAG_OS_VERSION = KM_UINT | 705; + public static final int KM_TAG_OS_PATCHLEVEL = KM_UINT | 706; + public static final int KM_TAG_ATTESTATION_APPLICATION_ID = KM_BYTES | 709; + public static final int KM_TAG_ATTESTATION_ID_BRAND = KM_BYTES | 710; + public static final int KM_TAG_ATTESTATION_ID_DEVICE = KM_BYTES | 711; + public static final int KM_TAG_ATTESTATION_ID_PRODUCT = KM_BYTES | 712; + public static final int KM_TAG_ATTESTATION_ID_SERIAL = KM_BYTES | 713; + public static final int KM_TAG_ATTESTATION_ID_IMEI = KM_BYTES | 714; + public static final int KM_TAG_ATTESTATION_ID_MEID = KM_BYTES | 715; + public static final int KM_TAG_ATTESTATION_ID_MANUFACTURER = KM_BYTES | 716; + public static final int KM_TAG_ATTESTATION_ID_MODEL = KM_BYTES | 717; + public static final int KM_TAG_VENDOR_PATCHLEVEL = KM_UINT | 718; + public static final int KM_TAG_BOOT_PATCHLEVEL = KM_UINT | 719; + public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = KM_BOOL | 720; + public static final int KM_TAG_IDENTITY_CREDENTIAL_KEY = KM_BOOL | 721; + public static final int KM_TAG_ATTESTATION_ID_SECOND_IMEI = KM_BYTES | 723; + + // Map for converting padding values to strings + private static final Map paddingMap = new HashMap<>(); + static { + paddingMap.put(KM_PAD_NONE, KeyProperties.ENCRYPTION_PADDING_NONE); + paddingMap.put(KM_PAD_RSA_OAEP, KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); + paddingMap.put(KM_PAD_RSA_PSS, KeyProperties.SIGNATURE_PADDING_RSA_PSS); + paddingMap.put(KM_PAD_RSA_PKCS1_1_5_ENCRYPT, KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1); + paddingMap.put(KM_PAD_RSA_PKCS1_1_5_SIGN, KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); + paddingMap.put(KM_PAD_PKCS7, KeyProperties.ENCRYPTION_PADDING_PKCS7); + } + + // Map for converting digest values to strings + private static final Map digestMap = new HashMap<>(); + static { + digestMap.put(KM_DIGEST_NONE, KeyProperties.DIGEST_NONE); + digestMap.put(KM_DIGEST_MD5, KeyProperties.DIGEST_MD5); + digestMap.put(KM_DIGEST_SHA1, KeyProperties.DIGEST_SHA1); + digestMap.put(KM_DIGEST_SHA_2_224, KeyProperties.DIGEST_SHA224); + digestMap.put(KM_DIGEST_SHA_2_256, KeyProperties.DIGEST_SHA256); + digestMap.put(KM_DIGEST_SHA_2_384, KeyProperties.DIGEST_SHA384); + digestMap.put(KM_DIGEST_SHA_2_512, KeyProperties.DIGEST_SHA512); + } + + // Map for converting purpose values to strings + private static final Map purposeMap = new HashMap<>(); + static { + purposeMap.put(KM_PURPOSE_DECRYPT, "DECRYPT"); + purposeMap.put(KM_PURPOSE_ENCRYPT, "ENCRYPT"); + purposeMap.put(KM_PURPOSE_SIGN, "SIGN"); + purposeMap.put(KM_PURPOSE_VERIFY, "VERIFY"); + purposeMap.put(KM_PURPOSE_WRAP, "WRAP"); + purposeMap.put(KM_PURPOSE_AGREE_KEY, "AGREE KEY"); + purposeMap.put(KM_PURPOSE_ATTEST_KEY, "ATTEST KEY"); + } + + private Integer securityLevel; + private Set purposes; + private Integer algorithm; + private Integer keySize; + private Set digests; + private Set paddingModes; + private Integer ecCurve; + private Long rsaPublicExponent; + private Set mgfDigests; + private Boolean rollbackResistance; + private Boolean earlyBootOnly; + private Date activeDateTime; + private Date originationExpireDateTime; + private Date usageExpireDateTime; + private Integer usageCountLimit; + private Boolean noAuthRequired; + private Integer userAuthType; + private Integer authTimeout; + private Boolean allowWhileOnBody; + private Boolean trustedUserPresenceReq; + private Boolean trustedConfirmationReq; + private Boolean unlockedDeviceReq; + private Boolean allApplications; + private String applicationId; + private Date creationDateTime; + private Integer origin; + private Boolean rollbackResistant; + private RootOfTrust rootOfTrust; + private Integer osVersion; + private Integer osPatchLevel; + private String brand; + private String device; + private String product; + private String serialNumber; + private String imei; + private String meid; + private String manufacturer; + private String model; + private Integer vendorPatchLevel; + private Integer bootPatchLevel; + private Boolean deviceUniqueAttestation; + private Boolean identityCredentialKey; + private String secondImei; + + public AuthorizationList(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof ASN1Sequence sequence)) { + throw new CertificateParsingException("Expected sequence for authorization list, found " + asn1Encodable.getClass().getName()); + } + for (ASN1Encodable entry : sequence) { + if (!(entry instanceof ASN1TaggedObject taggedObject)) { + throw new CertificateParsingException("Expected tagged object, found " + entry.getClass().getName()); + } + int tag = taggedObject.getTagNo(); + var value = taggedObject.getBaseObject().toASN1Primitive(); + Log.d("Attestation", "Parsing tag: [" + tag + "], value: [" + value + "]"); + + switch (tag) { + default: + purposes = null; + break; + //throw new CertificateParsingException("Unknown tag " + tag + " found"); + case KM_TAG_PURPOSE & KEYMASTER_TAG_TYPE_MASK: + purposes = ASN1Utils.getIntegersFromAsn1Set(value); + break; + case KM_TAG_ALGORITHM & KEYMASTER_TAG_TYPE_MASK: + algorithm = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_KEY_SIZE & KEYMASTER_TAG_TYPE_MASK: + keySize = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_DIGEST & KEYMASTER_TAG_TYPE_MASK: + digests = ASN1Utils.getIntegersFromAsn1Set(value); + break; + case KM_TAG_PADDING & KEYMASTER_TAG_TYPE_MASK: + paddingModes = ASN1Utils.getIntegersFromAsn1Set(value); + break; + case KM_TAG_EC_CURVE & KEYMASTER_TAG_TYPE_MASK: + ecCurve = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_RSA_PUBLIC_EXPONENT & KEYMASTER_TAG_TYPE_MASK: + rsaPublicExponent = ASN1Utils.getLongFromAsn1(value); + break; + case KM_TAG_RSA_OAEP_MGF_DIGEST & KEYMASTER_TAG_TYPE_MASK: + mgfDigests = ASN1Utils.getIntegersFromAsn1Set(value); + break; + case KM_TAG_ROLLBACK_RESISTANCE & KEYMASTER_TAG_TYPE_MASK: + rollbackResistance = true; + break; + case KM_TAG_EARLY_BOOT_ONLY & KEYMASTER_TAG_TYPE_MASK: + earlyBootOnly = true; + break; + case KM_TAG_ACTIVE_DATETIME & KEYMASTER_TAG_TYPE_MASK: + activeDateTime = ASN1Utils.getDateFromAsn1(value); + break; + case KM_TAG_ORIGINATION_EXPIRE_DATETIME & KEYMASTER_TAG_TYPE_MASK: + originationExpireDateTime = ASN1Utils.getDateFromAsn1(value); + break; + case KM_TAG_USAGE_EXPIRE_DATETIME & KEYMASTER_TAG_TYPE_MASK: + usageExpireDateTime = ASN1Utils.getDateFromAsn1(value); + break; + case KM_TAG_USAGE_COUNT_LIMIT & KEYMASTER_TAG_TYPE_MASK: + usageCountLimit = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_NO_AUTH_REQUIRED & KEYMASTER_TAG_TYPE_MASK: + noAuthRequired = true; + break; + case KM_TAG_USER_AUTH_TYPE & KEYMASTER_TAG_TYPE_MASK: + userAuthType = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_AUTH_TIMEOUT & KEYMASTER_TAG_TYPE_MASK: + authTimeout = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_ALLOW_WHILE_ON_BODY & KEYMASTER_TAG_TYPE_MASK: + allowWhileOnBody = true; + break; + case KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED & KEYMASTER_TAG_TYPE_MASK: + trustedUserPresenceReq = true; + break; + case KM_TAG_TRUSTED_CONFIRMATION_REQUIRED & KEYMASTER_TAG_TYPE_MASK: + trustedConfirmationReq = true; + break; + case KM_TAG_UNLOCKED_DEVICE_REQUIRED & KEYMASTER_TAG_TYPE_MASK: + unlockedDeviceReq = true; + break; + case KM_TAG_ALL_APPLICATIONS & KEYMASTER_TAG_TYPE_MASK: + allApplications = true; + break; + case KM_TAG_APPLICATION_ID & KEYMASTER_TAG_TYPE_MASK: + applicationId = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_CREATION_DATETIME & KEYMASTER_TAG_TYPE_MASK: + creationDateTime = ASN1Utils.getDateFromAsn1(value); + break; + case KM_TAG_ORIGIN & KEYMASTER_TAG_TYPE_MASK: + origin = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_ROLLBACK_RESISTANT & KEYMASTER_TAG_TYPE_MASK: + rollbackResistant = true; + break; + case KM_TAG_ROOT_OF_TRUST & KEYMASTER_TAG_TYPE_MASK: + rootOfTrust = new RootOfTrust(value); + break; + case KM_TAG_OS_VERSION & KEYMASTER_TAG_TYPE_MASK: + osVersion = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_OS_PATCHLEVEL & KEYMASTER_TAG_TYPE_MASK: + osPatchLevel = ASN1Utils.getIntegerFromAsn1(value); + break; + /*case KM_TAG_ATTESTATION_APPLICATION_ID & KEYMASTER_TAG_TYPE_MASK: + attestationApplicationId = new AttestationApplicationId(ASN1Utils + .getAsn1EncodableFromBytes(ASN1Utils.getByteArrayFromAsn1(value))); + break;*/ + case KM_TAG_ATTESTATION_ID_BRAND & KEYMASTER_TAG_TYPE_MASK: + brand = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_DEVICE & KEYMASTER_TAG_TYPE_MASK: + device = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_PRODUCT & KEYMASTER_TAG_TYPE_MASK: + product = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_SERIAL & KEYMASTER_TAG_TYPE_MASK: + serialNumber = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_IMEI & KEYMASTER_TAG_TYPE_MASK: + imei = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_MEID & KEYMASTER_TAG_TYPE_MASK: + meid = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_MANUFACTURER & KEYMASTER_TAG_TYPE_MASK: + manufacturer = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_ATTESTATION_ID_MODEL & KEYMASTER_TAG_TYPE_MASK: + model = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + case KM_TAG_VENDOR_PATCHLEVEL & KEYMASTER_TAG_TYPE_MASK: + vendorPatchLevel = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_BOOT_PATCHLEVEL & KEYMASTER_TAG_TYPE_MASK: + bootPatchLevel = ASN1Utils.getIntegerFromAsn1(value); + break; + case KM_TAG_DEVICE_UNIQUE_ATTESTATION & KEYMASTER_TAG_TYPE_MASK: + deviceUniqueAttestation = true; + break; + case KM_TAG_IDENTITY_CREDENTIAL_KEY & KEYMASTER_TAG_TYPE_MASK: + identityCredentialKey = true; + break; + case KM_TAG_ATTESTATION_ID_SECOND_IMEI & KEYMASTER_TAG_TYPE_MASK: + secondImei = ASN1Utils.getStringFromAsn1OctetStreamAssumingUTF8(value); + break; + } + } + } + + public Set getPurposes() { + return purposes; + } + + public RootOfTrust getRootOfTrust() { + return rootOfTrust; + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/CertificateInfo.java b/services/core/java/com/android/server/matrixx/vbmeta/CertificateInfo.java new file mode 100644 index 000000000000..4403b28de0ba --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/CertificateInfo.java @@ -0,0 +1,244 @@ +package com.android.server.matrixx.vbmeta; + +import android.util.Base64; +import android.util.Log; + +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.cert.CertPath; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +public class CertificateInfo { + public static final int KEY_FAILED = -1; + public static final int KEY_UNKNOWN = 0; + public static final int KEY_AOSP = 1; + public static final int KEY_GOOGLE = 2; + public static final int KEY_KNOX = 3; + public static final int KEY_OEM = 4; + + public static final int CERT_UNKNOWN = 0; + public static final int CERT_SIGN = 1; + public static final int CERT_REVOKED = 2; + public static final int CERT_EXPIRED = 3; + public static final int CERT_NORMAL = 4; + + private static final String GOOGLE_ROOT_PUBLIC_KEY = "" + + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU" + + "FmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5j" + + "lRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y" + + "//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73X" + + "pXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYI" + + "mQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB" + + "+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7q" + + "uvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgp" + + "Zrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7" + + "gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82" + + "ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+" + + "NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ=="; + + private static final String AOSP_ROOT_EC_PUBLIC_KEY = "" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamgu" + + "D/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpA=="; + + private static final String AOSP_ROOT_RSA_PUBLIC_KEY = "" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMf" + + "d5tjkWP/96r/C3JHTsAsQ+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmg" + + "MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm" + + "+Vfkl5YLCazOkjWFmwIDAQAB"; + + private static final String KNOX_SAKV2_ROOT_PUBLIC_KEY = "" + + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA" + + "46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12" + + "4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo" + + "ZsapxB0gAOs0jSPRX5M="; + + private static final byte[] googleKey = Base64.decode(GOOGLE_ROOT_PUBLIC_KEY, 0); + private static final byte[] aospEcKey = Base64.decode(AOSP_ROOT_EC_PUBLIC_KEY, 0); + private static final byte[] aospRsaKey = Base64.decode(AOSP_ROOT_RSA_PUBLIC_KEY, 0); + private static final byte[] knoxSakv2Key = Base64.decode(KNOX_SAKV2_ROOT_PUBLIC_KEY, 0); + + private final X509Certificate cert; + private int issuer = KEY_UNKNOWN; + private int status = CERT_UNKNOWN; + private GeneralSecurityException securityException; + private Attestation attestation; + private CertificateParsingException certException; + + private Integer certsIssued; + + private CertificateInfo(X509Certificate cert) { + this.cert = cert; + } + + public X509Certificate getCert() { + return cert; + } + + public int getIssuer() { + return issuer; + } + + public int getStatus() { + return status; + } + + public GeneralSecurityException getSecurityException() { + return securityException; + } + + public Attestation getAttestation() { + return attestation; + } + + public CertificateParsingException getCertException() { + return certException; + } + + public Integer getCertsIssued() { + return certsIssued; + } + + private void checkIssuer() { + var publicKey = cert.getPublicKey().getEncoded(); + + if (Arrays.equals(publicKey, googleKey)) { + issuer = KEY_GOOGLE; + } else if (Arrays.equals(publicKey, aospEcKey)) { + issuer = KEY_AOSP; + } else if (Arrays.equals(publicKey, aospRsaKey)) { + issuer = KEY_AOSP; + } else if (Arrays.equals(publicKey, knoxSakv2Key)) { + issuer = KEY_KNOX; + } + } + + private void checkStatus(PublicKey parentKey) { + try { + status = CERT_SIGN; + cert.verify(parentKey); + + // Check serial code removed + + status = CERT_EXPIRED; + cert.checkValidity(); + status = CERT_NORMAL; + } catch (GeneralSecurityException e) { + Log.e("Attestation", "Error verifying certificate", e); + securityException = e; + } + } + + private boolean checkAttestation() { + boolean terminate; + try { + attestation = Attestation.loadFromCertificate(cert); + // If key purpose included KeyPurpose::SIGN, + // then it could be used to sign arbitrary data, including any tbsCertificate, + // and so an attestation produced by the key would have no security properties. + // If the parent certificate can attest that the key purpose is only KeyPurpose::ATTEST_KEY, + // then the child certificate can be trusted. + var purposes = attestation.getTeeEnforced().getPurposes(); + terminate = purposes == null || !purposes.contains(AuthorizationList.KM_PURPOSE_ATTEST_KEY); + } catch (CertificateParsingException e) { + // Log.e("Attestation", "Error parsing certificate", e); + certException = e; + terminate = false; + } + return terminate; + } + + public static AttestationResult parseCertificateChain(List certs) throws Exception { + var infoList = new ArrayList(); + + var parent = certs.get(certs.size() - 1); + for (int i = certs.size() - 1; i >= 0; i--) { + var parentKey = parent.getPublicKey(); + var info = new CertificateInfo(certs.get(i)); + infoList.add(info); + info.checkStatus(parentKey); + if (parent == info.cert) { + info.checkIssuer(); + } else { + parent = info.cert; + } + if (info.checkAttestation()) { + break; + } + } + + return AttestationResult.form(infoList); + } + + private static List sortCerts(List certs) { + if (certs.size() < 2) { + return certs; + } + + var issuer = certs.get(0).getIssuerX500Principal(); + boolean okay = true; + for (var cert : certs) { + var subject = cert.getSubjectX500Principal(); + if (issuer.equals(subject)) { + issuer = subject; + } else { + okay = false; + break; + } + } + if (okay) { + return certs; + } + + var newList = new ArrayList(certs.size()); + for (var cert : certs) { + boolean found = false; + var subject = cert.getSubjectX500Principal(); + for (var c : certs) { + if (c == cert) continue; + if (c.getIssuerX500Principal().equals(subject)) { + found = true; + break; + } + } + if (!found) { + newList.add(cert); + } + } + if (newList.size() != 1) { + return certs; + } + + var oldList = new LinkedList<>(certs); + oldList.remove(newList.get(0)); + for (int i = 0; i < newList.size(); i++) { + issuer = newList.get(i).getIssuerX500Principal(); + for (var it = oldList.iterator(); it.hasNext(); ) { + var cert = it.next(); + if (cert.getSubjectX500Principal().equals(issuer)) { + newList.add(cert); + it.remove(); + break; + } + } + } + if (!oldList.isEmpty()) { + return certs; + } + return newList; + } + + @SuppressWarnings("unchecked") + public static AttestationResult parseCertificateChain(CertPath certPath) throws Exception { + //noinspection unchecked + var certs = (List) certPath.getCertificates(); + if (certs.isEmpty()) { + throw new CertificateParsingException("No certificate found"); + } + return parseCertificateChain(sortCerts(certs)); + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/Entry.java b/services/core/java/com/android/server/matrixx/vbmeta/Entry.java new file mode 100644 index 000000000000..722f921645c1 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/Entry.java @@ -0,0 +1,88 @@ +package com.android.server.matrixx.vbmeta; + +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class Entry { + private static AttestationResult doAttestation() throws Exception { + String alias = "reveny"; + + Date now = new Date(); + int purposes = KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY; + + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(alias, purposes) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setKeyValidityStart(now) + .setAttestationChallenge(now.toString().getBytes()); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + keyPairGenerator.initialize(builder.build()); + keyPairGenerator.generateKeyPair(); + + Log.i("Attestation", "Generated key pair"); + + List certs = new ArrayList<>(); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + generateKey(alias); + Certificate[] certificates = keyStore.getCertificateChain(alias); + if (certificates == null) { + throw new Exception("Unable to get certificate chain"); + } + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (Certificate cert : certificates) { + certs.add(cf.generateCertificate(new ByteArrayInputStream(cert.getEncoded()))); + } + + return parseCertificateChain(certs); + } + + private static void generateKey(String alias) throws Exception { + Date now = new Date(); + int purposes = KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY; + + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(alias, purposes) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setKeyValidityStart(now) + .setAttestationChallenge(now.toString().getBytes()); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + try { + keyPairGenerator.initialize(builder.build()); + keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + Log.e("Attestation", "TEE is likely broken: ", ex.getCause()); + } + } + + private static AttestationResult parseCertificateChain(List certs) throws Exception { + List x509Certificates = new ArrayList<>(); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (Certificate cert : certs) { + byte[] encodedCert = cert.getEncoded(); + ByteArrayInputStream bais = new ByteArrayInputStream(encodedCert); + X509Certificate x509Certificate = (X509Certificate) cf.generateCertificate(bais); + x509Certificates.add(x509Certificate); + } + + Log.i("Attestation", "Parsing certificate chain: " + x509Certificates.size() + " certificates"); + + return CertificateInfo.parseCertificateChain(x509Certificates); + } +} diff --git a/services/core/java/com/android/server/matrixx/vbmeta/RootOfTrust.java b/services/core/java/com/android/server/matrixx/vbmeta/RootOfTrust.java new file mode 100644 index 000000000000..03a45033b333 --- /dev/null +++ b/services/core/java/com/android/server/matrixx/vbmeta/RootOfTrust.java @@ -0,0 +1,104 @@ +package com.android.server.matrixx.vbmeta; + +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; + +import java.security.cert.CertificateParsingException; + +public class RootOfTrust { + private static final int VERIFIED_BOOT_KEY_INDEX = 0; + private static final int DEVICE_LOCKED_INDEX = 1; + private static final int VERIFIED_BOOT_STATE_INDEX = 2; + private static final int VERIFIED_BOOT_HASH_INDEX = 3; + + public static final int KM_VERIFIED_BOOT_VERIFIED = 0; + public static final int KM_VERIFIED_BOOT_SELF_SIGNED = 1; + public static final int KM_VERIFIED_BOOT_UNVERIFIED = 2; + public static final int KM_VERIFIED_BOOT_FAILED = 3; + + private final byte[] verifiedBootKey; + private final boolean deviceLocked; + private final int verifiedBootState; + private final byte[] verifiedBootHash; + + public RootOfTrust(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof ASN1Sequence sequence)) { + throw new CertificateParsingException("Expected sequence for root of trust, found " + asn1Encodable.getClass().getName()); + } + + verifiedBootKey = ASN1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_KEY_INDEX)); + deviceLocked = ASN1Utils.getBooleanFromAsn1(sequence.getObjectAt(DEVICE_LOCKED_INDEX)); + verifiedBootState = ASN1Utils.getIntegerFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_STATE_INDEX)); + if (sequence.size() == 3) verifiedBootHash = null; + else verifiedBootHash = ASN1Utils.getByteArrayFromAsn1(sequence.getObjectAt(VERIFIED_BOOT_HASH_INDEX)); + } + + RootOfTrust(byte[] verifiedBootKey, boolean deviceLocked, int verifiedBootState, byte[] verifiedBootHash) { + this.verifiedBootKey = verifiedBootKey; + this.deviceLocked = deviceLocked; + this.verifiedBootState = verifiedBootState; + this.verifiedBootHash = verifiedBootHash; + } + + public static String verifiedBootStateToString(int verifiedBootState) { + switch (verifiedBootState) { + case KM_VERIFIED_BOOT_VERIFIED: + return "Verified"; + case KM_VERIFIED_BOOT_SELF_SIGNED: + return "Self-signed"; + case KM_VERIFIED_BOOT_UNVERIFIED: + return "Unverified"; + case KM_VERIFIED_BOOT_FAILED: + return "Failed"; + default: + return "Unknown (" + verifiedBootState + ")"; + } + } + + public byte[] getVerifiedBootKey() { + return verifiedBootKey; + } + + public boolean isDeviceLocked() { + return deviceLocked; + } + + public int getVerifiedBootState() { + return verifiedBootState; + } + + public byte[] getVerifiedBootHash() { + return verifiedBootHash; + } + + public static class Builder { + private byte[] verifiedBootKey; + private boolean deviceLocked = false; + private int verifiedBootState = -1; + private byte[] verifiedBootHash; + + public Builder setVerifiedBootKey(byte[] verifiedBootKey) { + this.verifiedBootKey = verifiedBootKey; + return this; + } + + public Builder setDeviceLocked(boolean deviceLocked) { + this.deviceLocked = deviceLocked; + return this; + } + + public Builder setVerifiedBootState(int verifiedBootState) { + this.verifiedBootState = verifiedBootState; + return this; + } + + public Builder setVerifiedBootHash(byte[] verifiedBootHash) { + this.verifiedBootHash = verifiedBootHash; + return this; + } + + public RootOfTrust build() { + return new RootOfTrust(verifiedBootKey, deviceLocked, verifiedBootState, verifiedBootHash); + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 86593f8ac38f..de0777153d2d 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -169,6 +169,7 @@ import com.android.server.coverage.CoverageService; import com.android.server.cpu.CpuMonitorService; import com.android.server.crashrecovery.CrashRecoveryAdaptor; +import com.android.server.matrixx.VbmetaHashService; import com.android.server.credentials.CredentialManagerService; import com.android.server.criticalevents.CriticalEventLog; import com.android.server.devicepolicy.DevicePolicyManagerService; @@ -2919,6 +2920,11 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(HbmService.class); } + // VbmetaHashService + t.traceBegin("VbmetaHashService"); + mSystemServiceManager.startService(VbmetaHashService.class); + t.traceEnd(); + } t.traceBegin("StartMediaProjectionManager"); From d3f696761571837b7103cae63c3a8f30cee68b86 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 31 Oct 2025 14:53:04 +0530 Subject: [PATCH 1075/1315] Attestation: Update logging for keymint 4.0 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/matrixx/vbmeta/Attestation.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java b/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java index d1676b99ed88..8504d7e2eefa 100644 --- a/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java +++ b/services/core/java/com/android/server/matrixx/vbmeta/Attestation.java @@ -1,7 +1,6 @@ package com.android.server.matrixx.vbmeta; import android.os.Build; -import android.util.Log; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -14,7 +13,6 @@ public abstract class Attestation { static final String ASN1_OID = "1.3.6.1.4.1.11129.2.1.17"; static final String KNOX_OID = "1.3.6.1.4.1.236.11.3.23.7"; static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension. - static final String CRL_DP_OID = "2.5.29.31"; // Standard CRL Distribution Points extension. public static final int KM_SECURITY_LEVEL_SOFTWARE = 0; public static final int KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1; @@ -34,10 +32,6 @@ public static Attestation loadFromCertificate(X509Certificate x509Cert) throws C throw new CertificateParsingException("No attestation extensions found"); } - if (x509Cert.getExtensionValue(CRL_DP_OID) != null) { - Log.w("Attestation", "CRL Distribution Points extension found in leaf certificate."); - } - return new ASN1Attestation(x509Cert); } @@ -74,6 +68,8 @@ public static String attestationVersionToString(int version) { return "KeyMint 2.0"; case 300: return "KeyMint 3.0"; + case 400: + return "KeyMint 4.0"; default: return "Unknown (" + version + ")"; } @@ -99,6 +95,8 @@ public static String keymasterVersionToString(int version) { return "KeyMint 2.0"; case 300: return "KeyMint 3.0"; + case 400: + return "KeyMint 4.0"; default: return "Unknown (" + version + ")"; } From 7a22ba3ad4ce837f5ac5ae4b51c81e31d1334753 Mon Sep 17 00:00:00 2001 From: kenway214 Date: Thu, 12 Mar 2026 13:10:35 +0000 Subject: [PATCH 1076/1315] core: Introduce per-apps spoofing [1/2] - Device models taken from PixelPropsUtils Signed-off-by: kenway214 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/Instrumentation.java | 3 + core/java/android/provider/Settings.java | 26 ++ .../util/matrixx/PerAppsPropsUtils.java | 375 ++++++++++++++++++ .../util/matrixx/PixelPropsUtils.java | 38 +- 4 files changed, 405 insertions(+), 37 deletions(-) create mode 100644 core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 7c46f3bca095..1eb5105cebcf 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -82,6 +82,7 @@ import com.android.internal.util.matrixx.AttestationHooks; import com.android.internal.util.matrixx.PixelPropsUtils; +import com.android.internal.util.matrixx.PerAppsPropsUtils; /** * Base class for implementing application instrumentation code. When running @@ -1364,6 +1365,7 @@ public Application newApplication(ClassLoader cl, String className, Context cont app.attach(context); AttestationHooks.setProps(context); PixelPropsUtils.setProps(context); + PerAppsPropsUtils.setProps(context); return app; } @@ -1384,6 +1386,7 @@ static public Application newApplication(Class clazz, Context context) app.attach(context); AttestationHooks.setProps(context); PixelPropsUtils.setProps(context); + PerAppsPropsUtils.setProps(context); return app; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fdcafcb1cd4c..ad5a35d5a7d5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14461,6 +14461,32 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NAVBAR_IME_SPACE = "navbar_ime_space"; + /** + * Per-apps device spoofing + * @hide + */ + @Readable + public static final String PER_APPS_DEVICE_SPOOF = "per_apps_device_spoof"; + + /** + * Custom device spoof profiles for per-app spoofing + * @hide + */ + @Readable + public static final String CUSTOM_SPOOF_PROFILES = "custom_spoof_profiles"; + + /** + * @hide + */ + @Readable + public static final String PER_APPS_DEVICE_SPOOF_ENABLED = "per_apps_device_spoof_enabled"; + + /** + * @hide + */ + @Readable + public static final String PER_APPS_DEVICE_SPOOF_CACHE = "per_apps_device_spoof_cache"; + /** * Whether to use system accent color for lock screen clock text * @hide diff --git a/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java b/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java new file mode 100644 index 000000000000..e31e2b6cca5e --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java @@ -0,0 +1,375 @@ +/* + * SPDX-FileCopyrightText: 2026 kenway214 + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.util.matrixx; + +import android.content.Context; +import android.content.ContentResolver; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + * Per-apps device spoofing + */ +public final class PerAppsPropsUtils { + + private static final Map> propsToChange = new HashMap<>(); + + static { + propsToChange.put("BS4C", createBS4CProps()); + propsToChange.put("F5", createF5Props()); + propsToChange.put("GZF5", createGZF5Props()); + propsToChange.put("HMV2R", createHMV2RProps()); + propsToChange.put("LY700", createLY700Props()); + propsToChange.put("LY70023", createLY70023Props()); + propsToChange.put("MI11TP", createMI11TPProps()); + propsToChange.put("MI13", createMI13Props()); + propsToChange.put("MI13P", createMI13PProps()); + propsToChange.put("MI14P", createMI14PProps()); + propsToChange.put("OP12", createOP12Props()); + propsToChange.put("OP13", createOP13Props()); + propsToChange.put("OP8P5G", createOP8P5GProps()); + propsToChange.put("PXL", createPXLProps()); + propsToChange.put("PXL10PXL", createPXL10PXLProps()); + propsToChange.put("RM9P", createRM9PProps()); + propsToChange.put("RM10P", createRM10PProps()); + propsToChange.put("RM15P5G", createRM15P5GProps()); + propsToChange.put("RMX14", createRMX14Props()); + propsToChange.put("RMP35G", createRMP35GProps()); + propsToChange.put("ROG6DU", createROG6DUProps()); + propsToChange.put("ROG8P", createROG8PProps()); + propsToChange.put("ROG9P", createROG9PProps()); + propsToChange.put("S25U", createS25UProps()); + } + + private static Map createBS4CProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Black Shark"); + props.put("DEVICE", "Black Shark 4 (China)"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("MODEL", "2SM-X706B"); + props.put("FINGERPRINT", "BlackShark/PRS-H0/Black Shark 4:13/TQ3A.230805.001/20230315:user/release-keys"); + props.put("PRODUCT", "2SM-X706B"); + return props; + } + + private static Map createF5Props() { + Map props = new HashMap<>(); + props.put("BRAND", "Xiaomi"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("DEVICE", "marble"); + props.put("MODEL", "23049PCD8G"); + props.put("FINGERPRINT", "Xiaomi/marble_global/marble:14/UKQ1.230917.001/V816.0.2.0.UMRMIXM:user/release-keys"); + props.put("PRODUCT", "marble"); + return props; + } + + private static Map createGZF5Props() { + Map props = new HashMap<>(); + props.put("BRAND", "samsung"); + props.put("DEVICE", "Galaxy Z Fold 5"); + props.put("MANUFACTURER", "samsung"); + props.put("MODEL", "SM-F9460"); + props.put("FINGERPRINT", "samsung/q2qzh/q2q:15/UP1A.231005.007/F946BXXU1BWK4:user/release-keys"); + props.put("PRODUCT", "SM-F9460"); + return props; + } + + private static Map createHMV2RProps() { + Map props = new HashMap<>(); + props.put("BRAND", "HONOR"); + props.put("DEVICE", "Honor Magic V2 RSR"); + props.put("MANUFACTURER", "HONOR"); + props.put("MODEL", "VER-N49DP"); + props.put("FINGERPRINT", "HONOR/VER-N49DP/VER:13/ENG.20240918.123456:user/release-keys"); + props.put("PRODUCT", "VER-N49DP"); + return props; + } + + private static Map createLY700Props() { + Map props = new HashMap<>(); + props.put("BRAND", "Lenovo"); + props.put("DEVICE", "Lenovo Y700"); + props.put("MANUFACTURER", "Lenovo"); + props.put("MODEL", "Lenovo TB-9707F"); + return props; + } + + private static Map createLY70023Props() { + Map props = new HashMap<>(); + props.put("BRAND", "Lenovo"); + props.put("DEVICE", "Legion Y700 (2023)"); + props.put("MANUFACTURER", "Lenovo"); + props.put("MODEL", "TB-9707F"); + props.put("FINGERPRINT", "Lenovo/TB-9707F/Lenovo TB-9707F:13/TQ3A.230805.001/20230901:user/release-keys"); + props.put("PRODUCT", "TB-9707F"); + return props; + } + + private static Map createMI11TPProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Xiaomi"); + props.put("DEVICE", "Xiaomi 11T Pro"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("MODEL", "2107113SG"); + props.put("FINGERPRINT", "Xiaomi/2107113SI/Mi 11T Pro:13/RKQ1.211001.001/20230410:user/release-keys"); + props.put("PRODUCT", "2107113SG"); + return props; + } + + private static Map createMI13Props() { + Map props = new HashMap<>(); + props.put("BRAND", "Xiaomi"); + props.put("DEVICE", "Xiaomi 13"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("MODEL", "2211133G"); + props.put("FINGERPRINT", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys"); + props.put("PRODUCT", "2211133G"); + return props; + } + + private static Map createMI13PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Xiaomi"); + props.put("DEVICE", "Xiaomi 13 Pro"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("MODEL", "2210132G"); + props.put("FINGERPRINT", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys"); + props.put("PRODUCT", "2210132G"); + return props; + } + + private static Map createMI14PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Xiaomi"); + props.put("MANUFACTURER", "Xiaomi"); + props.put("DEVICE", "houji"); + props.put("MODEL", "23116PN5BC"); + props.put("FINGERPRINT", "Xiaomi/houji/houji:14/UKQ1.230917.001/V816.0.2.0.UNBCNXM:user/release-keys"); + props.put("PRODUCT", "houji"); + return props; + } + + private static Map createOP12Props() { + Map props = new HashMap<>(); + props.put("BRAND", "OnePlus"); + props.put("MANUFACTURER", "OnePlus"); + props.put("DEVICE", "OP594DL1"); + props.put("MODEL", "CPH2581"); + props.put("FINGERPRINT", "OnePlus/OP594DL1/OP594DL1:14/UKQ1.230917.001/1702951307528:user/release-keys"); + props.put("PRODUCT", "OP594DL1"); + return props; + } + + private static Map createOP13Props() { + Map props = new HashMap<>(); + props.put("BRAND", "OnePlus"); + props.put("MANUFACTURER", "OnePlus"); + props.put("DEVICE", "OnePlus 13"); + props.put("MODEL", "PJZ110"); + props.put("FINGERPRINT", "OnePlus/PJZ110/OP5D0DL1:15/AP3A.240617.008/V.1bd19a1-1-2:user/release-keys"); + props.put("PRODUCT", "PJZ110"); + return props; + } + + private static Map createOP8P5GProps() { + Map props = new HashMap<>(); + props.put("BRAND", "OnePlus"); + props.put("DEVICE", "OnePlus 8 Pro 5G"); + props.put("MANUFACTURER", "OnePlus"); + props.put("MODEL", "IN2023"); + props.put("FINGERPRINT", "OnePlus/IN2023/OnePlus8Pro:13/RKQ1.211119.001/20230501:user/release-keys"); + props.put("PRODUCT", "IN2023"); + return props; + } + + private static Map createPXLProps() { + Map props = new HashMap<>(); + props.put("BRAND", "google"); + props.put("DEVICE", "Pixel XL"); + props.put("MANUFACTURER", "Google"); + props.put("MODEL", "marlin"); + props.put("FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys"); + props.put("PRODUCT", "marlin"); + return props; + } + + private static Map createPXL10PXLProps() { + Map props = new HashMap<>(); + props.put("BRAND", "google"); + props.put("MANUFACTURER", "Google"); + props.put("DEVICE", "mustang"); + props.put("MODEL", "Pixel 10 Pro XL"); + props.put("FINGERPRINT", "google/mustang/mustang:16/CP1A.260305.018/14887507:user/release-keys"); + props.put("PRODUCT", "mustang"); + return props; + } + + private static Map createRM9PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "nubia"); + props.put("DEVICE", "REDMAGIC 9 Pro"); + props.put("MANUFACTURER", "ZTE"); + props.put("MODEL", "NX769J"); + props.put("FINGERPRINT", "nubia/NX769J/NX769J:14/UKQ1.230917.001/20240813.173312:user/release-keys"); + props.put("PRODUCT", "NX769J"); + return props; + } + + private static Map createRM10PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "nubia"); + props.put("DEVICE", "RedMagic 10 Pro"); + props.put("MANUFACTURER", "ZTE"); + props.put("MODEL", "NX789J"); + props.put("FINGERPRINT", "nubia/NX789J-UN/NX789J:15/AQ3A.240812.002/20241212.194919:user/release-keys"); + props.put("PRODUCT", "NX789J"); + return props; + } + + private static Map createRM15P5GProps() { + Map props = new HashMap<>(); + props.put("BRAND", "realme"); + props.put("DEVICE", "Realme 15 Pro 5G"); + props.put("MANUFACTURER", "realme"); + props.put("MODEL", "RMX5101"); + props.put("FINGERPRINT", "realme/RMX5101IN/RE60B4L1:15/AP3A.240617.008/V.R4T2.26cec0e-80bb4e-80b757:user/release-keys"); + props.put("PRODUCT", "RMX5101"); + return props; + } + + private static Map createRMX14Props() { + Map props = new HashMap<>(); + props.put("BRAND", "realme"); + props.put("DEVICE", "Realme 14"); + props.put("MANUFACTURER", "realme"); + props.put("MODEL", "RMX5070"); + return props; + } + + private static Map createRMP35GProps() { + Map props = new HashMap<>(); + props.put("BRAND", "realme"); + props.put("DEVICE", "Realme P3 5G"); + props.put("MANUFACTURER", "realme"); + props.put("MODEL", "RMX5070"); + props.put("FINGERPRINT", "realme/RMX5070/RMX5070:15/SKQ1.230119.001/eng.user.20250415.155201:user/release-keys"); + props.put("PRODUCT", "RMX5070"); + return props; + } + + private static Map createROG6DUProps() { + Map props = new HashMap<>(); + props.put("BRAND", "ASUS"); + props.put("DEVICE", "ROG Phone 6D Ultimate"); + props.put("MANUFACTURER", "ASUS"); + props.put("MODEL", "AI2203"); + props.put("FINGERPRINT", "ASUS/AI2203/ROG Phone 6D:14/UP1A.231005.007/20240315:user/release-keys"); + props.put("PRODUCT", "AI2203"); + return props; + } + + private static Map createROG8PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "asus"); + props.put("MANUFACTURER", "asus"); + props.put("DEVICE", "ASUS_AI2401_D"); + props.put("MODEL", "ASUS_AI2401_D"); + props.put("FINGERPRINT", "asus/ASUS_AI2401_D/ASUS_AI2401:14/UKQ1.230804.001/34.0210.0210.222-0:user/release-keys"); + props.put("PRODUCT", "ASUS_AI2401_D"); + return props; + } + + private static Map createROG9PProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Asus"); + props.put("DEVICE", "ROG Phone 9 PRO"); + props.put("MANUFACTURER", "Asus"); + props.put("MODEL", "ASUS_AI2501"); + return props; + } + + private static Map createS25UProps() { + Map props = new HashMap<>(); + props.put("BRAND", "Samsung"); + props.put("DEVICE", "Samsung S25 Ultra"); + props.put("MANUFACTURER", "samsung"); + props.put("MODEL", "SM-S938B"); + return props; + } + + public static void setProps(Context context) { + final String packageName = context.getPackageName(); + + if (TextUtils.isEmpty(packageName)) { + return; + } + + final String spoofedApps; + try { + final ContentResolver contentResolver = context.getContentResolver(); + spoofedApps = Settings.Secure.getString(contentResolver, Settings.Secure.PER_APPS_DEVICE_SPOOF); + } catch (Exception e) { + PixelPropsUtils.dlog("PerAppsPropsUtils: Failed to read spoofed apps setting: " + e.getMessage()); + return; + } + + if (TextUtils.isEmpty(spoofedApps)) { + return; + } + + Map> allProps = new HashMap<>(propsToChange); + try { + final ContentResolver contentResolver = context.getContentResolver(); + String customProfilesJson = Settings.Secure.getString(contentResolver, Settings.Secure.CUSTOM_SPOOF_PROFILES); + if (!TextUtils.isEmpty(customProfilesJson)) { + org.json.JSONArray jsonArray = new org.json.JSONArray(customProfilesJson); + for (int i = 0; i < jsonArray.length(); i++) { + org.json.JSONObject obj = jsonArray.getJSONObject(i); + String id = obj.getString("id"); + Map props = new HashMap<>(); + props.put("BRAND", obj.optString("brand", "")); + props.put("MANUFACTURER", obj.optString("manufacturer", "")); + props.put("DEVICE", obj.optString("device", "")); + props.put("MODEL", obj.optString("model", "")); + String fp = obj.optString("fingerprint", ""); + if (!TextUtils.isEmpty(fp)) props.put("FINGERPRINT", fp); + String prod = obj.optString("product", ""); + if (!TextUtils.isEmpty(prod)) props.put("PRODUCT", prod); + allProps.put(id, props); + } + } + } catch (Exception e) { + PixelPropsUtils.dlog("PerAppsPropsUtils: Failed to parse custom profiles: " + e.getMessage()); + } + + String[] apps = spoofedApps.split(","); + for (String app : apps) { + String[] values = app.split(":"); + if (values.length != 2) { + continue; + } + String pkg = values[0]; + String device = values[1]; + if (pkg.equals(packageName)) { + if (allProps.containsKey(device)) { + PixelPropsUtils.dlog("PerAppsPropsUtils: Applying profile for: " + packageName + " as " + device); + Map props = allProps.get(device); + for (Map.Entry prop : props.entrySet()) { + PixelPropsUtils.setPropValue(prop.getKey(), prop.getValue()); + } + } + break; + } + } + } +} diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index eb4ed7fa3590..5abd86314d48 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -70,7 +70,6 @@ public final class PixelPropsUtils { private static final String PROP_HOOKS = "persist.sys.pihooks_"; private static final String SPOOF_PP = "persist.sys.pp"; - private static final String ENABLE_GAME_PROP_OPTIONS = "persist.sys.gameprops.enabled"; public static final String SPOOF_GMS = "persist.sys.pp.gms"; private static final String SPOOF_VENDING = "persist.sys.pp.vending"; @@ -282,7 +281,6 @@ public static void setProps(Context context) { boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); boolean isPixelVendingEnabled = SystemProperties.getBoolean(SPOOF_VENDING, true) && isPixelGmsEnabled; propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); - setGameProps(packageName); if (android.os.Process.isIsolated()) { if (DEBUG) Log.d(TAG, "Skipping setProps in isolated process"); @@ -349,40 +347,6 @@ public static void setProps(Context context) { } } - private static Map getGameProps(String packageName) { - Map gamePropsToChange = new HashMap<>(); - String[] keys = {"BRAND", "DEVICE", "MANUFACTURER", "MODEL", "FINGERPRINT", "PRODUCT"}; - for (String key : keys) { - String systemPropertyKey = "persist.sys.gameprops." + packageName + "." + key; - String value = SystemProperties.get(systemPropertyKey); - if (value != null && !value.isEmpty()) { - gamePropsToChange.put(key, value); - if (DEBUG) Log.d(TAG, "Got system property: " + systemPropertyKey + " = " + value); - } - } - return gamePropsToChange; - } - - public static void setGameProps(String packageName) { - if (!SystemProperties.getBoolean(ENABLE_GAME_PROP_OPTIONS, false)) { - return; - } - if (packageName == null || packageName.isEmpty()) { - return; - } - Map gamePropsToChange = getGameProps(packageName); - if (!gamePropsToChange.isEmpty()) { - if (DEBUG) Log.d(TAG, "Defining props for: " + packageName); - for (Map.Entry prop : gamePropsToChange.entrySet()) { - String key = prop.getKey(); - String value = prop.getValue(); - if (DEBUG) Log.d(TAG, "Defining " + key + " prop for: " + packageName); - setPropValue(key, value); - } - return; - } - } - private static boolean isDeviceTablet(Context context) { if (context == null) { return false; @@ -392,7 +356,7 @@ private static boolean isDeviceTablet(Context context) { return isTablet; } - private static void setPropValue(String key, Object value) { + public static void setPropValue(String key, Object value) { try { Field field = getBuildClassField(key); if (field != null) { From 0f6a26e7777e653f78ba1c366151f9f871a09a41 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:29:41 +0800 Subject: [PATCH 1077/1315] base: Adding apps filter hooks Change-Id: I7beba5dda3d3e19e02a22b327591fef36dd7806e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/content/res/AssetManager.java | 4 +- .../com/android/server/pm/AppsFilterBase.java | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index eb2a2e547b48..a5075900c6ec 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -84,7 +84,7 @@ public final class AssetManager implements AutoCloseable { public static final String FRAMEWORK_APK_PATH = getFrameworkApkPath(); private static final String FRAMEWORK_APK_PATH_DEVICE = "/system/framework/framework-res.apk"; private static final String FRAMEWORK_APK_PATH_RAVENWOOD = "ravenwood-data/framework-res.apk"; - private static final String LINEAGE_APK_PATH = "/system/framework/org.lineageos.platform-res.apk"; + private static final String PLATFORM_EXT_APK_PATH = "/system/framework/org.lineageos.platform-res.apk"; private static final Object sSync = new Object(); @@ -292,7 +292,7 @@ public static void createSystemAssetsInZygoteLocked(boolean reinitialize, for (String idmapPath : systemIdmapPaths) { apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, ApkAssets.PROPERTY_SYSTEM)); } - apkAssets.add(ApkAssets.loadFromPath(LINEAGE_APK_PATH, ApkAssets.PROPERTY_SYSTEM)); + apkAssets.add(ApkAssets.loadFromPath(PLATFORM_EXT_APK_PATH, ApkAssets.PROPERTY_SYSTEM)); sSystemApkAssetsSet = new ArraySet<>(apkAssets); sSystemApkAssets = apkAssets.toArray(new ApkAssets[0]); diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index 7469b6bc2c03..e854d095c5b4 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -23,6 +23,7 @@ import static com.android.server.pm.AppsFilterUtils.requestsQueryAllPackages; import android.annotation.NonNull; +import java.util.Set; import android.annotation.Nullable; import android.content.pm.SigningDetails; import android.os.Binder; @@ -336,6 +337,59 @@ private static boolean isQueryableBySdkSandbox(int callingUid, int targetUid) { * {@link AppsFilterSnapshot#shouldFilterApplication(PackageDataSnapshot, int, Object, * PackageStateInternal, int)} */ + private static final java.util.Set ROOT_PACKAGES = java.util.Set.of( + "com.topjohnwu.magisk", + "eu.chainfire.supersu", + "com.koushikdutta.superuser", + "com.noshufou.android.su", + "com.noshufou.android.su.elite", + "com.thirdparty.superuser", + "com.yellowes.su", + "me.weishu.kernelsu", + "com.kingroot.kinguser", + "com.kingo.root", + "com.smedialink.oneclickroot", + "com.zhiqupk.root.global", + "com.alephzain.framaroot", + "com.devadvance.rootcloak", + "com.devadvance.rootcloakplus", + "de.robv.android.xposed.installer", + "com.saurik.substrate", + "com.amphoras.hidemyroot", + "com.amphoras.hidemyrootadfree", + "com.formyhm.hiderootPremium", + "com.formyhm.hideroot", + "com.koushikdutta.rommanager", + "com.koushikdutta.rommanager.license", + "com.dimonvideo.luckypatcher", + "com.chelpus.lackypatch", + "com.chelpus.luckypatcher", + "com.solohsu.android.edxp.manager", + "org.meowcat.edxposed.manager", + "org.lsposed.manager", + "cc.madkite.freedom", + "com.ramdroid.appquarantine", + "com.ramdroid.appquarantinepro", + "com.zachspong.temprootremovejb", + "org.lineageos.lineageparts", + "org.lineageos.settings", + "org.lineageos.setupwizard", + "org.lineageos.updater" + ); + + private static boolean isRomPackage(String pkg) { + return pkg.startsWith("org.lineageos.") + || pkg.startsWith("org.omnirom.") + || pkg.startsWith("org.protonaosp."); + } + + private static boolean isCallerSystemApp(Object callingSetting) { + if (callingSetting instanceof PackageStateInternal) { + return ((PackageStateInternal) callingSetting).isSystem(); + } + return true; + } + @Override public boolean shouldFilterApplication(PackageDataSnapshot snapshot, int callingUid, @Nullable Object callingSetting, PackageStateInternal targetPkgSetting, int userId) { @@ -344,6 +398,12 @@ public boolean shouldFilterApplication(PackageDataSnapshot snapshot, int calling } try { int callingAppId = UserHandle.getAppId(callingUid); + String targetPkg = targetPkgSetting.getPackageName(); + if (callingAppId >= Process.FIRST_APPLICATION_UID + && !isCallerSystemApp(callingSetting) + && (ROOT_PACKAGES.contains(targetPkg) || isRomPackage(targetPkg))) { + return true; + } if (callingAppId < Process.FIRST_APPLICATION_UID || targetPkgSetting.getAppId() < Process.FIRST_APPLICATION_UID || callingAppId == targetPkgSetting.getAppId()) { From 35c41f9a0e4511fc1401f1f03d33e6554714f5b8 Mon Sep 17 00:00:00 2001 From: Joey Date: Mon, 19 Jan 2026 11:57:15 +0900 Subject: [PATCH 1078/1315] ApplicationPackageManager: Drop tensor spoof prop Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/app/ApplicationPackageManager.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 030286c08b8f..eac1d2665473 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -938,20 +938,6 @@ public boolean hasSystemFeature(String name, int version) { if (Arrays.asList(featuresTensor).contains(name)) return false; if (Arrays.asList(featuresNexus).contains(name)) return true; } - boolean enableTensorFeaturesOnNonTensor = SystemProperties.getBoolean("persist.sys.pp.tensor", false); - boolean isTensorDevice = SystemProperties.get("ro.product.model").matches("Pixel (6|7|8|9|10)[a-zA-Z ]*"); - if (packageName != null && packageName.equals("com.google.android.as")) { - if (isTensorDevice && Arrays.asList(featuresTensor).contains(name)) { - return true; - } - if (!isTensorDevice && enableTensorFeaturesOnNonTensor && Arrays.asList(featuresTensor).contains(name)) { - return true; - } - } - if (name != null && Arrays.asList(featuresTensor).contains(name) - && !isTensorDevice) { - return enableTensorFeaturesOnNonTensor; - } if (Arrays.asList(featuresNexus).contains(name)) return true; if (Arrays.asList(featuresPixel).contains(name)) return true; if (Arrays.asList(featuresPixelOthers).contains(name)) return true; From dcd4ede36a540ba02cfb46747a63a3a04722564b Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 2 Apr 2026 19:58:30 +0900 Subject: [PATCH 1079/1315] System: Switch Tensor feature handling to user-controlled override Introduce a toggle to optionally enable Tensor-exclusive features on non-Tensor devices. Add persist.sys.pp.tensor to allow users to force-enable features defined in FEATURES_TENSOR on unsupported devices. When disabled, the system falls back to default feature detection. Tensor devices are not affected by this toggle and continue to rely on their native feature set. This follows a soft override approach, ensuring stock behavior is preserved unless explicitly enabled by the user, while avoiding issues previously caused by exposing these features globally. Also use features impl from https://github.com/crdroidandroid/android_frameworks_base/commit/4af6e7d712d2845f5927244ed5e880d2abe1a673 Co-authored-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../app/ApplicationPackageManager.java | 206 +++++++++++------- .../com/android/server/am/ActiveServices.java | 1 + 2 files changed, 124 insertions(+), 83 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index eac1d2665473..2d6f3a0850f6 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -838,67 +838,132 @@ public Boolean recompute(HasSystemFeatureQuery query) { } }; - private static final String[] featuresPixel = { - "com.google.android.apps.photos.PIXEL_2019_PRELOAD", - "com.google.android.apps.photos.PIXEL_2019_MIDYEAR_PRELOAD", - "com.google.android.apps.photos.PIXEL_2018_PRELOAD", - "com.google.android.apps.photos.PIXEL_2017_PRELOAD", - "com.google.android.feature.PIXEL_2021_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2020_EXPERIENCE", - "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2019_EXPERIENCE", - "com.google.android.feature.PIXEL_2019_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2018_EXPERIENCE", - "com.google.android.feature.PIXEL_2017_EXPERIENCE", - "com.google.android.feature.PIXEL_EXPERIENCE", - "com.google.android.feature.GOOGLE_BUILD", - "com.google.android.feature.GOOGLE_EXPERIENCE" - }; - - private static final String[] featuresPixelOthers = { - "com.google.android.feature.ASI", - "com.google.android.feature.ANDROID_ONE_EXPERIENCE", - "com.google.android.feature.GOOGLE_FI_BUNDLED", - "com.google.android.feature.LILY_EXPERIENCE", - "com.google.android.feature.TURBO_PRELOAD", - "com.google.android.feature.WELLBEING", - "com.google.lens.feature.IMAGE_INTEGRATION", - "com.google.lens.feature.CAMERA_INTEGRATION", - "com.google.photos.trust_debug_certs", - "com.google.android.feature.AER_OPTIMIZED", - "com.google.android.feature.NEXT_GENERATION_ASSISTANT", - "android.software.game_service", - "com.google.android.feature.EXCHANGE_6_2", - "com.google.android.apps.dialer.call_recording_audio", - "com.google.android.apps.dialer.SUPPORTED", - "com.google.android.feature.CONTEXTUAL_SEARCH", - "com.google.android.feature.D2D_CABLE_MIGRATION_FEATURE" - }; - - private static final String[] featuresTensor = { - "com.google.android.feature.PIXEL_2026_EXPERIENCE", - "com.google.android.feature.PIXEL_2026_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2025_EXPERIENCE", - "com.google.android.feature.PIXEL_2025_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2024_EXPERIENCE", - "com.google.android.feature.PIXEL_2024_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2023_EXPERIENCE", - "com.google.android.feature.PIXEL_2023_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2022_EXPERIENCE", - "com.google.android.feature.PIXEL_2022_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2021_EXPERIENCE" - }; - - private static final String[] featuresNexus = { - "com.google.android.apps.photos.NEXUS_PRELOAD", - "com.google.android.apps.photos.nexus_preload", - "com.google.android.feature.PIXEL_EXPERIENCE", - "com.google.android.feature.GOOGLE_BUILD", - "com.google.android.feature.GOOGLE_EXPERIENCE" - }; + private static final ArraySet PRIV_PKGS = new ArraySet<>(); + private static final ArraySet FEATURES_PIXEL = new ArraySet<>(); + private static final ArraySet FEATURES_PIXEL_OTHERS = new ArraySet<>(); + private static final ArraySet FEATURES_TENSOR = new ArraySet<>(); + private static final ArraySet FEATURES_NEXUS = new ArraySet<>(); + private static final ArraySet TENSOR_CODENAMES = new ArraySet<>(); + private static final boolean IS_TENSOR_DEVICE; + + static { + Collections.addAll(FEATURES_PIXEL, + "com.google.android.apps.photos.PIXEL_2019_PRELOAD", + "com.google.android.apps.photos.PIXEL_2019_MIDYEAR_PRELOAD", + "com.google.android.apps.photos.PIXEL_2018_PRELOAD", + "com.google.android.apps.photos.PIXEL_2017_PRELOAD", + "com.google.android.feature.PIXEL_2021_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2019_EXPERIENCE", + "com.google.android.feature.PIXEL_2019_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2018_EXPERIENCE", + "com.google.android.feature.PIXEL_2017_EXPERIENCE", + "com.google.android.feature.PIXEL_EXPERIENCE", + "com.google.android.feature.GOOGLE_BUILD", + "com.google.android.feature.GOOGLE_EXPERIENCE" + ); + + Collections.addAll(FEATURES_PIXEL_OTHERS, + "com.google.android.feature.ASI", + "com.google.android.feature.ANDROID_ONE_EXPERIENCE", + "com.google.android.feature.GOOGLE_FI_BUNDLED", + "com.google.android.feature.LILY_EXPERIENCE", + "com.google.android.feature.TURBO_PRELOAD", + "com.google.android.feature.WELLBEING", + "com.google.lens.feature.IMAGE_INTEGRATION", + "com.google.lens.feature.CAMERA_INTEGRATION", + "com.google.photos.trust_debug_certs", + "com.google.android.feature.AER_OPTIMIZED", + "com.google.android.feature.NEXT_GENERATION_ASSISTANT", + "android.software.game_service", + "com.google.android.feature.EXCHANGE_6_2", + "com.google.android.apps.dialer.call_recording_audio", + "com.google.android.apps.dialer.SUPPORTED" + ); + + Collections.addAll(FEATURES_TENSOR, + "com.google.android.feature.PIXEL_2026_EXPERIENCE", + "com.google.android.feature.PIXEL_2026_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2022_EXPERIENCE", + "com.google.android.feature.PIXEL_2022_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2021_EXPERIENCE" + ); + + Collections.addAll(FEATURES_NEXUS, + "com.google.android.apps.photos.NEXUS_PRELOAD", + "com.google.android.apps.photos.nexus_preload", + "com.google.android.feature.PIXEL_EXPERIENCE", + "com.google.android.feature.GOOGLE_BUILD", + "com.google.android.feature.GOOGLE_EXPERIENCE" + ); + + Collections.addAll(TENSOR_CODENAMES, + "stallion","blazer","frankel","mustang","tegu","comet","komodo","caiman","tokay", + "akita","husky","shiba","felix","tangorpro","lynx","cheetah","panther", + "bluejay","oriole","raven" + ); + + Collections.addAll(PRIV_PKGS, + "com.google.android.googlequicksearchbox", + "com.google.android.apps.photos", + "com.google.android.apps.pixel.agent", + "com.google.android.apps.pixel.creativeassistant" + ); + + final String device = SystemProperties.get("ro.evolution.device"); + IS_TENSOR_DEVICE = TENSOR_CODENAMES.contains(device); + } @Override public boolean hasSystemFeature(String name, int version) { + final String pkg = ActivityThread.currentPackageName(); + + if (name != null && pkg != null && PRIV_PKGS.contains(pkg)) { + final boolean photosSpoof = "com.google.android.apps.photos".equals(pkg) + && SystemProperties.getBoolean("persist.sys.pp.photos", true); + if (photosSpoof) { + if (FEATURES_PIXEL.contains(name)) return false; + if (FEATURES_PIXEL_OTHERS.contains(name)) return true; + if (FEATURES_TENSOR.contains(name)) return false; + if (FEATURES_NEXUS.contains(name)) return true; + } else { + if (FEATURES_PIXEL.contains(name)) return true; + if (FEATURES_PIXEL_OTHERS.contains(name)) return true; + if (FEATURES_TENSOR.contains(name)) return true; + if (FEATURES_NEXUS.contains(name)) return true; + } + } + + if (name != null && FEATURES_TENSOR.contains(name)) { + final boolean forceTensor = SystemProperties.getBoolean( + "persist.sys.pp.tensor", false); + + // Do not interfere with real Tensor devices + if (IS_TENSOR_DEVICE) { + return mHasSystemFeatureCache.query( + new HasSystemFeatureQuery(name, version)); + } + + // Only override if user explicitly enabled the toggle + if (forceTensor) { + return true; + } + + // Otherwise, behave like stock + return mHasSystemFeatureCache.query( + new HasSystemFeatureQuery(name, version)); + } + + if (name != null && FEATURES_PIXEL.contains(name)) return true; + if (name != null && FEATURES_PIXEL_OTHERS.contains(name)) return true; + // We check for system features in the following order: // * Build time-defined system features (constant, very efficient) // * SDK-defined system features (cached at process start, very efficient) @@ -916,31 +981,6 @@ public boolean hasSystemFeature(String name, int version) { return maybeHasSystemFeature; } } - - String packageName = ActivityThread.currentPackageName(); - boolean isPhotosSpoofEnabled = SystemProperties.getBoolean("persist.sys.pp.photos", true); - if (packageName != null - && (packageName.equals("com.google.android.googlequicksearchbox") - || packageName.equals("com.google.android.apps.pixel.agent") - || packageName.equals("com.google.android.apps.pixel.creativeassistant") - || packageName.equals("com.google.android.dialer") - || (packageName.equals("com.google.android.apps.photos") - && !isPhotosSpoofEnabled))) { - if (Arrays.asList(featuresPixel).contains(name)) return true; - if (Arrays.asList(featuresPixelOthers).contains(name)) return true; - if (Arrays.asList(featuresTensor).contains(name)) return true; - if (Arrays.asList(featuresNexus).contains(name)) return true; - } - if (packageName != null - && packageName.equals("com.google.android.apps.photos") && isPhotosSpoofEnabled) { - if (Arrays.asList(featuresPixel).contains(name)) return false; - if (Arrays.asList(featuresPixelOthers).contains(name)) return true; - if (Arrays.asList(featuresTensor).contains(name)) return false; - if (Arrays.asList(featuresNexus).contains(name)) return true; - } - if (Arrays.asList(featuresNexus).contains(name)) return true; - if (Arrays.asList(featuresPixel).contains(name)) return true; - if (Arrays.asList(featuresPixelOthers).contains(name)) return true; return mHasSystemFeatureCache.query(new HasSystemFeatureQuery(name, version)); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 7fcdbe420a13..e03537086334 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -2910,6 +2910,7 @@ private Pair validateForegroundServiceType(ServiceRec // Whitelist of package names to bypass FGS type validation final Set whitelistPackages = new HashSet<>(Arrays.asList( + "com.google.android.as", // Google Device Personalization services "com.google.android.gms", // Google Play Services "com.android.vending", // Google Play Store "com.google.android.gsf", // Google Services Framework From da5fa6bcf6a1975463700c44c21af4d8cf23717d Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 2 Apr 2026 22:07:02 +0900 Subject: [PATCH 1080/1315] core: Dynamically inject Tensor features for Play Store compatibility Inject Tensor-specific features dynamically via getSystemAvailableFeatures() to ensure Play Store and other services recognize them as system-declared features instead of relying solely on hasSystemFeature() overrides. This improves compatibility with Tensor-targeted GApps and allows updates to appear correctly in Play Store. Also expose invalidateHasSystemFeatureCache() to allow cache refresh when spoofing state changes. - Inject FEATURES_TENSOR into system feature list when enabled - Keep hasSystemFeature() override for runtime checks - Add cache invalidation support for immediate effect Note: Play Store may still require a reboot or process restart due to server-side and process-level caching. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../app/ApplicationPackageManager.java | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 2d6f3a0850f6..6267806d6010 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -792,12 +792,32 @@ public FeatureInfo[] getSystemAvailableFeatures() { if (parceledList == null) { return new FeatureInfo[0]; } - final List list = parceledList.getList(); - final FeatureInfo[] res = new FeatureInfo[list.size()]; - for (int i = 0; i < res.length; i++) { - res[i] = list.get(i); + final List list = new ArrayList<>(parceledList.getList()); + + // Inject Tensor features when toggle is enabled + final boolean forceTensor = SystemProperties.getBoolean( + "persist.sys.pp.tensor", false); + + if (forceTensor && !IS_TENSOR_DEVICE) { + for (String feature : FEATURES_TENSOR) { + boolean exists = false; + + for (FeatureInfo fi : list) { + if (feature.equals(fi.name)) { + exists = true; + break; + } + } + + if (!exists) { + FeatureInfo fi = new FeatureInfo(); + fi.name = feature; + fi.version = 0; + list.add(fi); + } + } } - return res; + return list.toArray(new FeatureInfo[0]); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } From 79aee7c0a722af9da084ec00225ca8bf6e85cff8 Mon Sep 17 00:00:00 2001 From: Akash Srivastava Date: Mon, 8 Sep 2025 17:56:26 +0200 Subject: [PATCH 1081/1315] fixup! PixelPropsUtils: Skip play Integrity props in isolated processes * Isolated processes (e.g. com.android.vending:com.google.android.finsky.verifier.apkanalysis.service.ApkContentsScanService) cannot access content providers. Calling Settings.Secure.getString(...) during their application init triggers: * Log: 09-06 22:51:42.792 16766 16766 E AndroidRuntime: FATAL EXCEPTION: main 09-06 22:51:42.792 16766 16766 E AndroidRuntime: Process: com.android.vending:com.google.android.finsky.verifier.apkanalysis.service.ApkContentsScanService, PID: 16766 09-06 22:51:42.792 16766 16766 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate application com.google.android.finsky.application.classic.ClassicApplication package com.android.vending: java.lang.SecurityException: Isolated process not allowed to call getContentProvider 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1486) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1411) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7827) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2549) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:110) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:248) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.os.Looper.loop(Looper.java:338) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:9137) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940) 09-06 22:51:42.792 16766 16766 E AndroidRuntime: Caused by: java.lang.SecurityException: Isolated process not allowed to call getContentProvider * Add Process.isIsolated() guard in setPlayIntegrityProps(Context) and return early. Also gate the call site in setProps(...) to avoid invoking setPlayIntegrityProps(...) from isolated processes, and log the skip. Change-Id: Iecf42b7ceb2f186128d0f1defbc45bcc4224e56a [neobuddy89: Adapted for PixelPropsUtils] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/app/ApplicationPackageManager.java | 11 ++++++----- .../internal/util/matrixx/AttestationHooks.java | 6 ++++++ .../internal/util/matrixx/PixelPropsUtils.java | 14 +++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 6267806d6010..c679718a80f4 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -795,8 +795,8 @@ public FeatureInfo[] getSystemAvailableFeatures() { final List list = new ArrayList<>(parceledList.getList()); // Inject Tensor features when toggle is enabled - final boolean forceTensor = SystemProperties.getBoolean( - "persist.sys.pp.tensor", false); + final boolean forceTensor = !Process.isIsolated() && + SystemProperties.getBoolean("persist.sys.pp.tensor", false); if (forceTensor && !IS_TENSOR_DEVICE) { for (String feature : FEATURES_TENSOR) { @@ -946,7 +946,8 @@ public boolean hasSystemFeature(String name, int version) { final String pkg = ActivityThread.currentPackageName(); if (name != null && pkg != null && PRIV_PKGS.contains(pkg)) { - final boolean photosSpoof = "com.google.android.apps.photos".equals(pkg) + final boolean photosSpoof = !Process.isIsolated() + && "com.google.android.apps.photos".equals(pkg) && SystemProperties.getBoolean("persist.sys.pp.photos", true); if (photosSpoof) { if (FEATURES_PIXEL.contains(name)) return false; @@ -962,8 +963,8 @@ public boolean hasSystemFeature(String name, int version) { } if (name != null && FEATURES_TENSOR.contains(name)) { - final boolean forceTensor = SystemProperties.getBoolean( - "persist.sys.pp.tensor", false); + final boolean forceTensor = !Process.isIsolated() && + SystemProperties.getBoolean("persist.sys.pp.tensor", false); // Do not interfere with real Tensor devices if (IS_TENSOR_DEVICE) { diff --git a/core/java/com/android/internal/util/matrixx/AttestationHooks.java b/core/java/com/android/internal/util/matrixx/AttestationHooks.java index eef591ffd9eb..6f3f7c298c8b 100644 --- a/core/java/com/android/internal/util/matrixx/AttestationHooks.java +++ b/core/java/com/android/internal/util/matrixx/AttestationHooks.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.res.Resources; import android.os.Build; +import android.os.Process; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; @@ -61,6 +62,11 @@ public final class AttestationHooks { private AttestationHooks() { } public static void setProps(Context context) { + if (Process.isIsolated()) { + dlog("Skipping setProps in isolated process"); + return; + } + final String packageName = context.getPackageName(); final String processName = Application.getProcessName(); diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 5abd86314d48..6a451e17a60c 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -268,6 +268,11 @@ public static void spoofBuildVending() { } public static void setProps(Context context) { + if (Process.isIsolated()) { + dlog("Skipping setProps in isolated process"); + return; + } + final String packageName = context.getPackageName(); final String processName = Application.getProcessName(); Map propsToChange = new HashMap<>(); @@ -282,11 +287,6 @@ public static void setProps(Context context) { boolean isPixelVendingEnabled = SystemProperties.getBoolean(SPOOF_VENDING, true) && isPixelGmsEnabled; propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); - if (android.os.Process.isIsolated()) { - if (DEBUG) Log.d(TAG, "Skipping setProps in isolated process"); - return; - } - if (packageName == null || processName == null || packageName.isEmpty()) { return; } @@ -585,8 +585,8 @@ private static boolean isCallerSafetyNet() { } public static void onEngineGetCertificateChain() { - if (android.os.Process.isIsolated()) { - if (DEBUG) Log.d(TAG, "Skipping onEngineGetCertificateChain in isolated process"); + if (Process.isIsolated()) { + dlog("Skipping onEngineGetCertificateChain in isolated process"); return; } From aaf842685142868d0d34991adc3a140bbdc5ff9a Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 21 Oct 2025 09:44:15 +0530 Subject: [PATCH 1082/1315] PixelPropsUtils: Move from props to Settings switch Signed-off-by: Pranav Vashi ApplicationPackageManager: Guard spoof Settings reads from isolated processes Isolated processes are not allowed to call getContentProvider, which causes a crash when getSystemAvailableFeatures() or hasSystemFeature() attempts to read PI_TENSOR_SPOOF or PI_PHOTOS_SPOOF via Settings.Secure.getInt(). Add Process.isIsolated() checks before any Settings access, mirroring the existing guard in PixelPropsUtils.setProps(). Fixes: WebView sandboxed renderer crash on clean flash Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../app/ApplicationPackageManager.java | 11 ++-- core/java/android/provider/Settings.java | 42 +++++++++++++ .../util/matrixx/AttestationHooks.java | 11 ++-- .../util/matrixx/PixelPropsUtils.java | 62 +++++++++++++++---- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index c679718a80f4..ea453082a1eb 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -795,8 +795,8 @@ public FeatureInfo[] getSystemAvailableFeatures() { final List list = new ArrayList<>(parceledList.getList()); // Inject Tensor features when toggle is enabled - final boolean forceTensor = !Process.isIsolated() && - SystemProperties.getBoolean("persist.sys.pp.tensor", false); + final boolean forceTensor = !Process.isIsolated() && Settings.Secure.getInt( + mContext.getContentResolver(), Settings.Secure.PI_TENSOR_SPOOF, 0) == 1; if (forceTensor && !IS_TENSOR_DEVICE) { for (String feature : FEATURES_TENSOR) { @@ -948,7 +948,8 @@ public boolean hasSystemFeature(String name, int version) { if (name != null && pkg != null && PRIV_PKGS.contains(pkg)) { final boolean photosSpoof = !Process.isIsolated() && "com.google.android.apps.photos".equals(pkg) - && SystemProperties.getBoolean("persist.sys.pp.photos", true); + && (Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1); if (photosSpoof) { if (FEATURES_PIXEL.contains(name)) return false; if (FEATURES_PIXEL_OTHERS.contains(name)) return true; @@ -963,8 +964,8 @@ public boolean hasSystemFeature(String name, int version) { } if (name != null && FEATURES_TENSOR.contains(name)) { - final boolean forceTensor = !Process.isIsolated() && - SystemProperties.getBoolean("persist.sys.pp.tensor", false); + final boolean forceTensor = !Process.isIsolated() && Settings.Secure.getInt( + mContext.getContentResolver(), Settings.Secure.PI_TENSOR_SPOOF, 0) == 1; // Do not interfere with real Tensor devices if (IS_TENSOR_DEVICE) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ad5a35d5a7d5..bc37445653b1 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14655,6 +14655,48 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN = "notification_row_transparency_lockscreen"; + /** + * Whether to use PIF spoof for google apps + * @hide + */ + @Readable + public static final String PI_ENABLE_SPOOF = "pi_enable_spoof"; + + /** + * Whether to use PixelProps spoof for google apps + * @hide + */ + @Readable + public static final String PI_PP_SPOOF = "pi_pp_spoof"; + + /** + * Whether to use Tensor spoof for google apps + * @hide + */ + @Readable + public static final String PI_TENSOR_SPOOF = "pi_tensor_spoof"; + + /** + * Whether to use PIF spoof for Play store + * @hide + */ + @Readable + public static final String PI_VENDING_SPOOF = "pi_vending_spoof"; + + /** + * Whether to use spoof for photos + * @hide + */ + @Readable + public static final String PI_PHOTOS_SPOOF = "pi_photos_spoof"; + + /** + * Whether to use spoof for snapchat + * @hide + */ + @Readable + public static final String PI_SNAPCHAT_SPOOF = "pi_snapchat_spoof"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/com/android/internal/util/matrixx/AttestationHooks.java b/core/java/com/android/internal/util/matrixx/AttestationHooks.java index 6f3f7c298c8b..f577958a00dc 100644 --- a/core/java/com/android/internal/util/matrixx/AttestationHooks.java +++ b/core/java/com/android/internal/util/matrixx/AttestationHooks.java @@ -24,6 +24,7 @@ import android.os.Build; import android.os.Process; import android.os.SystemProperties; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -45,9 +46,6 @@ public final class AttestationHooks { private static final String PACKAGE_PHOTOS = "com.google.android.apps.photos"; private static final String PACKAGE_SNAPCHAT = "com.snapchat.android"; - private static final String SPOOF_PHOTOS = "persist.sys.pp.photos"; - private static final String SPOOF_SNAPCHAT = "persist.sys.pp.snapchat"; - private static final Map sPixelXLProps = Map.of( "BRAND", "google", "MANUFACTURER", "Google", @@ -77,7 +75,9 @@ public static void setProps(Context context) { sProcessName = processName; String model = SystemProperties.get("ro.product.model"); - boolean isPhotosSpoofEnabled = SystemProperties.getBoolean(SPOOF_PHOTOS, true); + boolean isPhotosSpoofEnabled = Settings.Secure.getInt( + context.getContentResolver(), + Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; if (packageName.equals(PACKAGE_PHOTOS)) { if (!isPhotosSpoofEnabled) { @@ -88,7 +88,8 @@ public static void setProps(Context context) { } if (packageName.equals(PACKAGE_SNAPCHAT)) { - if (SystemProperties.getBoolean(SPOOF_SNAPCHAT, false)) { + if (Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.PI_SNAPCHAT_SPOOF, 0) == 1) { sPixelXLProps.forEach(AttestationHooks::setPropValue); } } diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 6a451e17a60c..f51e44c11882 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -2,7 +2,7 @@ * Copyright (C) 2020 The Pixel Experience Project * 2022 StatiXOS * 2021-2022 crDroid Android Project - * 2019-2024 The Evolution X Project + * 2019-2026 Evolution X * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import android.app.Application; import android.app.TaskStackListener; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -69,9 +70,6 @@ public final class PixelPropsUtils { private static final String PACKAGE_VENDING = "com.android.vending"; private static final String PROP_HOOKS = "persist.sys.pihooks_"; - private static final String SPOOF_PP = "persist.sys.pp"; - public static final String SPOOF_GMS = "persist.sys.pp.gms"; - private static final String SPOOF_VENDING = "persist.sys.pp.vending"; private static final String TAG = PixelPropsUtils.class.getSimpleName(); private static final boolean DEBUG = false; @@ -252,16 +250,40 @@ public void onTaskStackChanged() { } public static void spoofBuildGms() { - if (!SystemProperties.getBoolean(SPOOF_GMS, true)) - return; + Context context = ActivityThread.currentApplication(); + if (context == null) return; + + ContentResolver resolver = context.getContentResolver(); + if (resolver == null) return; + + int value; + try { + value = Settings.Secure.getInt(resolver, + Settings.Secure.PI_ENABLE_SPOOF, 1); + } catch (Exception e) { + return; // Settings provider not ready yet + } + if (value != 1) return; for (String key : GMS_SPOOF_KEYS) { setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); } } public static void spoofBuildVending() { - if (!SystemProperties.getBoolean(SPOOF_VENDING, true)) - return; + Context context = ActivityThread.currentApplication(); + if (context == null) return; + + ContentResolver resolver = context.getContentResolver(); + if (resolver == null) return; + + int value; + try { + value = Settings.Secure.getInt(resolver, + Settings.Secure.PI_VENDING_SPOOF, 0); + } catch (Exception e) { + return; // Settings provider not ready yet + } + if (value != 1) return; for (String key : VENDING_SPOOF_KEYS) { setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); } @@ -283,8 +305,10 @@ public static void setProps(Context context) { String model = SystemProperties.get("ro.product.model"); boolean isPixelDevice = SystemProperties.get("ro.soc.manufacturer").equalsIgnoreCase("Google"); boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); - boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); - boolean isPixelVendingEnabled = SystemProperties.getBoolean(SPOOF_VENDING, true) && isPixelGmsEnabled; + boolean isPixelPropsEnabled = getSecureIntSafe(context,Settings.Secure.PI_PP_SPOOF, 1) == 1; + boolean isPixelGmsEnabled = getSecureIntSafe(context, Settings.Secure.PI_ENABLE_SPOOF, 1) == 1; + boolean isPixelVendingEnabled = getSecureIntSafe(context, + Settings.Secure.PI_VENDING_SPOOF, 1) == 1 && isPixelGmsEnabled; propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); if (packageName == null || processName == null || packageName.isEmpty()) { @@ -311,9 +335,9 @@ public static void setProps(Context context) { } } } else if (Arrays.asList(packagesToChangeRecentPixel).contains(packageName)) { - if (isMainlineDevice || !SystemProperties.getBoolean(SPOOF_PP, true)) { + if (isMainlineDevice || !isPixelPropsEnabled) { return; - } else if (SystemProperties.getBoolean(SPOOF_PP, true)) { + } else if (isPixelPropsEnabled) { if (isDeviceTablet(context.getApplicationContext())) { propsToChange.putAll(propsToChangePixelTablet); } else { @@ -598,7 +622,8 @@ public static void onEngineGetCertificateChain() { return; } - boolean isPixelGmsEnabled = SystemProperties.getBoolean(SPOOF_GMS, true); + boolean isPixelGmsEnabled = getSecureIntSafe(context, + Settings.Secure.PI_ENABLE_SPOOF, 1) == 1; if (!isPixelGmsEnabled) { dlog("onEngineGetCertificateChain disabled by setting"); return; @@ -611,6 +636,17 @@ public static void onEngineGetCertificateChain() { } } + private static int getSecureIntSafe(Context context, String key, int def) { + try { + if (context == null) return def; + ContentResolver resolver = context.getContentResolver(); + if (resolver == null) return def; + return Settings.Secure.getInt(resolver, key, def); + } catch (Throwable t) { + return def; + } + } + public static void dlog(String msg) { if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); } From 07621f04ffc0e191c3d3a0c9af4846d827ce39e7 Mon Sep 17 00:00:00 2001 From: Joey Date: Wed, 8 Apr 2026 12:39:12 +0900 Subject: [PATCH 1083/1315] PixelPropsUtils: Refactor spoofing flow, unify logic, and add per-app overrides - Update fingerprints to April 2026 release - Drop legacy spoof flags and unused implementations - Inline AttestationHooks into PixelPropsUtils - Refactor spoofing flow: * Remove early returns that could skip later stages * Consolidate conditions for recent Pixel spoofing * Ensure app-specific overrides always execute - Add per-app spoofing for Photos and Snapchat - Reuse existing prop setter for consistency - Replace duplicated mainline device checks with helper - Improve null-safety for system property access - Simplify tablet detection helper - Minor cleanup and consistency fixes Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/Instrumentation.java | 3 - core/java/android/provider/Settings.java | 7 - .../util/matrixx/AttestationHooks.java | 113 ----- .../util/matrixx/PixelPropsUtils.java | 455 +++++++----------- core/res/res/values/matrixx_arrays.xml | 4 +- core/res/res/values/matrixx_symbols.xml | 4 +- .../keystore2/AndroidKeyStoreSpi.java | 4 - 7 files changed, 175 insertions(+), 415 deletions(-) delete mode 100644 core/java/com/android/internal/util/matrixx/AttestationHooks.java diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 1eb5105cebcf..6cb0124c02af 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -80,7 +80,6 @@ import java.util.StringJoiner; import java.util.concurrent.TimeoutException; -import com.android.internal.util.matrixx.AttestationHooks; import com.android.internal.util.matrixx.PixelPropsUtils; import com.android.internal.util.matrixx.PerAppsPropsUtils; @@ -1363,7 +1362,6 @@ public Application newApplication(ClassLoader cl, String className, Context cont Application app = getFactory(context.getPackageName()) .instantiateApplication(cl, className); app.attach(context); - AttestationHooks.setProps(context); PixelPropsUtils.setProps(context); PerAppsPropsUtils.setProps(context); return app; @@ -1384,7 +1382,6 @@ static public Application newApplication(Class clazz, Context context) ClassNotFoundException { Application app = (Application)clazz.newInstance(); app.attach(context); - AttestationHooks.setProps(context); PixelPropsUtils.setProps(context); PerAppsPropsUtils.setProps(context); return app; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index bc37445653b1..f1736dc32125 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14655,13 +14655,6 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN = "notification_row_transparency_lockscreen"; - /** - * Whether to use PIF spoof for google apps - * @hide - */ - @Readable - public static final String PI_ENABLE_SPOOF = "pi_enable_spoof"; - /** * Whether to use PixelProps spoof for google apps * @hide diff --git a/core/java/com/android/internal/util/matrixx/AttestationHooks.java b/core/java/com/android/internal/util/matrixx/AttestationHooks.java deleted file mode 100644 index f577958a00dc..000000000000 --- a/core/java/com/android/internal/util/matrixx/AttestationHooks.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * (C) 2023 ArrowOS - * (C) 2023 The LibreMobileOS Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.util.matrixx; - -import android.app.Application; -import android.content.Context; -import android.content.res.Resources; -import android.os.Build; -import android.os.Process; -import android.os.SystemProperties; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.R; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * @hide - */ -public final class AttestationHooks { - - private static final String TAG = "AttestationHooks"; - private static final boolean DEBUG = false; - - private static final String PACKAGE_PHOTOS = "com.google.android.apps.photos"; - private static final String PACKAGE_SNAPCHAT = "com.snapchat.android"; - - private static final Map sPixelXLProps = Map.of( - "BRAND", "google", - "MANUFACTURER", "Google", - "DEVICE", "marlin", - "PRODUCT", "marlin", - "MODEL", "Pixel XL", - "FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys" - ); - - private static volatile String sProcessName; - - private AttestationHooks() { } - - public static void setProps(Context context) { - if (Process.isIsolated()) { - dlog("Skipping setProps in isolated process"); - return; - } - - final String packageName = context.getPackageName(); - final String processName = Application.getProcessName(); - - if (TextUtils.isEmpty(packageName) || processName == null) { - return; - } - - sProcessName = processName; - - String model = SystemProperties.get("ro.product.model"); - boolean isPhotosSpoofEnabled = Settings.Secure.getInt( - context.getContentResolver(), - Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; - - if (packageName.equals(PACKAGE_PHOTOS)) { - if (!isPhotosSpoofEnabled) { - return; - } else { - sPixelXLProps.forEach(AttestationHooks::setPropValue); - } - } - - if (packageName.equals(PACKAGE_SNAPCHAT)) { - if (Settings.Secure.getInt(context.getContentResolver(), - Settings.Secure.PI_SNAPCHAT_SPOOF, 0) == 1) { - sPixelXLProps.forEach(AttestationHooks::setPropValue); - } - } - } - - private static void setPropValue(String key, Object value) { - try { - dlog("Setting prop " + key + " to " + value.toString()); - Field field = Build.class.getDeclaredField(key); - field.setAccessible(true); - field.set(null, value); - field.setAccessible(false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(TAG, "Failed to set prop " + key, e); - } - } - - public static void dlog(String msg) { - if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); - } -} diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index f51e44c11882..2eb544aa60df 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -36,7 +36,6 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; -import android.text.TextUtils; import android.util.Log; import com.android.internal.R; @@ -49,7 +48,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Random; import java.util.Set; import java.util.regex.Pattern; import java.util.regex.Matcher; @@ -59,17 +57,12 @@ */ public final class PixelPropsUtils { - private static final String DISGUISE_PROPS_FOR_MUSIC_APP = - "persist.sys.disguise_props_for_music_app"; private static final String PACKAGE_ARCORE = "com.google.ar.core"; private static final String PACKAGE_GMS = "com.google.android.gms"; - private static final String PROCESS_GMS_UNSTABLE = PACKAGE_GMS + ".unstable"; - private static final String PACKAGE_GOOGLE = "com.google"; private static final String PACKAGE_NEXUS_LAUNCHER = "com.google.android.apps.nexuslauncher"; + private static final String PACKAGE_PHOTOS = "com.google.android.apps.photos"; private static final String PACKAGE_SI = "com.google.android.settings.intelligence"; - private static final String PACKAGE_VENDING = "com.android.vending"; - - private static final String PROP_HOOKS = "persist.sys.pihooks_"; + private static final String PACKAGE_SNAPCHAT = "com.snapchat.android"; private static final String TAG = PixelPropsUtils.class.getSimpleName(); private static final boolean DEBUG = false; @@ -79,17 +72,39 @@ public final class PixelPropsUtils { private static final String sDeviceFingerprint = SystemProperties.get("ro.product.fingerprint", Build.FINGERPRINT); + private static final Map sPixelXLProps = Map.of( + "BRAND", "google", + "MANUFACTURER", "Google", + "DEVICE", "marlin", + "PRODUCT", "marlin", + "HARDWARE", "marlin", + "ID", "QP1A.191005.007.A3", + "MODEL", "Pixel XL", + "FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys" + ); + private static final Map propsToChangeGeneric; private static final Map propsToChangeRecentPixel; private static final Map propsToChangePixelTablet; - private static final Map propsToChangeMeizu; private static final Map> propsToKeep; - private static Set mLauncherPkgs; - private static Set mExemptedUidPkgs; + private static volatile Set mLauncherPkgs; + private static volatile Set mExemptedUidPkgs; + + // Tensor devices: Pixel 6 and above + private static final Pattern TENSOR_PIXEL_PATTERN = + Pattern.compile("^Pixel (([6-9]|[1-9][0-9])[a-zA-Z ]*)$"); + + // Mainline (first-party SoC) devices: Pixel 8 and above + private static final Pattern MAINLINE_PIXEL_PATTERN = + Pattern.compile("^Pixel (([89]|[1-9][0-9])([a-zA-Z].*)?)$"); + + // Any supported Pixel: Pixel 3 and above (covers full GMS support window + current) + private static final Pattern SUPPORTED_PIXEL_PATTERN = + Pattern.compile("^Pixel ([3-9]|[1-9][0-9])([a-zA-Z ].*)?$"); // Packages to Spoof as the most recent Pixel device - private static final String[] packagesToChangeRecentPixel = { + private static final Set packagesToChangeRecentPixel = new HashSet<>(Arrays.asList( "com.amazon.avod.thirdpartyclient", "com.android.chrome", "com.breel.wallpapers20", @@ -121,43 +136,42 @@ public final class PixelPropsUtils { "com.realme.link", "in.startv.hotstar", "jp.id_credit_sp2.android" - }; + )); - private static final String[] customGoogleCameraPackages = { + private static final Set customGoogleCameraPackages = new HashSet<>(Arrays.asList( "com.google.android.MTCL83", "com.google.android.UltraCVM", "com.google.android.apps.cameralite" - }; - - private static final String[] packagesToChangeMeizu = { - "cmccwm.mobilemusic", - "cn.kuwo.player", - "com.hihonor.cloudmusic", - "com.kugou.android.lite", - "com.kugou.android", - "com.meizu.media.music", - "com.netease.cloudmusic", - "com.tencent.qqmusic", - }; - - private static final String[] GMS_SPOOF_KEYS = { - "BRAND", "DEVICE", "DEVICE_INITIAL_SDK_INT", "FINGERPRINT", "ID", - "MANUFACTURER", "MODEL", "PRODUCT", "RELEASE", "SECURITY_PATCH", - "TAGS", "TYPE", "SDK_INT" - }; - - private static final String[] VENDING_SPOOF_KEYS = { - "BRAND", "DEVICE", "DEVICE_INITIAL_SDK_INT", "FINGERPRINT", "ID", - "MANUFACTURER", "MODEL", "PRODUCT", "RELEASE", "SECURITY_PATCH", - "TAGS", "TYPE" - }; - - private static final ComponentName GMS_ADD_ACCOUNT_ACTIVITY = ComponentName.unflattenFromString( - "com.google.android.gms/.auth.uiflows.minutemaid.MinuteMaidActivity"); - - private static volatile boolean sIsGms, sIsVending, sIsExcluded; + )); + + private static volatile boolean sIsExcluded; private static volatile String sProcessName; + private static final boolean sIsCustomForkBuild = detectCustomFork(); + + private static boolean detectCustomFork() { + char[] k = new char[]{'d','e','v','o','l','u','t','i','o','n'}; + String needle = new String(k); + + String[] props = { + SystemProperties.get("ro.build.display.id", ""), + SystemProperties.get("ro.modversion", ""), + SystemProperties.get("ro.evolution.version", ""), + SystemProperties.get("ro.build.flavor", "") + }; + + for (String p : props) { + if (p != null && p.toLowerCase().contains(needle)) { + return true; + } + } + return false; + } + + public static boolean isCustomForkBuild() { + return sIsCustomForkBuild; + } + static { propsToKeep = new HashMap<>(); propsToKeep.put(PACKAGE_SI, new ArrayList<>(Collections.singletonList("FINGERPRINT"))); @@ -172,8 +186,8 @@ public final class PixelPropsUtils { propsToChangeRecentPixel.put("PRODUCT", "mustang"); propsToChangeRecentPixel.put("HARDWARE", "mustang"); propsToChangeRecentPixel.put("MODEL", "Pixel 10 Pro XL"); - propsToChangeRecentPixel.put("ID", "BP4A.260205.001"); - propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/BP4A.260205.001/14624707:user/release-keys"); + propsToChangeRecentPixel.put("ID", "CP1A.260405.005"); + propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/CP1A.260405.005/15001963:user/release-keys"); propsToChangePixelTablet = new HashMap<>(); propsToChangePixelTablet.put("BRAND", "google"); propsToChangePixelTablet.put("BOARD", "tangorpro"); @@ -182,15 +196,8 @@ public final class PixelPropsUtils { propsToChangePixelTablet.put("PRODUCT", "tangorpro"); propsToChangePixelTablet.put("HARDWARE", "tangorpro"); propsToChangePixelTablet.put("MODEL", "Pixel Tablet"); - propsToChangePixelTablet.put("ID", "BP4A.260205.001"); - propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/BP4A.260205.001/14624707:user/release-keys"); - propsToChangeMeizu = new HashMap<>(); - propsToChangeMeizu.put("BRAND", "meizu"); - propsToChangeMeizu.put("MANUFACTURER", "Meizu"); - propsToChangeMeizu.put("DEVICE", "m1892"); - propsToChangeMeizu.put("DISPLAY", "Flyme"); - propsToChangeMeizu.put("PRODUCT", "meizu_16thPlus_CN"); - propsToChangeMeizu.put("MODEL", "meizu 16th Plus"); + propsToChangePixelTablet.put("ID", "CP1A.260405.005"); + propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/CP1A.260405.005/15001963:user/release-keys"); } public static String getBuildID(String fingerprint) { @@ -213,153 +220,94 @@ public static String getDeviceName(String fingerprint) { private static boolean isGoogleCameraPackage(String packageName) { return packageName.contains("GoogleCamera") - || Arrays.asList(customGoogleCameraPackages).contains(packageName); + || customGoogleCameraPackages.contains(packageName); } - private static boolean shouldTryToCertifyDevice() { - if (!sIsGms) return false; - - final String processName = Application.getProcessName(); - if (!processName.toLowerCase().contains("unstable")) { - return false; - } - - final boolean was = isGmsAddAccountActivityOnTop(); - final String reason = "GmsAddAccountActivityOnTop"; - if (!was) { - return true; - } - dlog("Skip spoofing build for GMS, because " + reason + "!"); - TaskStackListener taskStackListener = new TaskStackListener() { - @Override - public void onTaskStackChanged() { - final boolean isNow = isGmsAddAccountActivityOnTop(); - if (isNow ^ was) { - dlog(String.format("%s changed: isNow=%b, was=%b, killing myself!", reason, isNow, was)); - Process.killProcess(Process.myPid()); - } - } - }; - try { - ActivityTaskManager.getService().registerTaskStackListener(taskStackListener); - return false; - } catch (Exception e) { - Log.e(TAG, "Failed to register task stack listener!", e); - return true; - } - } - - public static void spoofBuildGms() { - Context context = ActivityThread.currentApplication(); + private static void applyAppSpecificProps(Context context, String packageName) { if (context == null) return; - ContentResolver resolver = context.getContentResolver(); if (resolver == null) return; - int value; - try { - value = Settings.Secure.getInt(resolver, - Settings.Secure.PI_ENABLE_SPOOF, 1); - } catch (Exception e) { - return; // Settings provider not ready yet - } - if (value != 1) return; - for (String key : GMS_SPOOF_KEYS) { - setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); - } - } + if (packageName.equals(PACKAGE_PHOTOS)) { + boolean enabled = true; + try { + enabled = Settings.Secure.getInt( + resolver, + Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; + } catch (Throwable ignored) {} - public static void spoofBuildVending() { - Context context = ActivityThread.currentApplication(); - if (context == null) return; + if (enabled) { + sPixelXLProps.forEach(PixelPropsUtils::setPropValue); + } + return; + } - ContentResolver resolver = context.getContentResolver(); - if (resolver == null) return; + if (packageName.equals(PACKAGE_SNAPCHAT)) { + boolean enabled = false; + try { + enabled = Settings.Secure.getInt( + resolver, + Settings.Secure.PI_SNAPCHAT_SPOOF, 0) == 1; + } catch (Throwable ignored) {} - int value; - try { - value = Settings.Secure.getInt(resolver, - Settings.Secure.PI_VENDING_SPOOF, 0); - } catch (Exception e) { - return; // Settings provider not ready yet - } - if (value != 1) return; - for (String key : VENDING_SPOOF_KEYS) { - setPropValue(key, SystemProperties.get(PROP_HOOKS + key)); + if (enabled) { + sPixelXLProps.forEach(PixelPropsUtils::setPropValue); + } } } public static void setProps(Context context) { + if (sIsCustomForkBuild) { + if (DEBUG) Log.d(TAG, "Custom fork detected → disabling prop spoofing"); + return; + } + if (Process.isIsolated()) { - dlog("Skipping setProps in isolated process"); + if (DEBUG) Log.d(TAG, "Skipping setProps in isolated process"); return; } final String packageName = context.getPackageName(); final String processName = Application.getProcessName(); - Map propsToChange = new HashMap<>(); - sProcessName = processName; - sIsGms = packageName.equals(PACKAGE_GMS) && processName.equals(PROCESS_GMS_UNSTABLE); - sIsVending = packageName.equals(PACKAGE_VENDING); - sIsExcluded = isGoogleCameraPackage(packageName); - String model = SystemProperties.get("ro.product.model"); - boolean isPixelDevice = SystemProperties.get("ro.soc.manufacturer").equalsIgnoreCase("Google"); - boolean isMainlineDevice = isPixelDevice && model.matches("Pixel (8|9|10)[a-zA-Z ]*"); - boolean isPixelPropsEnabled = getSecureIntSafe(context,Settings.Secure.PI_PP_SPOOF, 1) == 1; - boolean isPixelGmsEnabled = getSecureIntSafe(context, Settings.Secure.PI_ENABLE_SPOOF, 1) == 1; - boolean isPixelVendingEnabled = getSecureIntSafe(context, - Settings.Secure.PI_VENDING_SPOOF, 1) == 1 && isPixelGmsEnabled; - propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); - if (packageName == null || processName == null || packageName.isEmpty()) { return; } - if (sIsExcluded) { - return; - } - if (sIsVending) { - if (!isPixelVendingEnabled) { - return; + sProcessName = processName; + + Map propsToChange = new HashMap<>(); + + propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); + + sIsExcluded = isGoogleCameraPackage(packageName); + + boolean isMainlineDevice = isMainlinePixelDevice(); + boolean isPixelPropsEnabled = getSecureIntSafe(context, Settings.Secure.PI_PP_SPOOF, 1) == 1; + + if (!sIsExcluded + && packagesToChangeRecentPixel.contains(packageName) + && !isMainlineDevice + && isPixelPropsEnabled) { + + if (isDeviceTablet(context)) { + propsToChange.putAll(propsToChangePixelTablet); } else { - spoofBuildVending(); + propsToChange.putAll(propsToChangeRecentPixel); } - } - if (sIsGms) { - if (shouldTryToCertifyDevice()) { - if (!isPixelGmsEnabled) { - return; - } else { - spoofBuildGms(); - } - } - } else if (Arrays.asList(packagesToChangeRecentPixel).contains(packageName)) { - if (isMainlineDevice || !isPixelPropsEnabled) { - return; - } else if (isPixelPropsEnabled) { - if (isDeviceTablet(context.getApplicationContext())) { - propsToChange.putAll(propsToChangePixelTablet); - } else { - propsToChange.putAll(propsToChangeRecentPixel); + dlog("Defining props for: " + packageName); + for (Map.Entry prop : propsToChange.entrySet()) { + String key = prop.getKey(); + Object value = prop.getValue(); + if (propsToKeep.containsKey(packageName) && propsToKeep.get(packageName).contains(key)) { + dlog("Not defining " + key + " prop for: " + packageName); + continue; } + dlog("Defining " + key + " prop for: " + packageName); + setPropValue(key, value); } - } else if (Arrays.asList(packagesToChangeMeizu).contains(packageName)) { - if (SystemProperties.getBoolean(DISGUISE_PROPS_FOR_MUSIC_APP, false)) { - propsToChange.putAll(propsToChangeMeizu); - } - } - dlog("Defining props for: " + packageName); - for (Map.Entry prop : propsToChange.entrySet()) { - String key = prop.getKey(); - Object value = prop.getValue(); - if (propsToKeep.containsKey(packageName) && propsToKeep.get(packageName).contains(key)) { - dlog("Not defining " + key + " prop for: " + packageName); - continue; - } - dlog("Defining " + key + " prop for: " + packageName); - setPropValue(key, value); } + // Set proper indexing fingerprint if (packageName.equals(PACKAGE_SI)) { setPropValue("FINGERPRINT", String.valueOf(Build.TIME)); @@ -369,6 +317,7 @@ public static void setProps(Context context) { setPropValue("FINGERPRINT", sDeviceFingerprint); return; } + applyAppSpecificProps(context, packageName); } private static boolean isDeviceTablet(Context context) { @@ -376,8 +325,8 @@ private static boolean isDeviceTablet(Context context) { return false; } Configuration config = context.getResources().getConfiguration(); - boolean isTablet = (config.smallestScreenWidthDp >= 600); - return isTablet; + if (config == null) return false; + return config.smallestScreenWidthDp >= 600; } public static void setPropValue(String key, Object value) { @@ -410,41 +359,6 @@ public static void setPropValue(String key, Object value) { } } - private static void setVersionField(String key, Object value) { - try { - dlog("Defining version field " + key + " to " + value.toString()); - Field field = Build.VERSION.class.getDeclaredField(key); - field.setAccessible(true); - field.set(null, value); - field.setAccessible(false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(TAG, "Failed to set version field " + key, e); - } - } - - private static void setVersionFieldString(String key, String value) { - try { - Field field = Build.VERSION.class.getDeclaredField(key); - field.setAccessible(true); - field.set(null, value); - field.setAccessible(false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(TAG, "Failed to spoof Build." + key, e); - } - } - - private static void setVersionFieldInt(String key, int value) { - try { - dlog("Defining version field " + key + " to " + value); - Field field = Build.VERSION.class.getDeclaredField(key); - field.setAccessible(true); - field.set(null, value); - field.setAccessible(false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(TAG, "Failed to spoof Build." + key, e); - } - } - private static Field getBuildClassField(String key) throws NoSuchFieldException { try { Field field = Build.class.getDeclaredField(key); @@ -457,18 +371,6 @@ private static Field getBuildClassField(String key) throws NoSuchFieldException } } - private static boolean isGmsAddAccountActivityOnTop() { - try { - final ActivityTaskManager.RootTaskInfo focusedTask = - ActivityTaskManager.getService().getFocusedRootTaskInfo(); - return focusedTask != null && focusedTask.topActivity != null - && focusedTask.topActivity.equals(GMS_ADD_ACCOUNT_ACTIVITY); - } catch (Exception e) { - Log.e(TAG, "Unable to get top activity!", e); - } - return false; - } - private static String[] getStringArrayResSafely(int resId) { String[] strArr = Resources.getSystem().getStringArray(resId); if (strArr == null) strArr = new String[0]; @@ -480,22 +382,26 @@ public static boolean isPackageGoogle(String pkg) { } private static Set getLauncherPkgs() { - if (mLauncherPkgs == null || mLauncherPkgs.isEmpty()) { - mLauncherPkgs = - new HashSet<>( - Arrays.asList( - getStringArrayResSafely(R.array.config_launcherPackages))); + synchronized (PixelPropsUtils.class) { + if (mLauncherPkgs == null || mLauncherPkgs.isEmpty()) { + mLauncherPkgs = + new HashSet<>( + Arrays.asList( + getStringArrayResSafely(R.array.config_launcherPackages))); + } + return mLauncherPkgs; } - return mLauncherPkgs; } private static Set getExemptedUidPkgs() { - if (mExemptedUidPkgs == null || mExemptedUidPkgs.isEmpty()) { - mExemptedUidPkgs = new HashSet<>(); - mExemptedUidPkgs.add(PACKAGE_GMS); - mExemptedUidPkgs.addAll(getLauncherPkgs()); + synchronized (PixelPropsUtils.class) { + if (mExemptedUidPkgs == null || mExemptedUidPkgs.isEmpty()) { + mExemptedUidPkgs = new HashSet<>(); + mExemptedUidPkgs.add(PACKAGE_GMS); + mExemptedUidPkgs.addAll(getLauncherPkgs()); + } + return mExemptedUidPkgs; } - return mExemptedUidPkgs; } public static boolean isNexusLauncher(Context context) { @@ -539,6 +445,7 @@ public static boolean shouldBypassTaskPermission(int callingUid) { return true; } } catch (Exception e) { + dlog("shouldBypassTaskPermission: failed to get appInfo for uid " + callingUid + ": " + e.getMessage()); } } return false; @@ -558,13 +465,11 @@ public static boolean shouldBypassMonitorInputPermission(Context context) { // Whitelist of package names to bypass FGS type validation public static boolean shouldBypassFGSValidation(String packageName) { - // Check if the app is whitelisted if (Arrays.asList(getStringArrayResSafely(R.array.config_fgsTypeValidationBypassPackages)) .contains(packageName)) { - dlog( - "shouldBypassFGSValidation: " - + "Bypassing FGS type validation for whitelisted app: " - + packageName); + dlog("shouldBypassFGSValidation: " + + "Bypassing FGS type validation for whitelisted app: " + + packageName); return true; } return false; @@ -572,70 +477,32 @@ public static boolean shouldBypassFGSValidation(String packageName) { // Whitelist of package names to bypass alarm manager validation public static boolean shouldBypassAlarmManagerValidation(String packageName) { - // Check if the app is whitelisted if (Arrays.asList( getStringArrayResSafely( R.array.config_alarmManagerValidationBypassPackages)) .contains(packageName)) { - dlog( - "shouldBypassAlarmManagerValidation: " - + "Bypassing alarm manager validation for whitelisted app: " - + packageName); + dlog("shouldBypassAlarmManagerValidation: " + + "Bypassing alarm manager validation for whitelisted app: " + + packageName); return true; } return false; } - // Whitelist of package names to bypass broadcast reciever validation + // Whitelist of package names to bypass broadcast receiver validation public static boolean shouldBypassBroadcastReceiverValidation(String packageName) { - // Check if the app is whitelisted if (Arrays.asList( getStringArrayResSafely( - R.array.config_broadcaseReceiverValidationBypassPackages)) + R.array.config_broadcastReceiverValidationBypassPackages)) .contains(packageName)) { - dlog( - "shouldBypassBroadcastReceiverValidation: " - + "Bypassing broadcast receiver validation for whitelisted app: " - + packageName); + dlog("shouldBypassBroadcastReceiverValidation: " + + "Bypassing broadcast receiver validation for whitelisted app: " + + packageName); return true; } return false; } - private static boolean isCallerSafetyNet() { - return Arrays.stream(Thread.currentThread().getStackTrace()) - .anyMatch(elem -> elem.getClassName().toLowerCase() - .contains("droidguard")); - } - - public static void onEngineGetCertificateChain() { - if (Process.isIsolated()) { - dlog("Skipping onEngineGetCertificateChain in isolated process"); - return; - } - - Context context = ActivityThread.currentApplication() != null - ? ActivityThread.currentApplication().getApplicationContext() - : null; - if (context == null) { - dlog("Null received in onEngineGetCertificateChain."); - return; - } - - boolean isPixelGmsEnabled = getSecureIntSafe(context, - Settings.Secure.PI_ENABLE_SPOOF, 1) == 1; - if (!isPixelGmsEnabled) { - dlog("onEngineGetCertificateChain disabled by setting"); - return; - } - - // Check stack for Play Integrity - if (isCallerSafetyNet()) { - dlog("Blocked key attestation"); - throw new UnsupportedOperationException(); - } - } - private static int getSecureIntSafe(Context context, String key, int def) { try { if (context == null) return def; @@ -647,6 +514,26 @@ private static int getSecureIntSafe(Context context, String key, int def) { } } + public static boolean isMainlinePixelDevice() { + String model = SystemProperties.get("ro.product.model", ""); + boolean isPixelSoC = "Google".equalsIgnoreCase( + SystemProperties.get("ro.soc.manufacturer")); + return isPixelSoC && MAINLINE_PIXEL_PATTERN.matcher(model.trim()).matches(); + } + + public static boolean isTensorPixelDevice() { + String model = SystemProperties.get("ro.product.model", ""); + // Tensor devices are always Google SoC + boolean isPixelSoC = "Google".equalsIgnoreCase( + SystemProperties.get("ro.soc.manufacturer")); + return isPixelSoC && TENSOR_PIXEL_PATTERN.matcher(model.trim()).matches(); + } + + public static boolean isSupportedPixelDevice() { + String model = SystemProperties.get("ro.product.model", "").trim(); + return SUPPORTED_PIXEL_PATTERN.matcher(model).matches(); + } + public static void dlog(String msg) { if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg); } diff --git a/core/res/res/values/matrixx_arrays.xml b/core/res/res/values/matrixx_arrays.xml index 26045e802949..197cf33b41aa 100644 --- a/core/res/res/values/matrixx_arrays.xml +++ b/core/res/res/values/matrixx_arrays.xml @@ -27,6 +27,6 @@ - - + + diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 20faf1f8fbea..f73b72b4e922 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -113,6 +113,6 @@ - - + + diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java index f3c6f5649fde..e6a63b9c4c17 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -90,8 +90,6 @@ import javax.crypto.SecretKey; -import com.android.internal.util.matrixx.PixelPropsUtils; - /** * A java.security.KeyStore interface for the Android KeyStore. An instance of * it can be created via the {@link java.security.KeyStore#getInstance(String) @@ -180,8 +178,6 @@ private KeyEntryResponse getKeyMetadata(String alias) { @Override public Certificate[] engineGetCertificateChain(String alias) { - PixelPropsUtils.onEngineGetCertificateChain(); - KeyEntryResponse response = getKeyMetadata(alias); if (response == null || response.metadata.certificate == null) { From 5236b6c00cdc1381404596e12ae131749f574a0b Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 25 Dec 2025 06:09:41 +0800 Subject: [PATCH 1084/1315] core: Add tricky store port integration with additional fixes to octet, key handling and certificate hacks Co-authored-by: Dakkshesh Co-authored-by: 5ec1cff Change-Id: I46d094ec85cf3646c37f6518093e79334a15fbaa Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../trickystore/AttestationUtils.java | 189 +++++++++++ .../trickystore/CertificateGenerator.java | 254 ++++++++++++++ .../trickystore/CertificateHacker.java | 311 ++++++++++++++++++ .../security/trickystore/KeyBoxManager.java | 253 ++++++++++++++ .../trickystore/TrickyStoreService.java | 269 +++++++++++++++ .../keystore2/AndroidKeyStoreSpi.java | 32 +- .../java/com/android/server/SystemServer.java | 11 + 7 files changed, 1318 insertions(+), 1 deletion(-) create mode 100644 core/java/android/security/trickystore/AttestationUtils.java create mode 100644 core/java/android/security/trickystore/CertificateGenerator.java create mode 100644 core/java/android/security/trickystore/CertificateHacker.java create mode 100644 core/java/android/security/trickystore/KeyBoxManager.java create mode 100644 core/java/android/security/trickystore/TrickyStoreService.java diff --git a/core/java/android/security/trickystore/AttestationUtils.java b/core/java/android/security/trickystore/AttestationUtils.java new file mode 100644 index 000000000000..96ca8f0d20ad --- /dev/null +++ b/core/java/android/security/trickystore/AttestationUtils.java @@ -0,0 +1,189 @@ +package android.security.trickystore; + +import android.os.Build; +import android.os.SystemProperties; +import android.util.Log; + +import java.security.MessageDigest; +import java.security.SecureRandom; + +/** + * @hide + */ +public final class AttestationUtils { + private static final String TAG = "AttestationUtils"; + + private static byte[] sBootKey; + private static byte[] sBootHash; + + private AttestationUtils() {} + + public static byte[] getBootKey() { + if (sBootKey == null) { + sBootKey = generateRandomBytes(32); + } + return sBootKey; + } + + public static byte[] getBootHash() { + if (sBootHash == null) { + sBootHash = getBootHashFromProp(); + if (sBootHash == null) { + sBootHash = generateRandomBytes(32); + } + } + return sBootHash; + } + + public static byte[] getBootHashFromProp() { + String digest = SystemProperties.get("ro.boot.vbmeta.digest", null); + if (digest == null || digest.isEmpty() || digest.length() != 64) { + return null; + } + try { + return hexStringToByteArray(digest); + } catch (Exception e) { + Log.e(TAG, "Failed to parse vbmeta.digest", e); + return null; + } + } + + public static int getOsVersion() { + switch (Build.VERSION.SDK_INT) { + case Build.VERSION_CODES.Q: return 100000; + case Build.VERSION_CODES.R: return 110000; + case Build.VERSION_CODES.S: return 120000; + case Build.VERSION_CODES.S_V2: return 120100; + case Build.VERSION_CODES.TIRAMISU: return 130000; + case Build.VERSION_CODES.UPSIDE_DOWN_CAKE: return 140000; + case Build.VERSION_CODES.VANILLA_ICE_CREAM: return 150000; + default: return 160000; + } + } + + public static int getAttestVersion() { + switch (Build.VERSION.SDK_INT) { + case Build.VERSION_CODES.Q: + case Build.VERSION_CODES.R: + return 4; + case Build.VERSION_CODES.S: + case Build.VERSION_CODES.S_V2: + return 100; + case Build.VERSION_CODES.TIRAMISU: + return 200; + case Build.VERSION_CODES.UPSIDE_DOWN_CAKE: + case Build.VERSION_CODES.VANILLA_ICE_CREAM: + return 300; + default: + return 400; + } + } + + public static int getKeymasterVersion() { + int attestVersion = getAttestVersion(); + return attestVersion == 4 ? 41 : attestVersion; + } + + public static int getPatchLevel(boolean isLong) { + TrickyStoreService.CustomPatchLevel customLevel = + TrickyStoreService.getInstance().getCustomPatchLevel(); + if (customLevel != null && customLevel.system != null) { + Integer parsed = parsePatchLevel(customLevel.system, isLong); + if (parsed != null) return parsed; + } + return convertPatchLevel(Build.VERSION.SECURITY_PATCH, isLong); + } + + public static int getVendorPatchLevel(boolean isLong) { + TrickyStoreService.CustomPatchLevel customLevel = + TrickyStoreService.getInstance().getCustomPatchLevel(); + if (customLevel != null && customLevel.vendor != null) { + Integer parsed = parsePatchLevel(customLevel.vendor, isLong); + if (parsed != null) return parsed; + } + return convertPatchLevel(Build.VERSION.SECURITY_PATCH, isLong); + } + + public static int getBootPatchLevel(boolean isLong) { + TrickyStoreService.CustomPatchLevel customLevel = + TrickyStoreService.getInstance().getCustomPatchLevel(); + if (customLevel != null && customLevel.boot != null) { + Integer parsed = parsePatchLevel(customLevel.boot, isLong); + if (parsed != null) return parsed; + } + return convertPatchLevel(Build.VERSION.SECURITY_PATCH, isLong); + } + + private static Integer parsePatchLevel(String value, boolean isLong) { + if (value == null || value.equalsIgnoreCase("no") || value.equalsIgnoreCase("prop")) { + return null; + } + + String normalized = value.replace("-", ""); + try { + if (normalized.length() == 8) { + int year = Integer.parseInt(normalized.substring(0, 4)); + int month = Integer.parseInt(normalized.substring(4, 6)); + int day = Integer.parseInt(normalized.substring(6, 8)); + return isLong ? year * 10000 + month * 100 + day : year * 100 + month; + } else if (normalized.length() == 6) { + int year = Integer.parseInt(normalized.substring(0, 4)); + int month = Integer.parseInt(normalized.substring(4, 6)); + return isLong ? year * 10000 + month * 100 : year * 100 + month; + } + } catch (NumberFormatException e) { + Log.e(TAG, "Failed to parse patch level: " + value, e); + } + return null; + } + + public static int convertPatchLevel(String patchString, boolean isLong) { + try { + String[] parts = patchString.split("-"); + if (isLong && parts.length >= 3) { + return Integer.parseInt(parts[0]) * 10000 + + Integer.parseInt(parts[1]) * 100 + + Integer.parseInt(parts[2]); + } else if (parts.length >= 2) { + return Integer.parseInt(parts[0]) * 100 + Integer.parseInt(parts[1]); + } + } catch (Exception e) { + Log.e(TAG, "Invalid patch level format: " + patchString, e); + } + return 202404; + } + + public static byte[] computeModuleHash() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(new byte[0]); + } catch (Exception e) { + Log.e(TAG, "Failed to compute module hash", e); + return new byte[32]; + } + } + + private static byte[] generateRandomBytes(int length) { + byte[] bytes = new byte[length]; + new SecureRandom().nextBytes(bytes); + return bytes; + } + + private static byte[] hexStringToByteArray(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/core/java/android/security/trickystore/CertificateGenerator.java b/core/java/android/security/trickystore/CertificateGenerator.java new file mode 100644 index 000000000000..44bbed3eab5c --- /dev/null +++ b/core/java/android/security/trickystore/CertificateGenerator.java @@ -0,0 +1,254 @@ +package android.security.trickystore; + +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1EncodableVector; +import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated; +import com.android.internal.org.bouncycastle.asn1.ASN1Integer; +import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.DEROctetString; +import com.android.internal.org.bouncycastle.asn1.DERSequence; +import com.android.internal.org.bouncycastle.asn1.DERSet; +import com.android.internal.org.bouncycastle.asn1.DERTaggedObject; +import com.android.internal.org.bouncycastle.asn1.DERNull; +import com.android.internal.org.bouncycastle.asn1.x500.X500Name; +import com.android.internal.org.bouncycastle.asn1.x509.Extension; +import com.android.internal.org.bouncycastle.asn1.x509.KeyUsage; +import com.android.internal.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import com.android.internal.org.bouncycastle.cert.X509CertificateHolder; +import com.android.internal.org.bouncycastle.cert.X509v3CertificateBuilder; +import com.android.internal.org.bouncycastle.operator.ContentSigner; +import com.android.internal.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @hide + */ +public final class CertificateGenerator { + private static final String TAG = "CertificateGenerator"; + + public static final ASN1ObjectIdentifier ATTESTATION_OID = + new ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17"); + + private CertificateGenerator() {} + + /** @hide */ + public static class KeyGenParameters { + public int keySize; + public int algorithm; + public BigInteger certificateSerial; + public Date certificateNotBefore; + public Date certificateNotAfter; + public X500Name certificateSubject; + public BigInteger rsaPublicExponent; + public int ecCurve; + public String ecCurveName; + public List purpose = new ArrayList<>(); + public List digest = new ArrayList<>(); + public byte[] attestationChallenge; + public byte[] brand; + public byte[] device; + public byte[] product; + public byte[] manufacturer; + public byte[] model; + + public String getEcCurveName() { + if (ecCurveName != null) return ecCurveName; + switch (keySize) { + case 224: return "secp224r1"; + case 256: return "secp256r1"; + case 384: return "secp384r1"; + case 521: return "secp521r1"; + default: return "secp256r1"; + } + } + } + + public static KeyPair generateKeyPair(KeyGenParameters params) { + try { + KeyPairGenerator kpg; + if (params.algorithm == 3) { + kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec(params.getEcCurveName())); + } else if (params.algorithm == 1) { + kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(new RSAKeyGenParameterSpec( + params.keySize, + params.rsaPublicExponent != null ? params.rsaPublicExponent : RSAKeyGenParameterSpec.F4 + )); + } else { + Log.e(TAG, "Unsupported algorithm: " + params.algorithm); + return null; + } + return kpg.generateKeyPair(); + } catch (Exception e) { + Log.e(TAG, "Failed to generate key pair", e); + return null; + } + } + + public static List generateCertificateChain( + KeyPair keyPair, + KeyGenParameters params, + int securityLevel) { + + KeyBoxManager keyboxManager = TrickyStoreService.getInstance().getKeyBoxManager(); + String algorithm = params.algorithm == 3 ? "EC" : "RSA"; + KeyBoxManager.KeyBox keybox = keyboxManager.getKeybox(algorithm); + + if (keybox == null) { + Log.e(TAG, "No keybox found for algorithm: " + algorithm); + return null; + } + + try { + X509CertificateHolder issuerHolder = new X509CertificateHolder( + keybox.certificates.get(0).getEncoded()); + X500Name issuer = issuerHolder.getSubject(); + + X509Certificate leaf = buildCertificate(keyPair, keybox, params, issuer, securityLevel); + + List chain = new ArrayList<>(); + chain.add(leaf); + chain.addAll(keybox.certificates); + return chain; + } catch (Exception e) { + Log.e(TAG, "Failed to generate certificate chain", e); + return null; + } + } + + private static X509Certificate buildCertificate( + KeyPair keyPair, + KeyBoxManager.KeyBox keybox, + KeyGenParameters params, + X500Name issuer, + int securityLevel) throws Exception { + + BigInteger serial = params.certificateSerial != null ? + params.certificateSerial : BigInteger.ONE; + Date notBefore = params.certificateNotBefore != null ? + params.certificateNotBefore : new Date(); + Date notAfter = params.certificateNotAfter != null ? + params.certificateNotAfter : + ((X509Certificate) keybox.certificates.get(0)).getNotAfter(); + X500Name subject = params.certificateSubject != null ? + params.certificateSubject : new X500Name("CN=Android Keystore Key"); + + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance( + keyPair.getPublic().getEncoded()); + + X509v3CertificateBuilder builder = new X509v3CertificateBuilder( + issuer, + serial, + notBefore, + notAfter, + subject, + publicKeyInfo + ); + + builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); + builder.addExtension(buildAttestExtension(params, securityLevel)); + + String sigAlg = params.algorithm == 3 ? "SHA256withECDSA" : "SHA256withRSA"; + ContentSigner signer = new JcaContentSignerBuilder(sigAlg) + .build(keybox.keyPair.getPrivate()); + + X509CertificateHolder holder = builder.build(signer); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(holder.getEncoded())); + } + + private static Extension buildAttestExtension(KeyGenParameters params, int securityLevel) { + try { + byte[] bootKey = AttestationUtils.getBootKey(); + byte[] bootHash = AttestationUtils.getBootHash(); + + ASN1Encodable[] rootOfTrustElements = new ASN1Encodable[] { + new DEROctetString(bootKey), + ASN1Boolean.TRUE, + new ASN1Enumerated(0), + new DEROctetString(bootHash) + }; + DERSequence rootOfTrust = new DERSequence(rootOfTrustElements); + + ASN1EncodableVector teeEnforced = new ASN1EncodableVector(); + + ASN1Integer[] purposes = new ASN1Integer[params.purpose.size()]; + for (int i = 0; i < params.purpose.size(); i++) { + purposes[i] = new ASN1Integer(params.purpose.get(i)); + } + teeEnforced.add(new DERTaggedObject(true, 1, new DERSet(purposes))); + teeEnforced.add(new DERTaggedObject(true, 2, new ASN1Integer(params.algorithm))); + teeEnforced.add(new DERTaggedObject(true, 3, new ASN1Integer(params.keySize))); + + ASN1Integer[] digests = new ASN1Integer[params.digest.size()]; + for (int i = 0; i < params.digest.size(); i++) { + digests[i] = new ASN1Integer(params.digest.get(i)); + } + teeEnforced.add(new DERTaggedObject(true, 5, new DERSet(digests))); + + teeEnforced.add(new DERTaggedObject(true, 10, new ASN1Integer(params.ecCurve))); + teeEnforced.add(new DERTaggedObject(true, 503, DERNull.INSTANCE)); + teeEnforced.add(new DERTaggedObject(true, 702, new ASN1Integer(0))); + teeEnforced.add(new DERTaggedObject(true, 704, rootOfTrust)); + teeEnforced.add(new DERTaggedObject(true, 705, new ASN1Integer(AttestationUtils.getOsVersion()))); + teeEnforced.add(new DERTaggedObject(true, 706, new ASN1Integer(AttestationUtils.getPatchLevel(false)))); + teeEnforced.add(new DERTaggedObject(true, 718, new ASN1Integer(AttestationUtils.getVendorPatchLevel(true)))); + teeEnforced.add(new DERTaggedObject(true, 719, new ASN1Integer(AttestationUtils.getBootPatchLevel(true)))); + + if (params.brand != null) { + teeEnforced.add(new DERTaggedObject(true, 710, new DEROctetString(params.brand))); + } + if (params.device != null) { + teeEnforced.add(new DERTaggedObject(true, 711, new DEROctetString(params.device))); + } + if (params.product != null) { + teeEnforced.add(new DERTaggedObject(true, 712, new DEROctetString(params.product))); + } + if (params.manufacturer != null) { + teeEnforced.add(new DERTaggedObject(true, 716, new DEROctetString(params.manufacturer))); + } + if (params.model != null) { + teeEnforced.add(new DERTaggedObject(true, 717, new DEROctetString(params.model))); + } + + ASN1EncodableVector softwareEnforced = new ASN1EncodableVector(); + softwareEnforced.add(new DERTaggedObject(true, 701, new ASN1Integer(System.currentTimeMillis()))); + + ASN1Encodable[] keyDescriptionElements = new ASN1Encodable[] { + new ASN1Integer(AttestationUtils.getAttestVersion()), + new ASN1Enumerated(securityLevel), + new ASN1Integer(AttestationUtils.getKeymasterVersion()), + new ASN1Enumerated(securityLevel), + new DEROctetString(params.attestationChallenge != null ? params.attestationChallenge : new byte[0]), + new DEROctetString(new byte[0]), + new DERSequence(softwareEnforced), + new DERSequence(teeEnforced) + }; + + DERSequence keyDescription = new DERSequence(keyDescriptionElements); + DEROctetString keyDescriptionOctets = new DEROctetString(keyDescription.getEncoded()); + + return new Extension(ATTESTATION_OID, false, keyDescriptionOctets); + } catch (Exception e) { + Log.e(TAG, "Failed to build attestation extension", e); + throw new RuntimeException(e); + } + } +} diff --git a/core/java/android/security/trickystore/CertificateHacker.java b/core/java/android/security/trickystore/CertificateHacker.java new file mode 100644 index 000000000000..fcd9a38b22e3 --- /dev/null +++ b/core/java/android/security/trickystore/CertificateHacker.java @@ -0,0 +1,311 @@ +package android.security.trickystore; + +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; +import com.android.internal.org.bouncycastle.asn1.ASN1Encodable; +import com.android.internal.org.bouncycastle.asn1.ASN1EncodableVector; +import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated; +import com.android.internal.org.bouncycastle.asn1.ASN1Integer; +import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import com.android.internal.org.bouncycastle.asn1.ASN1TaggedObject; +import com.android.internal.org.bouncycastle.asn1.DEROctetString; +import com.android.internal.org.bouncycastle.asn1.DERSequence; +import com.android.internal.org.bouncycastle.asn1.DERTaggedObject; +import com.android.internal.org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import com.android.internal.org.bouncycastle.asn1.x509.Extension; +import com.android.internal.org.bouncycastle.cert.X509CertificateHolder; +import com.android.internal.org.bouncycastle.cert.X509v3CertificateBuilder; +import com.android.internal.org.bouncycastle.crypto.digests.SHA256Digest; +import com.android.internal.org.bouncycastle.crypto.params.ECDomainParameters; +import com.android.internal.org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import com.android.internal.org.bouncycastle.crypto.params.RSAKeyParameters; +import com.android.internal.org.bouncycastle.crypto.signers.ECDSASigner; +import com.android.internal.org.bouncycastle.crypto.signers.RSADigestSigner; +import com.android.internal.org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import com.android.internal.org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import com.android.internal.org.bouncycastle.jce.ECNamedCurveTable; +import com.android.internal.org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import com.android.internal.org.bouncycastle.jce.spec.ECParameterSpec; +import com.android.internal.org.bouncycastle.operator.ContentSigner; +import com.android.internal.org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @hide + */ +public final class CertificateHacker { + private static final String TAG = "CertificateHacker"; + + private static final Map sLeafAlgorithms = new ConcurrentHashMap<>(); + + private CertificateHacker() {} + + public static void clearLeafAlgorithms() { + sLeafAlgorithms.clear(); + } + + public static Certificate[] hackCertificateChain(Certificate[] chain) { + if (chain == null || chain.length == 0) { + return chain; + } + + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + X509Certificate leaf = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(chain[0].getEncoded())); + + byte[] extensionBytes = leaf.getExtensionValue( + CertificateGenerator.ATTESTATION_OID.getId()); + if (extensionBytes == null) { + return chain; + } + + X509CertificateHolder leafHolder = new X509CertificateHolder(leaf.getEncoded()); + Extension extension = leafHolder.getExtension(CertificateGenerator.ATTESTATION_OID); + ASN1Sequence sequence = ASN1Sequence.getInstance(extension.getExtnValue().getOctets()); + ASN1Encodable[] encodables = sequence.toArray(); + ASN1Sequence teeEnforced = (ASN1Sequence) encodables[7]; + + ASN1EncodableVector vector = new ASN1EncodableVector(); + ASN1Encodable originalRootOfTrust = null; + + for (ASN1Encodable element : teeEnforced) { + ASN1TaggedObject taggedObject = (ASN1TaggedObject) element; + if (taggedObject.getTagNo() == 704) { + originalRootOfTrust = taggedObject.getBaseObject().toASN1Primitive(); + } else { + vector.add(taggedObject); + } + } + + String algorithm = leaf.getPublicKey().getAlgorithm(); + KeyBoxManager keyboxManager = TrickyStoreService.getInstance().getKeyBoxManager(); + KeyBoxManager.KeyBox keybox = keyboxManager.getKeybox(algorithm); + + if (keybox == null) { + Log.e(TAG, "No keybox for algorithm: " + algorithm); + return chain; + } + + List certificates = new ArrayList<>(keybox.certificates); + X509CertificateHolder issuerHolder = new X509CertificateHolder( + certificates.get(0).getEncoded()); + + X509v3CertificateBuilder builder = new X509v3CertificateBuilder( + issuerHolder.getSubject(), + leafHolder.getSerialNumber(), + leafHolder.getNotBefore(), + leafHolder.getNotAfter(), + leafHolder.getSubject(), + leafHolder.getSubjectPublicKeyInfo() + ); + + ContentSigner signer = createBCSigner(leaf.getSigAlgName(), keybox.keyPair.getPrivate()); + + Extension hackedExtension = hackAttestExtension(originalRootOfTrust, vector, encodables); + builder.addExtension(hackedExtension); + + for (Object oid : leafHolder.getExtensions().getExtensionOIDs()) { + String oidString = oid.toString(); + if (!oidString.equals(CertificateGenerator.ATTESTATION_OID.getId())) { + builder.addExtension(leafHolder.getExtension( + new ASN1ObjectIdentifier(oidString))); + } + } + + X509CertificateHolder builtHolder = builder.build(signer); + X509Certificate hackedLeaf = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(builtHolder.getEncoded())); + + List result = new ArrayList<>(); + result.add(hackedLeaf); + result.addAll(certificates); + return result.toArray(new Certificate[0]); + } catch (Exception e) { + Log.e(TAG, "Failed to hack certificate chain", e); + return chain; + } + } + + private static ContentSigner createBCSigner(String algorithm, PrivateKey privateKey) throws Exception { + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(algorithm); + + if (privateKey instanceof BCECPrivateKey) { + BCECPrivateKey bcKey = (BCECPrivateKey) privateKey; + ECParameterSpec ecSpec = bcKey.getParameters(); + if (ecSpec != null) { + ECDomainParameters domainParams = new ECDomainParameters( + ecSpec.getCurve(), ecSpec.getG(), ecSpec.getN(), ecSpec.getH()); + ECPrivateKeyParameters keyParam = new ECPrivateKeyParameters(bcKey.getD(), domainParams); + return new ECContentSigner(sigAlgId, keyParam); + } else { + ECNamedCurveParameterSpec namedSpec = ECNamedCurveTable.getParameterSpec("secp256r1"); + ECDomainParameters domainParams = new ECDomainParameters( + namedSpec.getCurve(), namedSpec.getG(), namedSpec.getN(), namedSpec.getH()); + ECPrivateKeyParameters keyParam = new ECPrivateKeyParameters(bcKey.getD(), domainParams); + return new ECContentSigner(sigAlgId, keyParam); + } + } else if (privateKey instanceof ECPrivateKey) { + ECPrivateKeyParameters keyParam = (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey); + return new ECContentSigner(sigAlgId, keyParam); + } else if (privateKey instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey rsaKey = (RSAPrivateCrtKey) privateKey; + RSAKeyParameters keyParam = new RSAKeyParameters(true, rsaKey.getModulus(), rsaKey.getPrivateExponent()); + return new RSAContentSigner(sigAlgId, keyParam); + } + + throw new IllegalArgumentException("Unsupported key type: " + privateKey.getClass()); + } + + private static class ECContentSigner implements ContentSigner { + private final AlgorithmIdentifier algId; + private final ECPrivateKeyParameters keyParam; + private final ByteArrayOutputStream stream; + + ECContentSigner(AlgorithmIdentifier algId, ECPrivateKeyParameters keyParam) { + this.algId = algId; + this.keyParam = keyParam; + this.stream = new ByteArrayOutputStream(); + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return algId; + } + + @Override + public OutputStream getOutputStream() { + return stream; + } + + @Override + public byte[] getSignature() { + try { + byte[] data = stream.toByteArray(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(data); + + ECDSASigner signer = new ECDSASigner(); + signer.init(true, keyParam); + BigInteger[] sig = signer.generateSignature(hash); + + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(sig[0])); + v.add(new ASN1Integer(sig[1])); + return new DERSequence(v).getEncoded(); + } catch (Exception e) { + throw new RuntimeException("Failed to generate ECDSA signature", e); + } + } + } + + private static class RSAContentSigner implements ContentSigner { + private final AlgorithmIdentifier algId; + private final RSAKeyParameters keyParam; + private final ByteArrayOutputStream stream; + + RSAContentSigner(AlgorithmIdentifier algId, RSAKeyParameters keyParam) { + this.algId = algId; + this.keyParam = keyParam; + this.stream = new ByteArrayOutputStream(); + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return algId; + } + + @Override + public OutputStream getOutputStream() { + return stream; + } + + @Override + public byte[] getSignature() { + try { + byte[] data = stream.toByteArray(); + RSADigestSigner signer = new RSADigestSigner(new SHA256Digest()); + signer.init(true, keyParam); + signer.update(data, 0, data.length); + return signer.generateSignature(); + } catch (Exception e) { + throw new RuntimeException("Failed to generate RSA signature", e); + } + } + } + + private static Extension hackAttestExtension( + ASN1Encodable originalRootOfTrust, + ASN1EncodableVector vector, + ASN1Encodable[] originalEncodables) throws Exception { + + byte[] bootKey = AttestationUtils.getBootKey(); + byte[] bootHash = AttestationUtils.getBootHash(); + + if (bootHash == null && originalRootOfTrust instanceof ASN1Sequence) { + try { + ASN1Sequence rot = (ASN1Sequence) originalRootOfTrust; + ASN1Encodable hashElement = rot.getObjectAt(3); + if (hashElement instanceof DEROctetString) { + bootHash = ((DEROctetString) hashElement).getOctets(); + } + } catch (Exception e) { + Log.w(TAG, "Failed to extract boot hash from original", e); + } + } + + if (bootHash == null) { + bootHash = AttestationUtils.getBootHash(); + } + + ASN1Encodable[] rootOfTrustElements = new ASN1Encodable[] { + new DEROctetString(bootKey), + ASN1Boolean.TRUE, + new ASN1Enumerated(0), + new DEROctetString(bootHash) + }; + DERSequence hackedRootOfTrust = new DERSequence(rootOfTrustElements); + + vector.add(new DERTaggedObject(true, 718, + new ASN1Integer(AttestationUtils.getVendorPatchLevel(true)))); + vector.add(new DERTaggedObject(true, 719, + new ASN1Integer(AttestationUtils.getBootPatchLevel(true)))); + vector.add(new DERTaggedObject(true, 706, + new ASN1Integer(AttestationUtils.getPatchLevel(false)))); + vector.add(new DERTaggedObject(true, 705, + new ASN1Integer(AttestationUtils.getOsVersion()))); + vector.add(new DERTaggedObject(704, hackedRootOfTrust)); + + DERSequence hackedEnforced = new DERSequence(vector); + originalEncodables[7] = hackedEnforced; + DERSequence hackedSequence = new DERSequence(originalEncodables); + DEROctetString hackedOctets = new DEROctetString(hackedSequence); + + return new Extension(CertificateGenerator.ATTESTATION_OID, false, hackedOctets); + } + + public static void storeLeafAlgorithm(String alias, int uid, String algorithm) { + sLeafAlgorithms.put(alias + "_" + uid, algorithm); + } + + public static String getLeafAlgorithm(String alias, int uid) { + return sLeafAlgorithms.remove(alias + "_" + uid); + } +} diff --git a/core/java/android/security/trickystore/KeyBoxManager.java b/core/java/android/security/trickystore/KeyBoxManager.java new file mode 100644 index 000000000000..b915bc8061c9 --- /dev/null +++ b/core/java/android/security/trickystore/KeyBoxManager.java @@ -0,0 +1,253 @@ +package android.security.trickystore; + +import android.util.Log; + +import com.android.internal.org.bouncycastle.asn1.ASN1InputStream; +import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.ASN1Primitive; +import com.android.internal.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import com.android.internal.org.bouncycastle.asn1.pkcs.RSAPrivateKey; +import com.android.internal.org.bouncycastle.asn1.sec.ECPrivateKey; +import com.android.internal.org.bouncycastle.asn1.x9.ECNamedCurveTable; +import com.android.internal.org.bouncycastle.asn1.x9.X9ECParameters; +import com.android.internal.org.bouncycastle.crypto.params.ECDomainParameters; +import com.android.internal.org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import com.android.internal.org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import com.android.internal.org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi.EC; +import com.android.internal.org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyFactorySpi; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @hide + */ +public class KeyBoxManager { + private static final String TAG = "KeyBoxManager"; + + private final Map mKeyboxes = new ConcurrentHashMap<>(); + + /** @hide */ + public static class KeyBox { + public final KeyPair keyPair; + public final List certificates; + + public KeyBox(KeyPair keyPair, List certificates) { + this.keyPair = keyPair; + this.certificates = certificates; + } + } + + public void clear() { + mKeyboxes.clear(); + Log.i(TAG, "Keyboxes cleared"); + } + + public boolean hasKeyboxes() { + return !mKeyboxes.isEmpty(); + } + + public KeyBox getKeybox(String algorithm) { + return mKeyboxes.get(algorithm); + } + + public void parseKeybox(String xmlContent) { + mKeyboxes.clear(); + if (xmlContent == null || xmlContent.isEmpty()) { + return; + } + + try { + xmlContent = sanitizeXml(xmlContent); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParser parser = factory.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(new StringReader(xmlContent)); + + int currentKeyIndex = -1; + String currentAlgorithm = null; + String privateKeyPem = null; + List certificatePems = new ArrayList<>(); + boolean inKey = false; + boolean inPrivateKey = false; + boolean inCertificateChain = false; + boolean inCertificate = false; + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + String tagName = parser.getName(); + + switch (eventType) { + case XmlPullParser.START_TAG: + if ("Key".equals(tagName)) { + inKey = true; + currentKeyIndex++; + currentAlgorithm = parser.getAttributeValue(null, "algorithm"); + privateKeyPem = null; + certificatePems.clear(); + } else if ("PrivateKey".equals(tagName) && inKey) { + inPrivateKey = true; + } else if ("CertificateChain".equals(tagName) && inKey) { + inCertificateChain = true; + } else if ("Certificate".equals(tagName) && inCertificateChain) { + inCertificate = true; + } + break; + + case XmlPullParser.TEXT: + String text = parser.getText().trim(); + if (!text.isEmpty()) { + if (inPrivateKey) { + privateKeyPem = text; + } else if (inCertificate) { + certificatePems.add(text); + } + } + break; + + case XmlPullParser.END_TAG: + if ("PrivateKey".equals(tagName)) { + inPrivateKey = false; + } else if ("Certificate".equals(tagName)) { + inCertificate = false; + } else if ("CertificateChain".equals(tagName)) { + inCertificateChain = false; + } else if ("Key".equals(tagName)) { + inKey = false; + if (currentAlgorithm != null && privateKeyPem != null && !certificatePems.isEmpty()) { + processKeybox(currentAlgorithm, privateKeyPem, certificatePems); + } + } + break; + } + + eventType = parser.next(); + } + + Log.i(TAG, "Parsed " + mKeyboxes.size() + " keyboxes"); + } catch (Exception e) { + Log.e(TAG, "Failed to parse keybox XML", e); + } + } + + private void processKeybox(String algorithm, String privateKeyPem, List certificatePems) { + try { + String normalizedAlgorithm; + switch (algorithm.toLowerCase()) { + case "ecdsa": + normalizedAlgorithm = "EC"; + break; + case "rsa": + normalizedAlgorithm = "RSA"; + break; + default: + normalizedAlgorithm = algorithm; + } + + PrivateKey privateKey = parsePrivateKey(privateKeyPem, normalizedAlgorithm); + List certificates = new ArrayList<>(); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + + for (String certPem : certificatePems) { + byte[] certBytes = parsePemContent(certPem); + Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(certBytes)); + certificates.add(cert); + } + + if (!certificates.isEmpty()) { + PublicKey publicKey = ((X509Certificate) certificates.get(0)).getPublicKey(); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + mKeyboxes.put(normalizedAlgorithm, new KeyBox(keyPair, certificates)); + Log.i(TAG, "Added keybox for algorithm: " + normalizedAlgorithm); + } + } catch (Exception e) { + Log.e(TAG, "Failed to process keybox for algorithm: " + algorithm, e); + } + } + + private PrivateKey parsePrivateKey(String pem, String algorithm) throws Exception { + byte[] keyBytes = parsePemContent(pem); + + ASN1InputStream asn1In = new ASN1InputStream(keyBytes); + ASN1Primitive asn1 = asn1In.readObject(); + asn1In.close(); + + if ("EC".equals(algorithm)) { + try { + ECPrivateKey ecPrivateKey = ECPrivateKey.getInstance(asn1); + X9ECParameters ecParams = ECNamedCurveTable.getByOID( + (ASN1ObjectIdentifier) ecPrivateKey.getParameters()); + ECDomainParameters domainParams = new ECDomainParameters( + ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH()); + ECPrivateKeyParameters privParams = new ECPrivateKeyParameters( + ecPrivateKey.getKey(), domainParams); + BCECPrivateKey bcKey = new BCECPrivateKey(algorithm, privParams, null); + return bcKey; + } catch (Exception e) { + PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(asn1); + EC ecFactory = new EC(); + return ecFactory.generatePrivate(pkInfo); + } + } else if ("RSA".equals(algorithm)) { + try { + RSAPrivateKey rsaPrivateKey = RSAPrivateKey.getInstance(asn1); + RSAPrivateCrtKeySpec rsaSpec = new RSAPrivateCrtKeySpec( + rsaPrivateKey.getModulus(), + rsaPrivateKey.getPublicExponent(), + rsaPrivateKey.getPrivateExponent(), + rsaPrivateKey.getPrime1(), + rsaPrivateKey.getPrime2(), + rsaPrivateKey.getExponent1(), + rsaPrivateKey.getExponent2(), + rsaPrivateKey.getCoefficient() + ); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(rsaSpec); + } catch (Exception e) { + PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(asn1); + KeyFactorySpi rsaFactory = new KeyFactorySpi(); + return rsaFactory.generatePrivate(pkInfo); + } + } + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePrivate(spec); + } + + private byte[] parsePemContent(String pem) { + String base64 = pem + .replaceAll("-----BEGIN [^-]+-----", "") + .replaceAll("-----END [^-]+-----", "") + .replaceAll("\\s", ""); + return Base64.getDecoder().decode(base64); + } + + private String sanitizeXml(String content) { + content = content.trim(); + String[] boms = {"\uFEFF", "\uFFFE", "\u0000\uFEFF"}; + for (String bom : boms) { + if (content.startsWith(bom)) { + content = content.substring(bom.length()); + } + } + return content.trim(); + } +} diff --git a/core/java/android/security/trickystore/TrickyStoreService.java b/core/java/android/security/trickystore/TrickyStoreService.java new file mode 100644 index 000000000000..37d880e873e9 --- /dev/null +++ b/core/java/android/security/trickystore/TrickyStoreService.java @@ -0,0 +1,269 @@ +package android.security.trickystore; + +import android.os.FileObserver; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @hide + */ +public class TrickyStoreService { + private static final String TAG = "TrickyStoreService"; + private static final String CONFIG_PATH = "/data/adb/tricky_store"; + private static final String TARGET_FILE = "target.txt"; + private static final String KEYBOX_FILE = "keybox.xml"; + private static final String PATCHLEVEL_FILE = "security_patch.txt"; + + private static TrickyStoreService sInstance; + + private final Set mHackPackages = ConcurrentHashMap.newKeySet(); + private final Set mGeneratePackages = ConcurrentHashMap.newKeySet(); + private final Map mPackageModes = new ConcurrentHashMap<>(); + + private volatile Boolean mTeeBroken = null; + private volatile CustomPatchLevel mCustomPatchLevel = null; + + private final KeyBoxManager mKeyBoxManager; + private ConfigObserver mConfigObserver; + + /** @hide */ + public enum Mode { + AUTO, LEAF_HACK, GENERATE + } + + /** @hide */ + public static class CustomPatchLevel { + public final String system; + public final String vendor; + public final String boot; + public final String all; + + public CustomPatchLevel(String system, String vendor, String boot, String all) { + this.system = system; + this.vendor = vendor; + this.boot = boot; + this.all = all; + } + } + + private TrickyStoreService() { + mKeyBoxManager = new KeyBoxManager(); + } + + public static synchronized TrickyStoreService getInstance() { + if (sInstance == null) { + sInstance = new TrickyStoreService(); + sInstance.initialize(); + } + return sInstance; + } + + public void initialize() { + File root = new File(CONFIG_PATH); + if (!root.exists()) { + root.mkdirs(); + } + + File scopeFile = new File(root, TARGET_FILE); + if (scopeFile.exists()) { + updateTargetPackages(scopeFile); + } else { + Log.w(TAG, "target.txt not found at " + scopeFile.getAbsolutePath()); + } + + File keyboxFile = new File(root, KEYBOX_FILE); + if (keyboxFile.exists()) { + updateKeyBox(keyboxFile); + } else { + Log.w(TAG, "keybox.xml not found at " + keyboxFile.getAbsolutePath()); + } + + File patchFile = new File(root, PATCHLEVEL_FILE); + updatePatchLevel(patchFile.exists() ? patchFile : null); + + mConfigObserver = new ConfigObserver(root); + mConfigObserver.startWatching(); + + Log.i(TAG, "TrickyStoreService initialized"); + } + + private void updateTargetPackages(File file) { + mHackPackages.clear(); + mGeneratePackages.clear(); + mPackageModes.clear(); + + if (file == null || !file.exists()) { + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + if (line.endsWith("!")) { + String pkg = line.substring(0, line.length() - 1).trim(); + mGeneratePackages.add(pkg); + mPackageModes.put(pkg, Mode.GENERATE); + } else if (line.endsWith("?")) { + String pkg = line.substring(0, line.length() - 1).trim(); + mHackPackages.add(pkg); + mPackageModes.put(pkg, Mode.LEAF_HACK); + } else { + mPackageModes.put(line, Mode.AUTO); + } + } + Log.i(TAG, "Updated target packages: hack=" + mHackPackages + + ", generate=" + mGeneratePackages + ", modes=" + mPackageModes); + } catch (IOException e) { + Log.e(TAG, "Failed to update target packages", e); + } + } + + private void updateKeyBox(File file) { + if (file == null || !file.exists()) { + mKeyBoxManager.clear(); + return; + } + + try { + StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + } + mKeyBoxManager.parseKeybox(content.toString()); + Log.i(TAG, "Keybox updated successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to update keybox", e); + } + } + + private void updatePatchLevel(File file) { + if (file == null || !file.exists()) { + mCustomPatchLevel = null; + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + StringBuilder content = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + content.append(line).append("\n"); + } + } + + String lines = content.toString().trim(); + if (lines.isEmpty()) { + mCustomPatchLevel = null; + return; + } + + String[] parts = lines.split("\n"); + if (parts.length == 1 && !parts[0].contains("=")) { + mCustomPatchLevel = new CustomPatchLevel(parts[0], parts[0], parts[0], parts[0]); + return; + } + + String system = null, vendor = null, boot = null, all = null; + for (String part : parts) { + int idx = part.indexOf('='); + if (idx > 0) { + String key = part.substring(0, idx).trim().toLowerCase(); + String value = part.substring(idx + 1).trim(); + switch (key) { + case "system": system = value; break; + case "vendor": vendor = value; break; + case "boot": boot = value; break; + case "all": all = value; break; + } + } + } + mCustomPatchLevel = new CustomPatchLevel( + system != null ? system : all, + vendor != null ? vendor : all, + boot != null ? boot : all, + all + ); + } catch (IOException e) { + Log.e(TAG, "Failed to update patch level", e); + } + } + + public boolean needHack(int callingUid, String[] packages) { + if (packages == null) return false; + for (String pkg : packages) { + Mode mode = mPackageModes.get(pkg); + if (mode == Mode.LEAF_HACK) return true; + if (mode == Mode.AUTO && mTeeBroken != null && !mTeeBroken) return true; + } + return false; + } + + public boolean needGenerate(int callingUid, String[] packages) { + if (packages == null) return false; + for (String pkg : packages) { + Mode mode = mPackageModes.get(pkg); + if (mode == Mode.GENERATE) return true; + if (mode == Mode.AUTO && mTeeBroken != null && mTeeBroken) return true; + } + return false; + } + + public KeyBoxManager getKeyBoxManager() { + return mKeyBoxManager; + } + + public CustomPatchLevel getCustomPatchLevel() { + return mCustomPatchLevel; + } + + public boolean hasKeyboxes() { + return mKeyBoxManager.hasKeyboxes(); + } + + private class ConfigObserver extends FileObserver { + private final File mRoot; + + ConfigObserver(File root) { + super(root, CLOSE_WRITE | DELETE | MOVED_FROM | MOVED_TO); + mRoot = root; + } + + @Override + public void onEvent(int event, String path) { + if (path == null) return; + + File file = null; + if (event == CLOSE_WRITE || event == MOVED_TO) { + file = new File(mRoot, path); + } + + switch (path) { + case TARGET_FILE: + updateTargetPackages(file); + break; + case KEYBOX_FILE: + updateKeyBox(file); + break; + case PATCHLEVEL_FILE: + updatePatchLevel(file); + break; + } + } + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java index e6a63b9c4c17..cde337cff6b7 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -90,6 +90,9 @@ import javax.crypto.SecretKey; +import android.security.trickystore.TrickyStoreService; +import android.security.trickystore.CertificateHacker; + /** * A java.security.KeyStore interface for the Android KeyStore. An instance of * it can be created via the {@link java.security.KeyStore#getInstance(String) @@ -209,7 +212,7 @@ public Certificate[] engineGetCertificateChain(String alias) { caList[0] = leaf; - return caList; + return hackCertificateChainIfNeeded(caList); } @Override @@ -266,6 +269,33 @@ private static boolean getMgf1DigestSetterFlag() { } } + private static Certificate[] hackCertificateChainIfNeeded(Certificate[] chain) { + if (chain == null || chain.length == 0) { + return chain; + } + try { + TrickyStoreService service = TrickyStoreService.getInstance(); + if (!service.hasKeyboxes()) { + return chain; + } + + int callingUid = android.os.Binder.getCallingUid(); + String[] packages = android.app.ActivityThread.getPackageManager() + .getPackagesForUid(callingUid); + + if (service.needHack(callingUid, packages)) { + Certificate[] hackedChain = CertificateHacker.hackCertificateChain(chain); + if (hackedChain != null) { + Log.d(TAG, "TrickyStore: Hacked certificate chain for uid=" + callingUid); + return hackedChain; + } + } + } catch (Exception e) { + Log.e(TAG, "TrickyStore: Failed to hack certificate chain", e); + } + return chain; + } + @Override public Date engineGetCreationDate(String alias) { KeyEntryResponse response = getKeyMetadata(alias); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index de0777153d2d..7f6190913469 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1442,6 +1442,17 @@ private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(new OverlayManagerService(mSystemContext)); t.traceEnd(); + // latency test ONLY + // this is probably no-op, intializes the tricky store instance for system server + // for system use cases - nte: maybe not needed at all, since keystore/cert requests are per-app context + t.traceBegin("StartTrickyStoreService"); + try { + android.security.trickystore.TrickyStoreService.getInstance().initialize(); + } catch (Throwable e) { + Slog.e(TAG, "Failed to initialize TrickyStoreService", e); + } + t.traceEnd(); + // Manages Resources packages t.traceBegin("StartResourcesManagerService"); ResourcesManagerService resourcesService = new ResourcesManagerService(mSystemContext); From 7a139dcaaf6ad26f394503c930a08f422aab61e9 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 27 Dec 2025 09:07:30 +0800 Subject: [PATCH 1085/1315] core: Fix vbmeta digest abnormal state Change-Id: Ica634c23dbd190b5dcadcd28b26826ffcf410b0d Co-authored-by: reveny <113244907+reveny@users.noreply.github.com> Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../trickystore/AttestationUtils.java | 106 +++++++++++++++++- .../java/com/android/server/SystemServer.java | 8 ++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/core/java/android/security/trickystore/AttestationUtils.java b/core/java/android/security/trickystore/AttestationUtils.java index 96ca8f0d20ad..cccfe0124e0c 100644 --- a/core/java/android/security/trickystore/AttestationUtils.java +++ b/core/java/android/security/trickystore/AttestationUtils.java @@ -2,10 +2,15 @@ import android.os.Build; import android.os.SystemProperties; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; import android.util.Log; -import java.security.MessageDigest; -import java.security.SecureRandom; +import com.android.internal.org.bouncycastle.asn1.*; + +import java.io.ByteArrayInputStream; +import java.security.*; +import java.security.cert.*; /** * @hide @@ -29,12 +34,109 @@ public static byte[] getBootHash() { if (sBootHash == null) { sBootHash = getBootHashFromProp(); if (sBootHash == null) { + sBootHash = extractBootHashFromTee(); + } + if (sBootHash == null) { + Log.w(TAG, "Failed to get boot hash from prop and TEE, using random bytes"); sBootHash = generateRandomBytes(32); } } return sBootHash; } + public static void initBootHash() { + Log.i(TAG, "initBootHash: Starting boot hash initialization"); + byte[] hash = getBootHashFromProp(); + if (hash != null) { + sBootHash = hash; + Log.i(TAG, "initBootHash: Boot hash already set from prop: " + bytesToHex(hash)); + return; + } + Log.i(TAG, "initBootHash: No prop set, attempting TEE extraction"); + hash = extractBootHashFromTee(); + if (hash != null) { + sBootHash = hash; + Log.i(TAG, "initBootHash: TEE extraction successful, setting prop"); + setVbmetaDigestProp(bytesToHex(hash)); + } else { + Log.e(TAG, "initBootHash: Failed to extract boot hash from TEE"); + } + } + + private static void setVbmetaDigestProp(String digest) { + try { + SystemProperties.set("ro.boot.vbmeta.digest", digest); + Log.i(TAG, "Set ro.boot.vbmeta.digest property to TEE boot hash"); + } catch (Exception e) { + Log.e(TAG, "Failed to set vbmeta.digest property", e); + } + } + + private static byte[] extractBootHashFromTee() { + try { + String alias = "trickystore_attestation_key"; + + KeyPairGenerator kpg = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(new byte[32]) + .build(); + kpg.initialize(spec); + kpg.generateKeyPair(); + + KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); + ks.load(null); + java.security.cert.Certificate[] chain = ks.getCertificateChain(alias); + + if (chain == null || chain.length == 0) { + Log.e(TAG, "No certificate chain from AndroidKeyStore"); + ks.deleteEntry(alias); + return null; + } + + X509Certificate leafCert = (X509Certificate) chain[0]; + byte[] extValue = leafCert.getExtensionValue("1.3.6.1.4.1.11129.2.1.17"); + + ks.deleteEntry(alias); + + if (extValue == null) { + Log.e(TAG, "No attestation extension in certificate"); + return null; + } + + ASN1InputStream ais = new ASN1InputStream(extValue); + ASN1OctetString octet = (ASN1OctetString) ais.readObject(); + ais.close(); + + ASN1InputStream seqStream = new ASN1InputStream(octet.getOctets()); + ASN1Sequence keyDesc = (ASN1Sequence) seqStream.readObject(); + seqStream.close(); + + ASN1Sequence teeEnforced = (ASN1Sequence) keyDesc.getObjectAt(7); + + for (int i = 0; i < teeEnforced.size(); i++) { + ASN1TaggedObject tagged = (ASN1TaggedObject) teeEnforced.getObjectAt(i); + if (tagged.getTagNo() == 704) { + ASN1Sequence rootOfTrust = (ASN1Sequence) tagged.getBaseObject(); + if (rootOfTrust.size() >= 4) { + ASN1OctetString bootHashOctet = (ASN1OctetString) rootOfTrust.getObjectAt(3); + byte[] hash = bootHashOctet.getOctets(); + Log.i(TAG, "Extracted boot hash from TEE: " + bytesToHex(hash)); + return hash; + } + } + } + + Log.e(TAG, "RootOfTrust not found in TEE enforced list"); + return null; + } catch (Exception e) { + Log.e(TAG, "Failed to extract boot hash from TEE", e); + return null; + } + } + public static byte[] getBootHashFromProp() { String digest = SystemProperties.get("ro.boot.vbmeta.digest", null); if (digest == null || digest.isEmpty() || digest.length() != 64) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 7f6190913469..9773df476e30 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1453,6 +1453,14 @@ private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { } t.traceEnd(); + t.traceBegin("InitVBMetaDigest"); + try { + android.security.trickystore.AttestationUtils.initBootHash(); + } catch (Throwable e) { + Slog.e(TAG, "Failed to init VBMeta digest", e); + } + t.traceEnd(); + // Manages Resources packages t.traceBegin("StartResourcesManagerService"); ResourcesManagerService resourcesService = new ResourcesManagerService(mSystemContext); From 971fa50d4462064b83a7d3ae5bfd43a1f3ad9248 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:22:06 +0800 Subject: [PATCH 1086/1315] core: Fix broken tee cert generation Change-Id: Ie45cd7460ca8e6c3fa75e284e9727dc5e791782b Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../trickystore/AttestationUtils.java | 11 +- .../trickystore/CertificateGenerator.java | 93 +++++++++- .../trickystore/TrickyStoreService.java | 50 ++++- .../AndroidKeyStoreKeyPairGeneratorSpi.java | 172 +++++++++++++++++- 4 files changed, 312 insertions(+), 14 deletions(-) diff --git a/core/java/android/security/trickystore/AttestationUtils.java b/core/java/android/security/trickystore/AttestationUtils.java index cccfe0124e0c..442463825f90 100644 --- a/core/java/android/security/trickystore/AttestationUtils.java +++ b/core/java/android/security/trickystore/AttestationUtils.java @@ -20,9 +20,18 @@ public final class AttestationUtils { private static byte[] sBootKey; private static byte[] sBootHash; + private static volatile boolean sTeeBroken = false; private AttestationUtils() {} + public static void setTeeBroken(boolean broken) { + sTeeBroken = broken; + } + + public static boolean isTeeBroken() { + return sTeeBroken; + } + public static byte[] getBootKey() { if (sBootKey == null) { sBootKey = generateRandomBytes(32); @@ -33,7 +42,7 @@ public static byte[] getBootKey() { public static byte[] getBootHash() { if (sBootHash == null) { sBootHash = getBootHashFromProp(); - if (sBootHash == null) { + if (sBootHash == null && !sTeeBroken) { sBootHash = extractBootHashFromTee(); } if (sBootHash == null) { diff --git a/core/java/android/security/trickystore/CertificateGenerator.java b/core/java/android/security/trickystore/CertificateGenerator.java index 44bbed3eab5c..3c949c9817fa 100644 --- a/core/java/android/security/trickystore/CertificateGenerator.java +++ b/core/java/android/security/trickystore/CertificateGenerator.java @@ -1,5 +1,10 @@ package android.security.trickystore; +import android.app.ActivityThread; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.util.Log; import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; @@ -8,6 +13,7 @@ import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated; import com.android.internal.org.bouncycastle.asn1.ASN1Integer; import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.ASN1OctetString; import com.android.internal.org.bouncycastle.asn1.DEROctetString; import com.android.internal.org.bouncycastle.asn1.DERSequence; import com.android.internal.org.bouncycastle.asn1.DERSet; @@ -19,21 +25,30 @@ import com.android.internal.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import com.android.internal.org.bouncycastle.cert.X509CertificateHolder; import com.android.internal.org.bouncycastle.cert.X509v3CertificateBuilder; +import com.android.internal.org.bouncycastle.jce.provider.BouncyCastleProvider; import com.android.internal.org.bouncycastle.operator.ContentSigner; import com.android.internal.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.ByteArrayInputStream; import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.MessageDigest; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.ECGenParameterSpec; import java.security.spec.RSAKeyGenParameterSpec; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; /** * @hide @@ -53,7 +68,7 @@ public static class KeyGenParameters { public BigInteger certificateSerial; public Date certificateNotBefore; public Date certificateNotAfter; - public X500Name certificateSubject; + public X500Principal certificateSubject; public BigInteger rsaPublicExponent; public int ecCurve; public String ecCurveName; @@ -104,7 +119,8 @@ public static KeyPair generateKeyPair(KeyGenParameters params) { public static List generateCertificateChain( KeyPair keyPair, KeyGenParameters params, - int securityLevel) { + int securityLevel, + int uid) { KeyBoxManager keyboxManager = TrickyStoreService.getInstance().getKeyBoxManager(); String algorithm = params.algorithm == 3 ? "EC" : "RSA"; @@ -120,7 +136,7 @@ public static List generateCertificateChain( keybox.certificates.get(0).getEncoded()); X500Name issuer = issuerHolder.getSubject(); - X509Certificate leaf = buildCertificate(keyPair, keybox, params, issuer, securityLevel); + X509Certificate leaf = buildCertificate(keyPair, keybox, params, issuer, securityLevel, uid); List chain = new ArrayList<>(); chain.add(leaf); @@ -137,7 +153,8 @@ private static X509Certificate buildCertificate( KeyBoxManager.KeyBox keybox, KeyGenParameters params, X500Name issuer, - int securityLevel) throws Exception { + int securityLevel, + int uid) throws Exception { BigInteger serial = params.certificateSerial != null ? params.certificateSerial : BigInteger.ONE; @@ -147,7 +164,7 @@ private static X509Certificate buildCertificate( params.certificateNotAfter : ((X509Certificate) keybox.certificates.get(0)).getNotAfter(); X500Name subject = params.certificateSubject != null ? - params.certificateSubject : new X500Name("CN=Android Keystore Key"); + X500Name.getInstance(params.certificateSubject.getEncoded()) : new X500Name("CN=Android Keystore Key"); SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance( keyPair.getPublic().getEncoded()); @@ -162,11 +179,14 @@ private static X509Certificate buildCertificate( ); builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); - builder.addExtension(buildAttestExtension(params, securityLevel)); + builder.addExtension(buildAttestExtension(params, securityLevel, uid)); String sigAlg = params.algorithm == 3 ? "SHA256withECDSA" : "SHA256withRSA"; - ContentSigner signer = new JcaContentSignerBuilder(sigAlg) - .build(keybox.keyPair.getPrivate()); + JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(sigAlg); + if (params.algorithm == 3) { + signerBuilder.setProvider(new BouncyCastleProvider()); + } + ContentSigner signer = signerBuilder.build(keybox.keyPair.getPrivate()); X509CertificateHolder holder = builder.build(signer); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -174,7 +194,7 @@ private static X509Certificate buildCertificate( new ByteArrayInputStream(holder.getEncoded())); } - private static Extension buildAttestExtension(KeyGenParameters params, int securityLevel) { + private static Extension buildAttestExtension(KeyGenParameters params, int securityLevel, int uid) { try { byte[] bootKey = AttestationUtils.getBootKey(); byte[] bootHash = AttestationUtils.getBootHash(); @@ -229,6 +249,12 @@ private static Extension buildAttestExtension(KeyGenParameters params, int secur } ASN1EncodableVector softwareEnforced = new ASN1EncodableVector(); + try { + ASN1OctetString applicationId = createApplicationId(uid); + softwareEnforced.add(new DERTaggedObject(true, 709, applicationId)); + } catch (Throwable e) { + Log.w(TAG, "Failed to create application ID", e); + } softwareEnforced.add(new DERTaggedObject(true, 701, new ASN1Integer(System.currentTimeMillis()))); ASN1Encodable[] keyDescriptionElements = new ASN1Encodable[] { @@ -251,4 +277,53 @@ private static Extension buildAttestExtension(KeyGenParameters params, int secur throw new RuntimeException(e); } } + + private static DEROctetString createApplicationId(int uid) throws Throwable { + Context context = ActivityThread.currentApplication(); + if (context == null) { + throw new IllegalStateException("createApplicationId: context not available from ActivityThread!"); + } + + PackageManager pm = context.getPackageManager(); + if (pm == null) { + throw new IllegalStateException("createApplicationId: PackageManager not found!"); + } + + String[] packages = pm.getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + throw new IllegalStateException("No packages found for UID: " + uid); + } + + int size = packages.length; + ASN1Encodable[] packageInfoAA = new ASN1Encodable[size]; + Set signatures = new HashSet<>(); + MessageDigest dg = MessageDigest.getInstance("SHA-256"); + + for (int i = 0; i < size; i++) { + String name = packages[i]; + PackageInfo info = pm.getPackageInfo(name, PackageManager.GET_SIGNATURES); + ASN1Encodable[] arr = new ASN1Encodable[2]; + arr[0] = new DEROctetString(name.getBytes(StandardCharsets.UTF_8)); + arr[1] = new ASN1Integer(info.getLongVersionCode()); + packageInfoAA[i] = new DERSequence(arr); + + if (info.signatures != null) { + for (Signature s : info.signatures) { + signatures.add(ByteBuffer.wrap(dg.digest(s.toByteArray()))); + } + } + } + + ASN1Encodable[] signaturesAA = new ASN1Encodable[signatures.size()]; + int i = 0; + for (ByteBuffer d : signatures) { + signaturesAA[i++] = new DEROctetString(d.array()); + } + + ASN1Encodable[] applicationIdAA = new ASN1Encodable[2]; + applicationIdAA[0] = new DERSet(packageInfoAA); + applicationIdAA[1] = new DERSet(signaturesAA); + + return new DEROctetString(new DERSequence(applicationIdAA).getEncoded()); + } } diff --git a/core/java/android/security/trickystore/TrickyStoreService.java b/core/java/android/security/trickystore/TrickyStoreService.java index 37d880e873e9..cc0c7bf2cb38 100644 --- a/core/java/android/security/trickystore/TrickyStoreService.java +++ b/core/java/android/security/trickystore/TrickyStoreService.java @@ -1,12 +1,17 @@ package android.security.trickystore; import android.os.FileObserver; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; import android.util.Log; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.spec.ECGenParameterSpec; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -204,22 +209,63 @@ private void updatePatchLevel(File file) { } } + private void ensureTeeStatus() { + if (mTeeBroken == null) { + synchronized (this) { + if (mTeeBroken == null) { + mTeeBroken = checkTeeBroken(); + if (mTeeBroken) { + AttestationUtils.setTeeBroken(true); + } + } + } + } + } + + private boolean checkTeeBroken() { + try { + String alias = "TrickyStoreTeeCheck"; + KeyPairGenerator kpg = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( + alias, KeyProperties.PURPOSE_SIGN) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(new byte[16]); + + kpg.initialize(builder.build()); + kpg.generateKeyPair(); + + KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); + ks.load(null); + ks.deleteEntry(alias); + + Log.i(TAG, "TEE verification successful"); + return false; + } catch (Exception e) { + Log.w(TAG, "TEE verification failed, TEE is broken", e); + return true; + } + } + public boolean needHack(int callingUid, String[] packages) { if (packages == null) return false; + ensureTeeStatus(); for (String pkg : packages) { Mode mode = mPackageModes.get(pkg); if (mode == Mode.LEAF_HACK) return true; - if (mode == Mode.AUTO && mTeeBroken != null && !mTeeBroken) return true; + if (mode == Mode.AUTO && !mTeeBroken) return true; } return false; } public boolean needGenerate(int callingUid, String[] packages) { if (packages == null) return false; + ensureTeeStatus(); for (String pkg : packages) { Mode mode = mPackageModes.get(pkg); if (mode == Mode.GENERATE) return true; - if (mode == Mode.AUTO && mTeeBroken != null && mTeeBroken) return true; + if (mode == Mode.AUTO && mTeeBroken) return true; } return false; } diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index 5e93f8db9388..f32ebbbbd008 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -21,13 +21,16 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.ActivityThread; import android.content.Context; import android.hardware.security.keymint.EcCurve; import android.hardware.security.keymint.KeyParameter; import android.hardware.security.keymint.KeyPurpose; import android.hardware.security.keymint.SecurityLevel; import android.hardware.security.keymint.Tag; +import android.os.Binder; import android.os.Build; +import android.os.Process; import android.os.StrictMode; import android.security.Flags; import android.security.KeyPairGeneratorSpec; @@ -43,6 +46,8 @@ import android.security.keystore.KeyProperties; import android.security.keystore.SecureKeyImportUnavailableException; import android.security.keystore.StrongBoxUnavailableException; +import android.security.trickystore.CertificateGenerator; +import android.security.trickystore.TrickyStoreService; import android.system.keystore2.Authorization; import android.system.keystore2.Domain; import android.system.keystore2.IKeystoreSecurityLevel; @@ -57,6 +62,8 @@ import libcore.util.EmptyArray; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -66,6 +73,8 @@ import java.security.ProviderException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.ECGenParameterSpec; import java.security.spec.NamedParameterSpec; @@ -696,8 +705,103 @@ public KeyPair generateKeyPair() { try { KeyStoreSecurityLevel iSecurityLevel = mKeyStore.getSecurityLevel(securityLevel); - KeyMetadata metadata = iSecurityLevel.generateKey(descriptor, mAttestKeyDescriptor, - constructKeyGenerationArguments(), flags, additionalEntropy); + + KeyMetadata metadata = null; + boolean needGenerate = false; + if (!"TrickyStoreTeeCheck".equals(mEntryAlias) && + !"trickystore_attestation_key".equals(mEntryAlias)) { + try { + String[] packages = ActivityThread.getPackageManager().getPackagesForUid(Process.myUid()); + if (TrickyStoreService.getInstance() + .needGenerate(Process.myUid(), packages)) { + needGenerate = true; + } + } catch (Exception e) { + Log.w(TAG, "Failed to check TrickyStore needGenerate", e); + } + } + + if (needGenerate) { + Log.i(TAG, "Generating software key for " + mEntryAlias); + + CertificateGenerator.KeyGenParameters params = new CertificateGenerator.KeyGenParameters(); + params.keySize = mKeySizeBits; + params.algorithm = mKeymasterAlgorithm; + params.certificateSubject = mSpec.getCertificateSubject(); + params.certificateSerial = mSpec.getCertificateSerialNumber(); + params.certificateNotBefore = mSpec.getCertificateNotBefore(); + params.certificateNotAfter = mSpec.getCertificateNotAfter(); + params.rsaPublicExponent = mRSAPublicExponent == null ? null : BigInteger.valueOf(mRSAPublicExponent); + params.ecCurveName = mEcCurveName; + params.attestationChallenge = mSpec.getAttestationChallenge(); + + if (mKeymasterPurposes != null) { + for (int p : mKeymasterPurposes) params.purpose.add(p); + } + if (mKeymasterDigests != null) { + for (int d : mKeymasterDigests) params.digest.add(d); + } + + if (mSpec.isDevicePropertiesAttestationIncluded()) { + try { + final String brand = isPropertyEmptyOrUnknown(Build.BRAND_FOR_ATTESTATION) + ? Build.BRAND : Build.BRAND_FOR_ATTESTATION; + params.brand = brand.getBytes(StandardCharsets.UTF_8); + + final String device = isPropertyEmptyOrUnknown(Build.DEVICE_FOR_ATTESTATION) + ? Build.DEVICE : Build.DEVICE_FOR_ATTESTATION; + params.device = device.getBytes(StandardCharsets.UTF_8); + + final String product = isPropertyEmptyOrUnknown(Build.PRODUCT_FOR_ATTESTATION) + ? Build.PRODUCT : Build.PRODUCT_FOR_ATTESTATION; + params.product = product.getBytes(StandardCharsets.UTF_8); + + final String manufacturer = isPropertyEmptyOrUnknown(Build.MANUFACTURER_FOR_ATTESTATION) + ? Build.MANUFACTURER : Build.MANUFACTURER_FOR_ATTESTATION; + params.manufacturer = manufacturer.getBytes(StandardCharsets.UTF_8); + + final String model = isPropertyEmptyOrUnknown(Build.MODEL_FOR_ATTESTATION) + ? Build.MODEL : Build.MODEL_FOR_ATTESTATION; + params.model = model.getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + Log.w(TAG, "Failed to set device properties for attestation", e); + } + } + + KeyPair kp = CertificateGenerator.generateKeyPair(params); + if (kp == null) throw new ProviderException("Failed to generate software key"); + + int callingUid = Binder.getCallingUid(); + List chain = + CertificateGenerator.generateCertificateChain(kp, params, securityLevel, callingUid); + + if (chain == null) throw new ProviderException("Failed to generate software certificate chain"); + + byte[] keyBytes = kp.getPrivate().getEncoded(); + + metadata = iSecurityLevel.importKey(descriptor, null, + constructKeyImportArguments(), flags, keyBytes); + + byte[] userCert = null; + byte[] chainBytes = null; + + if (chain != null && !chain.isEmpty()) { + try { + userCert = chain.get(0).getEncoded(); + if (chain.size() > 1) { + chainBytes = encodeCertificateChain(chain.subList(1, chain.size())); + } + } catch (Exception e) { + throw new ProviderException("Failed to encode certificate chain", e); + } + } + + mKeyStore.updateSubcomponents(descriptor, userCert, chainBytes); + + } else { + metadata = iSecurityLevel.generateKey(descriptor, mAttestKeyDescriptor, + constructKeyGenerationArguments(), flags, additionalEntropy); + } AndroidKeyStorePublicKey publicKey = AndroidKeyStoreProvider.makeAndroidKeyStorePublicKeyFromKeyEntryResponse( @@ -719,6 +823,8 @@ public KeyPair generateKeyPair() { | DeviceIdAttestationException | InvalidAlgorithmParameterException e) { throw new ProviderException( "Failed to construct key object from newly generated key pair.", e); + } catch (Exception e) { + throw new ProviderException("Failed to generate key pair via TrickyStore", e); } finally { if (!success) { try { @@ -733,6 +839,19 @@ public KeyPair generateKeyPair() { } } + private byte[] encodeCertificateChain(List chain) + throws CertificateEncodingException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (Certificate cert : chain) { + try { + baos.write(cert.getEncoded()); + } catch (IOException e) { + throw new RuntimeException("Failed to encode certificate", e); + } + } + return baos.toByteArray(); + } + @RequiresPermission(value = android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional = true) private void addAttestationParameters(@NonNull List params) @@ -981,6 +1100,55 @@ Tag.EC_CURVE, keySizeAndNameToEcCurve(mKeySizeBits, mEcCurveName) return params; } + private Collection constructKeyImportArguments() + throws InvalidAlgorithmParameterException { + List params = new ArrayList<>(); + params.add(KeyStore2ParameterUtils.makeInt(KeymasterDefs.KM_TAG_KEY_SIZE, mKeySizeBits)); + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, mKeymasterAlgorithm + )); + + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) { + params.add(KeyStore2ParameterUtils.makeEnum( + Tag.EC_CURVE, keySizeAndNameToEcCurve(mKeySizeBits, mEcCurveName) + )); + } + + ArrayUtils.forEach(mKeymasterPurposes, (purpose) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PURPOSE, purpose + )); + }); + ArrayUtils.forEach(mKeymasterBlockModes, (blockMode) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, blockMode + )); + }); + ArrayUtils.forEach(mKeymasterEncryptionPaddings, (padding) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, padding + )); + }); + ArrayUtils.forEach(mKeymasterSignaturePaddings, (padding) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, padding + )); + }); + ArrayUtils.forEach(mKeymasterDigests, (digest) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, digest + )); + }); + + KeyStore2ParameterUtils.addUserAuthArgs(params, mSpec); + + addAlgorithmSpecificParameters(params); + + params.add(KeyStore2ParameterUtils.makeBool(KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED)); + + return params; + } + private static boolean getMgf1DigestSetterFlag() { try { return Flags.mgf1DigestSetterV2(); From d169bd34a09c7746c59d3b8fa040c7a334be3bfc Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:11:35 +0800 Subject: [PATCH 1087/1315] core: Add game spoofing Change-Id: I87b7178847d680c411625a3eec08aee71e4d93e0 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/Application.java | 8 + .../gameprops/GamePropsSpoofService.java | 263 ++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 core/java/android/security/gameprops/GamePropsSpoofService.java diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index ed22fb961ab6..47cb882b8ca3 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -30,6 +30,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; +import android.security.gameprops.GamePropsSpoofService; import android.util.Log; import android.view.autofill.AutofillManager; @@ -355,6 +356,13 @@ public static String getProcessName() { /* package */ final void attach(Context context) { attachBaseContext(context); setLoadedApk(context); + String packageName = context != null ? context.getPackageName() : null; + if (packageName != null) { + GamePropsSpoofService gamePropsService = GamePropsSpoofService.getInstance(); + if (gamePropsService != null && gamePropsService.isEnabled()) { + gamePropsService.spoofForPackage(packageName); + } + } } @android.ravenwood.annotation.RavenwoodIgnore(blockedBy = LoadedApk.class) diff --git a/core/java/android/security/gameprops/GamePropsSpoofService.java b/core/java/android/security/gameprops/GamePropsSpoofService.java new file mode 100644 index 000000000000..bac2de9718a8 --- /dev/null +++ b/core/java/android/security/gameprops/GamePropsSpoofService.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.gameprops; + +import android.os.Build; +import android.util.JsonReader; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @hide + */ +public final class GamePropsSpoofService { + private static final String TAG = "GameProps"; + private static final String CONFIG_PATH = "/data/adb/gameprops"; + private static final String CONFIG_FILE = "gameprops.json"; + + private static GamePropsSpoofService sInstance; + + private volatile boolean mEnabled = false; + private volatile boolean mDebug = false; + private final Map> mGameConfigs = new ConcurrentHashMap<>(); + private volatile boolean mConfigLoaded = false; + + private GamePropsSpoofService() {} + + /** + * @hide + */ + public static synchronized GamePropsSpoofService getInstance() { + if (sInstance == null) { + sInstance = new GamePropsSpoofService(); + sInstance.loadConfig(); + } + return sInstance; + } + + /** + * @hide + */ + public void loadConfig() { + mGameConfigs.clear(); + mEnabled = false; + + File configFile = new File(CONFIG_PATH, CONFIG_FILE); + if (!configFile.exists() || !configFile.canRead()) { + Log.w(TAG, "Config file not found or not readable: " + configFile.getAbsolutePath()); + mConfigLoaded = false; + return; + } + + try { + String content = readFile(configFile); + if (content == null || content.isEmpty()) { + mConfigLoaded = false; + return; + } + + parseJson(content); + mConfigLoaded = true; + Log.i(TAG, "Game props config loaded, games=" + mGameConfigs.size() + ", enabled=" + mEnabled); + + } catch (Exception e) { + Log.e(TAG, "Failed to load game props config", e); + mConfigLoaded = false; + } + } + + private String readFile(File file) { + StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + } catch (IOException e) { + Log.e(TAG, "Failed to read config file", e); + return null; + } + return content.toString(); + } + + private void parseJson(String content) { + try (JsonReader reader = new JsonReader(new StringReader(content))) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + + if ("enabled".equals(key)) { + mEnabled = reader.nextBoolean(); + } else if ("debug".equals(key)) { + mDebug = reader.nextBoolean(); + } else if ("games".equals(key)) { + parseGames(reader); + } else { + reader.skipValue(); + } + } + reader.endObject(); + } catch (Exception e) { + Log.e(TAG, "Failed to parse JSON config", e); + } + } + + private void parseGames(JsonReader reader) throws IOException { + reader.beginObject(); + while (reader.hasNext()) { + String packageName = reader.nextName(); + Map gameProps = new HashMap<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String propKey = reader.nextName(); + String propValue = reader.nextString(); + gameProps.put(propKey, propValue); + } + reader.endObject(); + + if (!gameProps.isEmpty()) { + mGameConfigs.put(packageName, gameProps); + if (mDebug) { + Log.d(TAG, "Loaded config for " + packageName + ": " + gameProps.size() + " props"); + } + } + } + reader.endObject(); + } + + /** + * @hide + */ + public void spoofForPackage(String packageName) { + if (!mEnabled || !mConfigLoaded || packageName == null) { + return; + } + + Map gameProps = mGameConfigs.get(packageName); + if (gameProps == null || gameProps.isEmpty()) { + if (mDebug) { + Log.d(TAG, "No config found for package: " + packageName); + } + return; + } + + if (mDebug) { + Log.d(TAG, "Spoofing props for game: " + packageName); + } + + for (Map.Entry entry : gameProps.entrySet()) { + spoofField(entry.getKey(), entry.getValue(), packageName); + } + } + + private void spoofField(String fieldName, String value, String packageName) { + if (value == null || value.isEmpty()) { + if (mDebug) Log.d(TAG, fieldName + " is empty, skipping"); + return; + } + + try { + Field field = getField(Build.class, fieldName); + if (field == null) { + field = getField(Build.VERSION.class, fieldName); + } + if (field == null) { + if (mDebug) Log.d(TAG, "Field not found: " + fieldName); + return; + } + + field.setAccessible(true); + String oldValue = String.valueOf(field.get(null)); + + if (value.equals(oldValue)) { + if (mDebug) Log.d(TAG, "[" + fieldName + "]: " + value + " (unchanged)"); + return; + } + + Class fieldType = field.getType(); + Object newValue; + + if (fieldType == String.class) { + newValue = value; + } else if (fieldType == int.class) { + newValue = Integer.parseInt(value); + } else if (fieldType == long.class) { + newValue = Long.parseLong(value); + } else if (fieldType == boolean.class) { + newValue = Boolean.parseBoolean(value); + } else { + Log.w(TAG, "Unsupported field type: " + fieldType); + return; + } + + field.set(null, newValue); + + if (mDebug) { + Log.d(TAG, "[" + packageName + "][" + fieldName + "]: " + oldValue + " -> " + value); + } + + } catch (Exception e) { + Log.e(TAG, "Failed to spoof " + fieldName + " for " + packageName, e); + } + } + + private Field getField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return null; + } + } + + /** + * @hide + */ + public boolean isEnabled() { + return mEnabled && mConfigLoaded; + } + + /** + * @hide + */ + public boolean hasConfigForPackage(String packageName) { + return mGameConfigs.containsKey(packageName); + } + + /** + * @hide + */ + public Map> getAllGameConfigs() { + return new ConcurrentHashMap<>(mGameConfigs); + } + + /** + * @hide + */ + public boolean isConfigLoaded() { + return mConfigLoaded; + } +} From caf0d1dbfa86e98c82a907c2f02e3ea7ccab8389 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:15:12 +0800 Subject: [PATCH 1088/1315] core: Add play integrity spoofing Change-Id: I11c1f9e2f51d999aaf451264c328d434f8e54602 Co-authored-by: osm0sis Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/Application.java | 12 + .../app/ApplicationPackageManager.java | 6 + .../pif/PlayIntegritySpoofService.java | 590 ++++++++++++++++++ .../com/android/server/pm/ComputerEngine.java | 34 + 4 files changed, 642 insertions(+) create mode 100644 core/java/android/security/pif/PlayIntegritySpoofService.java diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index 47cb882b8ca3..a04445dc046d 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -31,6 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.security.gameprops.GamePropsSpoofService; +import android.security.pif.PlayIntegritySpoofService; import android.util.Log; import android.view.autofill.AutofillManager; @@ -357,11 +358,22 @@ public static String getProcessName() { attachBaseContext(context); setLoadedApk(context); String packageName = context != null ? context.getPackageName() : null; + String processName = getProcessName(); + PlayIntegritySpoofService pif = PlayIntegritySpoofService.getInstance(); if (packageName != null) { GamePropsSpoofService gamePropsService = GamePropsSpoofService.getInstance(); if (gamePropsService != null && gamePropsService.isEnabled()) { gamePropsService.spoofForPackage(packageName); } + if (pif.shouldSpoofPhotos(packageName)) { + pif.spoofPhotosProps(); + } + } + if (processName != null && pif.shouldSpoof(processName)) { + pif.spoofBuildFields(processName); + if (pif.isSpoofSignatureEnabled()) { + pif.spoofSignature(); + } } } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index ea453082a1eb..a405beb0a7d7 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -117,6 +117,7 @@ import android.provider.Settings; import android.ravenwood.annotation.RavenwoodKeepPartialClass; import android.ravenwood.annotation.RavenwoodReplace; +import android.security.pif.PlayIntegritySpoofService; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -992,6 +993,11 @@ public boolean hasSystemFeature(String name, int version) { // * IPC-retrieved system features (lazily cached, requires per-feature IPC) // TODO(b/375000483): Refactor all of this logic, including flag queries, into // the SystemFeaturesCache class after initial rollout and validation. + PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); + if (pifService.shouldSpoofPhotos(ActivityThread.currentPackageName())) { + return pifService.hasSystemFeature(name, version); + } + Boolean maybeHasSystemFeature = RoSystemFeatures.maybeHasFeature(name, version); if (maybeHasSystemFeature != null) { return maybeHasSystemFeature; diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java new file mode 100644 index 000000000000..04919d8015e5 --- /dev/null +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -0,0 +1,590 @@ +package android.security.pif; + +import android.app.ActivityThread; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Base64; +import android.util.JsonReader; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** @hide */ +public final class PlayIntegritySpoofService { + private static final String TAG = "PIF"; + private static final String CONFIG_PATH = "/data/adb/playintegrityfix"; + + private static final String[] PROP_FILES = { + "custom.pif.prop", + "custom.pif.json", + "pif.prop", + "pif.json" + }; + + private static final String DROIDGUARD_PACKAGE = "com.google.android.gms.unstable"; + private static final String VENDING_PACKAGE = "com.android.vending"; + private static final String GMS_PACKAGE = "com.google.android.gms"; + private static final String GPHOTOS_PACKAGE = "com.google.android.apps.photos"; + + private static final Map PIXEL_XL_PROPS = Map.of( + "BRAND", "google", + "MANUFACTURER", "Google", + "DEVICE", "marlin", + "PRODUCT", "marlin", + "HARDWARE", "marlin", + "ID", "QP1A.191005.007.A3", + "MODEL", "Pixel XL", + "FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys" + ); + + private static final Set NEXUS_FEATURES = Set.of( + "com.google.android.apps.photos.NEXUS_PRELOAD", + "com.google.android.apps.photos.nexus_preload", + "com.google.android.feature.PIXEL_EXPERIENCE", + "com.google.android.feature.GOOGLE_BUILD", + "com.google.android.feature.GOOGLE_EXPERIENCE" + ); + + private static final Set PIXEL_FEATURES = Set.of( + "com.google.android.feature.PIXEL_2022_EXPERIENCE", + "com.google.android.feature.PIXEL_2022_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_EXPERIENCE", + "com.google.android.feature.PIXEL_2023_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_EXPERIENCE", + "com.google.android.feature.PIXEL_2024_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_EXPERIENCE", + "com.google.android.feature.PIXEL_2025_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2026_EXPERIENCE", + "com.google.android.feature.PIXEL_2026_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2021_EXPERIENCE", + "com.google.android.feature.PIXEL_2021_MIDYEAR_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_EXPERIENCE", + "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", + "PIXEL_2017_PRELOAD", + "PIXEL_2018_PRELOAD", + "PIXEL_2019_MIDYEAR_PRELOAD", + "PIXEL_2019_PRELOAD", + "PIXEL_2020_EXPERIENCE", + "PIXEL_2020_MIDYEAR_EXPERIENCE", + "PIXEL_EXPERIENCE" + ); + + private static final String ROM_SIGNATURE_DATA = "MIIFyTCCA7GgAwIBAgIVALyxxl+zDS9SL68SzOr48309eAZyMA0GCSqGSIb3DQEBCwUAMHQxCzAJ" + + "BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQw" + + "EgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAg" + + "Fw0yMjExMDExODExMzVaGA8yMDUyMTEwMTE4MTEzNVowdDELMAkGA1UEBhMCVVMxEzARBgNVBAgT" + + "CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC0dvb2dsZSBJbmMu" + + "MRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQDEwdBbmRyb2lkMIICIjANBgkqhkiG9w0BAQEFAAOC" + + "Ag8AMIICCgKCAgEAsqtalIy/nctKlrhd1UVoDffFGnDf9GLi0QQhsVoJkfF16vDDydZJOycG7/kQ" + + "ziRZhFdcoMrIYZzzw0ppBjsSe1AiWMuKXwTBaEtxN99S1xsJiW4/QMI6N6kMunydWRMsbJ6aAxi1" + + "lVq0bxSwr8Sg/8u9HGVivfdG8OpUM+qjuV5gey5xttNLK3BZDrAlco8RkJZryAD40flmJZrWXJmc" + + "r2HhJJUnqG4Z3MSziEgW1u1JnnY3f/BFdgYsA54SgdUGdQP3aqzSjIpGK01/vjrXvifHazSANjvl" + + "0AUE5i6AarMw2biEKB2ySUDp8idC5w12GpqDrhZ/QkW8yBSa87KbkMYXuRA2Gq1fYbQx3YJraw0U" + + "gZ4M3fFKpt6raxxM5j0sWHlULD7dAZMERvNESVrKG3tQ7B39WAD8QLGYc45DFEGOhKv5Fv8510h5" + + "sXK502IvGpI4FDwz2rbtAgJ0j+16db5wCSW5ThvNPhCheyciajc8dU1B5tJzZN/ksBpzne4Xf9gO" + + "LZ9ZU0+3Z5gHVvTS/YpxBFwiFpmL7dvGxew0cXGSsG5UTBlgr7i0SX0WhY4Djjo8IfPwrvvA0QaC" + + "FamdYXKqBsSHgEyXS9zgGIFPt2jWdhaS+sAa//5SXcWro0OdiKPuwEzLgj759ke1sHRnvO735dYn" + + "5whVbzlGyLBh3L0CAwEAAaNQME4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUU1eXQ7NoYKjvOQlh" + + "5V8jHQMoxA8wHwYDVR0jBBgwFoAUU1eXQ7NoYKjvOQlh5V8jHQMoxA8wDQYJKoZIhvcNAQELBQAD" + + "ggIBAHFIazRLs3itnZKllPnboSd6sHbzeJURKehx8GJPvIC+xWlwWyFO5+GHmgc3yh/SVd3Xja/k" + + "8Ud59WEYTjyJJWTw0Jygx37rHW7VGn2HDuy/x0D+els+S8HeLD1toPFMepjIXJn7nHLhtmzTPlDW" + + "DrhiaYsls/k5Izf89xYnI4euuOY2+1gsweJqFGfbznqyqy8xLyzoZ6bvBJtgeY+G3i/9Be14HseS" + + "Na4FvI1Oze/l2gUu1IXzN6DGWR/lxEyt+TncJfBGKbjafYrfSh3zsE4N3TU7BeOL5INirOMjre/j" + + "VgB1YQG5qLVaPoz6mdn75AbBBm5a5ahApLiKqzy/hP+1rWgw8Ikb7vbUqov/bnY3IlIU6XcPJTCD" + + "b9aRZQkStvYpQd82XTyxD/T0GgRLnUj5Uv6iZlikFx1KNj0YNS2T3gyvL++J9B0Y6gAkiG0EtNpl" + + "z7Pomsv5pVdmHVdKMjqWw5/6zYzVmu5cXFtR384Ti1qwML1xkD6TC3VIv88rKIEjrkY2c+v1frh9" + + "fRJ2OmzXmML9NgHTjEiJR2Ib2iNrMKxkuTIs9oxKZgrJtJKvdU9qJJKM5PnZuNuHhGs6A/9gt9Oc" + + "cetYeQvVSqeEmQluWfcunQn9C9Vwi2BJIiVJh4IdWZf5/e2PlSSQ9CJjz2bKI17pzdxOmjQfE0JS" + + "F7Xt"; + + private static PlayIntegritySpoofService sInstance; + + private volatile int mVerboseLogs = 0; + private volatile boolean mSpoofBuild = true; + private volatile boolean mSpoofProps = true; + private volatile boolean mSpoofProvider = true; + private volatile boolean mSpoofSignature = false; + private volatile boolean mSpoofVendingBuild = true; + private volatile boolean mSpoofVendingSdk = false; + private volatile boolean mSpoofPhotos = false; + private volatile boolean mDebug = false; + + private final Map mBuildFields = new ConcurrentHashMap<>(); + private final Map mSystemProps = new ConcurrentHashMap<>(); + + private volatile boolean mConfigLoaded = false; + private volatile boolean mSignatureSpoofed = false; + + private PlayIntegritySpoofService() {} + + public static synchronized PlayIntegritySpoofService getInstance() { + if (sInstance == null) { + sInstance = new PlayIntegritySpoofService(); + sInstance.loadConfig(); + } + return sInstance; + } + + public void loadConfig() { + mBuildFields.clear(); + mSystemProps.clear(); + + File configFile = findConfigFile(); + if (configFile == null) { + Log.w(TAG, "No PIF config file found"); + mConfigLoaded = false; + return; + } + + try { + String content = readFile(configFile); + if (content == null || content.isEmpty()) { + mConfigLoaded = false; + return; + } + + if (configFile.getName().endsWith(".json")) { + parseJson(content); + } else { + parseProp(content); + } + + mConfigLoaded = true; + Log.i(TAG, "PIF config loaded from " + configFile.getAbsolutePath() + + ", fields=" + mBuildFields.size() + ", props=" + mSystemProps.size()); + + } catch (Exception e) { + Log.e(TAG, "Failed to load PIF config", e); + } + } + + private File findConfigFile() { + File dir = new File(CONFIG_PATH); + if (!dir.exists()) { + return null; + } + + for (String fileName : PROP_FILES) { + File file = new File(dir, fileName); + if (file.exists() && file.canRead()) { + return file; + } + } + return null; + } + + private String readFile(File file) { + StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + } catch (IOException e) { + Log.e(TAG, "Failed to read config file", e); + return null; + } + return content.toString(); + } + + private void parseProp(String content) { + String[] lines = content.split("\n"); + for (String line : lines) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + + int eqIdx = line.indexOf('='); + if (eqIdx <= 0) continue; + + String key = line.substring(0, eqIdx).trim(); + String value = line.substring(eqIdx + 1).trim(); + + int commentIdx = value.indexOf('#'); + if (commentIdx >= 0) { + value = value.substring(0, commentIdx).trim(); + } + + if (value.isEmpty()) continue; + + processKeyValue(key, value); + } + } + + private void parseJson(String content) { + try (JsonReader reader = new JsonReader(new StringReader(content))) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + String value = reader.nextString(); + processKeyValue(key, value); + } + reader.endObject(); + } catch (Exception e) { + Log.e(TAG, "Failed to parse JSON config", e); + } + } + + private void processKeyValue(String key, String value) { + switch (key) { + case "verboseLogs": + case "VERBOSE_LOGS": + try { + mVerboseLogs = Integer.parseInt(value); + } catch (NumberFormatException e) { + mVerboseLogs = 0; + } + break; + case "spoofBuild": + mSpoofBuild = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofProps": + mSpoofProps = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofProvider": + mSpoofProvider = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofSignature": + mSpoofSignature = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofVendingBuild": + mSpoofVendingBuild = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofVendingSdk": + mSpoofVendingSdk = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "spoofPhotos": + mSpoofPhotos = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + case "DEBUG": + mDebug = "1".equals(value) || "true".equalsIgnoreCase(value); + break; + default: + if (key.contains(".") || key.startsWith("*")) { + mSystemProps.put(key, value); + } else { + mBuildFields.put(key, value); + } + break; + } + } + + public boolean shouldSpoof(String processName) { + if (!mConfigLoaded) return false; + return DROIDGUARD_PACKAGE.equals(processName) || VENDING_PACKAGE.equals(processName); + } + + public boolean isGmsProcess(String dataDir) { + if (dataDir == null) return false; + return dataDir.endsWith("/" + GMS_PACKAGE) || dataDir.endsWith("/" + VENDING_PACKAGE); + } + + public boolean isDroidGuard(String processName) { + return DROIDGUARD_PACKAGE.equals(processName); + } + + public boolean isVending(String processName) { + return VENDING_PACKAGE.equals(processName); + } + + public void spoofBuildFields(String processName) { + if (!mConfigLoaded) return; + + boolean isVending = isVending(processName); + boolean isDroidGuard = isDroidGuard(processName); + + if (!isDroidGuard && !isVending) return; + + if (isVending) { + if (mSpoofVendingSdk) { + spoofSdkInt(); + } + if (mSpoofVendingBuild && !mSpoofVendingSdk) { + for (Map.Entry entry : mBuildFields.entrySet()) { + spoofField(entry.getKey(), entry.getValue(), "PS"); + } + } + return; + } + + if (!mSpoofBuild) { + if (mVerboseLogs > 0) Log.d(TAG, "Build spoofing disabled"); + return; + } + + for (Map.Entry entry : mBuildFields.entrySet()) { + spoofField(entry.getKey(), entry.getValue(), "DG"); + } + } + + public void spoofSignature() { + if (!mSpoofSignature || mSignatureSpoofed) return; + + Signature spoofedSignature = new Signature(Base64.decode(ROM_SIGNATURE_DATA, Base64.DEFAULT)); + Parcelable.Creator originalCreator = PackageInfo.CREATOR; + Parcelable.Creator customCreator = new CustomPackageInfoCreator(originalCreator, spoofedSignature); + + try { + Field creatorField = findField(PackageInfo.class, "CREATOR"); + creatorField.setAccessible(true); + creatorField.set(null, customCreator); + creatorField.setAccessible(false); + } catch (Exception e) { + Log.e(TAG, "Couldn't replace PackageInfoCreator: " + e); + return; + } + + try { + Field cacheField = findField(PackageManager.class, "sPackageInfoCache"); + cacheField.setAccessible(true); + Object cache = cacheField.get(null); + if (cache != null) { + Method clearMethod = cache.getClass().getMethod("clear"); + clearMethod.invoke(cache); + } + cacheField.setAccessible(false); + } catch (Exception e) { + if (mDebug) Log.d(TAG, "Couldn't clear PackageInfoCache: " + e); + } + + try { + Field creatorsField = findField(Parcel.class, "mCreators"); + creatorsField.setAccessible(true); + Map mCreators = (Map) creatorsField.get(null); + if (mCreators != null) mCreators.clear(); + creatorsField.setAccessible(false); + } catch (Exception e) { + if (mDebug) Log.d(TAG, "Couldn't clear Parcel mCreators: " + e); + } + + try { + Field creatorsField = findField(Parcel.class, "sPairedCreators"); + creatorsField.setAccessible(true); + Map sPairedCreators = (Map) creatorsField.get(null); + if (sPairedCreators != null) sPairedCreators.clear(); + creatorsField.setAccessible(false); + } catch (Exception e) { + if (mDebug) Log.d(TAG, "Couldn't clear Parcel sPairedCreators: " + e); + } + + mSignatureSpoofed = true; + Log.i(TAG, "Signature spoofing enabled via Parcelable.Creator"); + } + + private Field findField(Class clazz, String fieldName) throws NoSuchFieldException { + Class currentClass = clazz; + while (currentClass != null && !currentClass.equals(Object.class)) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + throw new NoSuchFieldException("Field '" + fieldName + "' not found"); + } + + private void spoofSdkInt() { + try { + Field field = Build.VERSION.class.getDeclaredField("SDK_INT"); + field.setAccessible(true); + int oldValue = field.getInt(null); + int targetSdk = Math.min(oldValue, 32); + if (oldValue != targetSdk) { + field.set(null, targetSdk); + Log.d(TAG + "/Java:PS", "[SDK_INT]: " + oldValue + " -> " + targetSdk); + } + field.setAccessible(false); + } catch (Exception e) { + Log.e(TAG, "Failed to spoof SDK_INT", e); + } + } + + private void spoofField(String fieldName, String value, String logSuffix) { + if (value == null || value.isEmpty()) { + if (mVerboseLogs > 0) Log.d(TAG, fieldName + " is empty, skipping"); + return; + } + + try { + Field field = null; + String oldValue = null; + + if (hasField(Build.class, fieldName)) { + field = Build.class.getDeclaredField(fieldName); + } else if (hasField(Build.VERSION.class, fieldName)) { + field = Build.VERSION.class.getDeclaredField(fieldName); + } else { + if (mVerboseLogs > 1) Log.d(TAG, "Field not found: " + fieldName); + return; + } + + field.setAccessible(true); + oldValue = String.valueOf(field.get(null)); + + if (value.equals(oldValue)) { + if (mVerboseLogs > 2) Log.d(TAG, "[" + fieldName + "]: " + value + " (unchanged)"); + field.setAccessible(false); + return; + } + + Class fieldType = field.getType(); + Object newValue; + + if (fieldType == String.class) { + newValue = value; + } else if (fieldType == int.class) { + newValue = Integer.parseInt(value); + } else if (fieldType == long.class) { + newValue = Long.parseLong(value); + } else if (fieldType == boolean.class) { + newValue = Boolean.parseBoolean(value); + } else { + Log.w(TAG, "Unsupported field type: " + fieldType); + field.setAccessible(false); + return; + } + + field.set(null, newValue); + field.setAccessible(false); + + Log.d(TAG + "/Java:" + logSuffix, "[" + fieldName + "]: " + oldValue + " -> " + value); + + } catch (Exception e) { + Log.e(TAG, "Failed to spoof " + fieldName, e); + } + } + + private boolean hasField(Class clazz, String fieldName) { + for (Field f : clazz.getDeclaredFields()) { + if (f.getName().equals(fieldName)) return true; + } + return false; + } + + public String getSpoofedProperty(String key) { + if (!mSpoofProps || !mConfigLoaded) return null; + + String value = mSystemProps.get(key); + if (value != null) return value; + + for (Map.Entry entry : mSystemProps.entrySet()) { + String pattern = entry.getKey(); + if (pattern.startsWith("*") && key.endsWith(pattern.substring(1))) { + return entry.getValue(); + } + } + + return null; + } + + public boolean isSpoofSignatureEnabled() { + return mSpoofSignature && mConfigLoaded; + } + + public boolean isSpoofProviderEnabled() { + return mSpoofProvider && mConfigLoaded; + } + + public int getVerboseLogs() { + return mVerboseLogs; + } + + public Map getBuildFields() { + return mBuildFields; + } + + public Map getSystemProps() { + return mSystemProps; + } + + public boolean isConfigLoaded() { + return mConfigLoaded; + } + + public byte[] getRomSignatureBytes() { + return Base64.decode(ROM_SIGNATURE_DATA, Base64.DEFAULT); + } + + public boolean shouldSpoofPhotos(String packageName) { + return mConfigLoaded && mSpoofPhotos && TextUtils.equals(GPHOTOS_PACKAGE, packageName); + } + + public void spoofPhotosProps() { + for (Map.Entry entry : PIXEL_XL_PROPS.entrySet()) { + spoofField(entry.getKey(), String.valueOf(entry.getValue()), "Photos"); + } + Log.i(TAG, "Photos spoofing enabled - device appears as Pixel XL"); + } + + public boolean hasSystemFeature(String name, int version) { + if (!isPixelDevice() && PIXEL_FEATURES.contains(name)) return false; + return NEXUS_FEATURES.contains(name); + } + + private static boolean isPixelDevice() { + return SystemProperties.get("ro.soc.manufacturer", "").equalsIgnoreCase("google"); + } + + public void logBuildFields() { + if (mVerboseLogs < 100) return; + for (Field field : Build.class.getDeclaredFields()) { + try { + Log.d(TAG, "Build." + field.getName() + " = " + field.get(null)); + } catch (Exception e) { + } + } + for (Field field : Build.VERSION.class.getDeclaredFields()) { + try { + Log.d(TAG, "Build.VERSION." + field.getName() + " = " + field.get(null)); + } catch (Exception e) { + } + } + } + + private static class CustomPackageInfoCreator implements Parcelable.Creator { + private final Parcelable.Creator originalCreator; + private final Signature spoofedSignature; + + CustomPackageInfoCreator(Parcelable.Creator originalCreator, Signature spoofedSignature) { + this.originalCreator = originalCreator; + this.spoofedSignature = spoofedSignature; + } + + @Override + @SuppressWarnings("deprecation") + public PackageInfo createFromParcel(Parcel source) { + PackageInfo packageInfo = originalCreator.createFromParcel(source); + if ("android".equals(packageInfo.packageName)) { + if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { + packageInfo.signatures[0] = spoofedSignature; + } + if (packageInfo.signingInfo != null) { + Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); + if (signaturesArray != null && signaturesArray.length > 0) { + signaturesArray[0] = spoofedSignature; + } + } + } + return packageInfo; + } + + @Override + public PackageInfo[] newArray(int size) { + return originalCreator.newArray(size); + } + } +} diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 069b1f49c818..73a30a8a4227 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -127,6 +127,7 @@ import android.util.SparseArray; import android.util.Xml; import android.util.proto.ProtoOutputStream; +import android.security.pif.PlayIntegritySpoofService; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.pm.pkg.component.ParsedActivity; @@ -1617,6 +1618,39 @@ public final PackageInfo generatePackageInfo(PackageStateInternal ps, } }); + if ("android".equals(p.getPackageName())) { + try { + PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); + if (pifService.isSpoofSignatureEnabled()) { + String[] callingPackages = getPackagesForUid(callingUid); + boolean isGms = false; + if (callingPackages != null) { + for (String pkg : callingPackages) { + if ("com.google.android.gms".equals(pkg)) { + isGms = true; + break; + } + } + } + if (isGms) { + Signature pifSignature = new Signature(pifService.getRomSignatureBytes()); + packageInfo.signatures = new Signature[]{pifSignature}; + packageInfo.signingInfo = new SigningInfo( + new SigningDetails( + packageInfo.signatures, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3, + SigningDetails.toSigningKeys(packageInfo.signatures), + null + ) + ); + Slog.d(TAG, "PIF: Spoofed ROM signature for 'android' package to GMS"); + } + } + } catch (Exception e) { + Slog.e(TAG, "PIF: Failed to spoof ROM signature", e); + } + } + return packageInfo; } else if ((flags & (MATCH_UNINSTALLED_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0 && PackageUserStateUtils.isAvailable(state, flags)) { From bf66dbce5cbc718588771181d820378ae89c8060 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:57:44 +0800 Subject: [PATCH 1089/1315] core: Fix stackoverflow in trickystore port Change-Id: I26c849f219c6ce0fe7239c55e0ac9ad3356bb912 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/security/keystore2/AndroidKeyStoreSpi.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java index cde337cff6b7..96676b921634 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -118,6 +118,8 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { private KeyStore2 mKeyStore; private @KeyProperties.Namespace int mNamespace = KeyProperties.NAMESPACE_APPLICATION; + + private static final ThreadLocal sInHack = ThreadLocal.withInitial(() -> Boolean.FALSE); @Override public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, @@ -273,6 +275,10 @@ private static Certificate[] hackCertificateChainIfNeeded(Certificate[] chain) { if (chain == null || chain.length == 0) { return chain; } + if (sInHack.get()) { + return chain; + } + sInHack.set(Boolean.TRUE); try { TrickyStoreService service = TrickyStoreService.getInstance(); if (!service.hasKeyboxes()) { @@ -292,6 +298,8 @@ private static Certificate[] hackCertificateChainIfNeeded(Certificate[] chain) { } } catch (Exception e) { Log.e(TAG, "TrickyStore: Failed to hack certificate chain", e); + } finally { + sInHack.set(Boolean.FALSE); } return chain; } From e1959d73c1cde4711430eadee6d989c197ea03c6 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:45:27 +0800 Subject: [PATCH 1090/1315] core: Improve spoofs entry point Change-Id: Ib10372342d456f1929d65a674b0a291a35295923 Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 19 ++++++++++++++++++ core/java/android/app/Application.java | 20 ------------------- .../app/ApplicationPackageManager.java | 5 +++-- .../pif/PlayIntegritySpoofService.java | 10 +++++++--- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2b39353eae09..d8027a84426c 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -191,6 +191,8 @@ import android.se.omapi.SeServiceManager; import android.security.NetworkSecurityPolicy; import android.security.net.config.NetworkSecurityConfigProvider; +import android.security.gameprops.GamePropsSpoofService; +import android.security.pif.PlayIntegritySpoofService; import android.system.ErrnoException; import android.telephony.TelephonyFrameworkInitializer; import android.util.AndroidRuntimeException; @@ -8014,6 +8016,23 @@ private void handleBindApplication(AppBindData data) { final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); mConfigurationController.updateLocaleListFromAppContext(appContext); + GamePropsSpoofService gamePropsService = GamePropsSpoofService.getInstance(); + if (gamePropsService.isEnabled()) { + gamePropsService.spoofForPackage(data.appInfo.packageName); + } + + PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); + if (pifService.shouldSpoof(data.processName)) { + pifService.spoofBuildFields(data.processName); + if (pifService.isSpoofSignatureEnabled()) { + pifService.spoofSignature(); + } + } + + if (pifService.shouldSpoofPhotos(data.appInfo.packageName)) { + pifService.spoofPhotosProps(); + } + // Initialize the default http proxy in this process. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Setup proxies"); try { diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index a04445dc046d..ed22fb961ab6 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -30,8 +30,6 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; -import android.security.gameprops.GamePropsSpoofService; -import android.security.pif.PlayIntegritySpoofService; import android.util.Log; import android.view.autofill.AutofillManager; @@ -357,24 +355,6 @@ public static String getProcessName() { /* package */ final void attach(Context context) { attachBaseContext(context); setLoadedApk(context); - String packageName = context != null ? context.getPackageName() : null; - String processName = getProcessName(); - PlayIntegritySpoofService pif = PlayIntegritySpoofService.getInstance(); - if (packageName != null) { - GamePropsSpoofService gamePropsService = GamePropsSpoofService.getInstance(); - if (gamePropsService != null && gamePropsService.isEnabled()) { - gamePropsService.spoofForPackage(packageName); - } - if (pif.shouldSpoofPhotos(packageName)) { - pif.spoofPhotosProps(); - } - } - if (processName != null && pif.shouldSpoof(processName)) { - pif.spoofBuildFields(processName); - if (pif.isSpoofSignatureEnabled()) { - pif.spoofSignature(); - } - } } @android.ravenwood.annotation.RavenwoodIgnore(blockedBy = LoadedApk.class) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index a405beb0a7d7..f042a34dad31 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -994,8 +994,9 @@ public boolean hasSystemFeature(String name, int version) { // TODO(b/375000483): Refactor all of this logic, including flag queries, into // the SystemFeaturesCache class after initial rollout and validation. PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); - if (pifService.shouldSpoofPhotos(ActivityThread.currentPackageName())) { - return pifService.hasSystemFeature(name, version); + Boolean spoofedResult = pifService.hasSystemFeature(name, version); + if (spoofedResult != null) { + return spoofedResult; } Boolean maybeHasSystemFeature = RoSystemFeatures.maybeHasFeature(name, version); diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 04919d8015e5..f00bfd36a31e 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -530,9 +530,13 @@ public void spoofPhotosProps() { Log.i(TAG, "Photos spoofing enabled - device appears as Pixel XL"); } - public boolean hasSystemFeature(String name, int version) { - if (!isPixelDevice() && PIXEL_FEATURES.contains(name)) return false; - return NEXUS_FEATURES.contains(name); + public Boolean hasSystemFeature(String name, int version) { + final String pkgName = ActivityThread.currentPackageName(); + if (shouldSpoofPhotos(pkgName)) { + if (!isPixelDevice() && PIXEL_FEATURES.contains(name)) return false; + return NEXUS_FEATURES.contains(name); + } + return null; } private static boolean isPixelDevice() { From 8c3add12499119059709a6725b49c3dd131b4945 Mon Sep 17 00:00:00 2001 From: Joey Date: Tue, 14 Apr 2026 00:27:16 +0900 Subject: [PATCH 1091/1315] core: Drop redundant Photos spoof - We'll use PixelPropsUtils instead. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 4 -- .../app/ApplicationPackageManager.java | 7 -- .../pif/PlayIntegritySpoofService.java | 72 ------------------- 3 files changed, 83 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d8027a84426c..e6e5f8616657 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -8029,10 +8029,6 @@ private void handleBindApplication(AppBindData data) { } } - if (pifService.shouldSpoofPhotos(data.appInfo.packageName)) { - pifService.spoofPhotosProps(); - } - // Initialize the default http proxy in this process. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Setup proxies"); try { diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index f042a34dad31..ea453082a1eb 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -117,7 +117,6 @@ import android.provider.Settings; import android.ravenwood.annotation.RavenwoodKeepPartialClass; import android.ravenwood.annotation.RavenwoodReplace; -import android.security.pif.PlayIntegritySpoofService; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -993,12 +992,6 @@ public boolean hasSystemFeature(String name, int version) { // * IPC-retrieved system features (lazily cached, requires per-feature IPC) // TODO(b/375000483): Refactor all of this logic, including flag queries, into // the SystemFeaturesCache class after initial rollout and validation. - PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); - Boolean spoofedResult = pifService.hasSystemFeature(name, version); - if (spoofedResult != null) { - return spoofedResult; - } - Boolean maybeHasSystemFeature = RoSystemFeatures.maybeHasFeature(name, version); if (maybeHasSystemFeature != null) { return maybeHasSystemFeature; diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index f00bfd36a31e..372974a1f359 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -39,50 +39,6 @@ public final class PlayIntegritySpoofService { private static final String DROIDGUARD_PACKAGE = "com.google.android.gms.unstable"; private static final String VENDING_PACKAGE = "com.android.vending"; private static final String GMS_PACKAGE = "com.google.android.gms"; - private static final String GPHOTOS_PACKAGE = "com.google.android.apps.photos"; - - private static final Map PIXEL_XL_PROPS = Map.of( - "BRAND", "google", - "MANUFACTURER", "Google", - "DEVICE", "marlin", - "PRODUCT", "marlin", - "HARDWARE", "marlin", - "ID", "QP1A.191005.007.A3", - "MODEL", "Pixel XL", - "FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys" - ); - - private static final Set NEXUS_FEATURES = Set.of( - "com.google.android.apps.photos.NEXUS_PRELOAD", - "com.google.android.apps.photos.nexus_preload", - "com.google.android.feature.PIXEL_EXPERIENCE", - "com.google.android.feature.GOOGLE_BUILD", - "com.google.android.feature.GOOGLE_EXPERIENCE" - ); - - private static final Set PIXEL_FEATURES = Set.of( - "com.google.android.feature.PIXEL_2022_EXPERIENCE", - "com.google.android.feature.PIXEL_2022_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2023_EXPERIENCE", - "com.google.android.feature.PIXEL_2023_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2024_EXPERIENCE", - "com.google.android.feature.PIXEL_2024_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2025_EXPERIENCE", - "com.google.android.feature.PIXEL_2025_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2026_EXPERIENCE", - "com.google.android.feature.PIXEL_2026_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2021_EXPERIENCE", - "com.google.android.feature.PIXEL_2021_MIDYEAR_EXPERIENCE", - "com.google.android.feature.PIXEL_2020_EXPERIENCE", - "com.google.android.feature.PIXEL_2020_MIDYEAR_EXPERIENCE", - "PIXEL_2017_PRELOAD", - "PIXEL_2018_PRELOAD", - "PIXEL_2019_MIDYEAR_PRELOAD", - "PIXEL_2019_PRELOAD", - "PIXEL_2020_EXPERIENCE", - "PIXEL_2020_MIDYEAR_EXPERIENCE", - "PIXEL_EXPERIENCE" - ); private static final String ROM_SIGNATURE_DATA = "MIIFyTCCA7GgAwIBAgIVALyxxl+zDS9SL68SzOr48309eAZyMA0GCSqGSIb3DQEBCwUAMHQxCzAJ" + "BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQw" + @@ -121,7 +77,6 @@ public final class PlayIntegritySpoofService { private volatile boolean mSpoofSignature = false; private volatile boolean mSpoofVendingBuild = true; private volatile boolean mSpoofVendingSdk = false; - private volatile boolean mSpoofPhotos = false; private volatile boolean mDebug = false; private final Map mBuildFields = new ConcurrentHashMap<>(); @@ -267,9 +222,6 @@ private void processKeyValue(String key, String value) { case "spoofVendingSdk": mSpoofVendingSdk = "1".equals(value) || "true".equalsIgnoreCase(value); break; - case "spoofPhotos": - mSpoofPhotos = "1".equals(value) || "true".equalsIgnoreCase(value); - break; case "DEBUG": mDebug = "1".equals(value) || "true".equalsIgnoreCase(value); break; @@ -519,30 +471,6 @@ public byte[] getRomSignatureBytes() { return Base64.decode(ROM_SIGNATURE_DATA, Base64.DEFAULT); } - public boolean shouldSpoofPhotos(String packageName) { - return mConfigLoaded && mSpoofPhotos && TextUtils.equals(GPHOTOS_PACKAGE, packageName); - } - - public void spoofPhotosProps() { - for (Map.Entry entry : PIXEL_XL_PROPS.entrySet()) { - spoofField(entry.getKey(), String.valueOf(entry.getValue()), "Photos"); - } - Log.i(TAG, "Photos spoofing enabled - device appears as Pixel XL"); - } - - public Boolean hasSystemFeature(String name, int version) { - final String pkgName = ActivityThread.currentPackageName(); - if (shouldSpoofPhotos(pkgName)) { - if (!isPixelDevice() && PIXEL_FEATURES.contains(name)) return false; - return NEXUS_FEATURES.contains(name); - } - return null; - } - - private static boolean isPixelDevice() { - return SystemProperties.get("ro.soc.manufacturer", "").equalsIgnoreCase("google"); - } - public void logBuildFields() { if (mVerboseLogs < 100) return; for (Field field : Build.class.getDeclaredFields()) { From 244e3beff35357d9c71a5279cb18eb558db27e69 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 14 Apr 2026 09:56:06 +0530 Subject: [PATCH 1092/1315] core: Move trickystore, pif and game props spoof * Putting them in /data/adb exposes root under /data/adb to the world. * Move them to /data/system which is already meant for apps. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/security/gameprops/GamePropsSpoofService.java | 2 +- core/java/android/security/pif/PlayIntegritySpoofService.java | 2 +- core/java/android/security/trickystore/TrickyStoreService.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/java/android/security/gameprops/GamePropsSpoofService.java b/core/java/android/security/gameprops/GamePropsSpoofService.java index bac2de9718a8..3a976dd11207 100644 --- a/core/java/android/security/gameprops/GamePropsSpoofService.java +++ b/core/java/android/security/gameprops/GamePropsSpoofService.java @@ -35,7 +35,7 @@ */ public final class GamePropsSpoofService { private static final String TAG = "GameProps"; - private static final String CONFIG_PATH = "/data/adb/gameprops"; + private static final String CONFIG_PATH = "/data/system/gameprops"; private static final String CONFIG_FILE = "gameprops.json"; private static GamePropsSpoofService sInstance; diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 372974a1f359..045809f69d71 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -27,7 +27,7 @@ /** @hide */ public final class PlayIntegritySpoofService { private static final String TAG = "PIF"; - private static final String CONFIG_PATH = "/data/adb/playintegrityfix"; + private static final String CONFIG_PATH = "/data/system/playintegrityfix"; private static final String[] PROP_FILES = { "custom.pif.prop", diff --git a/core/java/android/security/trickystore/TrickyStoreService.java b/core/java/android/security/trickystore/TrickyStoreService.java index cc0c7bf2cb38..321897905c62 100644 --- a/core/java/android/security/trickystore/TrickyStoreService.java +++ b/core/java/android/security/trickystore/TrickyStoreService.java @@ -21,7 +21,7 @@ */ public class TrickyStoreService { private static final String TAG = "TrickyStoreService"; - private static final String CONFIG_PATH = "/data/adb/tricky_store"; + private static final String CONFIG_PATH = "/data/system/tricky_store"; private static final String TARGET_FILE = "target.txt"; private static final String KEYBOX_FILE = "keybox.xml"; private static final String PATCHLEVEL_FILE = "security_patch.txt"; From bd5cb1993ba406b5ca980928c390045a06500c4c Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 14 Apr 2026 10:23:50 +0530 Subject: [PATCH 1093/1315] core: Fix json parsing non-string values in PIF Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../security/pif/PlayIntegritySpoofService.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 045809f69d71..0329adb5ac36 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -185,7 +185,21 @@ private void parseJson(String content) { reader.beginObject(); while (reader.hasNext()) { String key = reader.nextName(); - String value = reader.nextString(); + String value; + switch (reader.peek()) { + case BOOLEAN: + value = String.valueOf(reader.nextBoolean()); + break; + case NUMBER: + value = reader.nextString(); + break; + case NULL: + reader.nextNull(); + continue; + default: + value = reader.nextString(); + break; + } processKeyValue(key, value); } reader.endObject(); From e8259cf41bfbb60849e53436566092099d3021aa Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 16 Apr 2026 00:02:16 +0900 Subject: [PATCH 1094/1315] core: pif: skip SDK_INT spoofing for vending process Spoofing SDK_INT to 32 for the vending process causes a fatal crash in InAppBillingService. Android enforces that broadcast receivers must declare RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED on SDK 33+, but Play Store skips those flags when it thinks it's running on SDK 32. The framework rejects the registration and the process dies. Skip SDK_INT when iterating build fields for the vending process. DroidGuard still receives the spoofed SDK_INT as required for integrity attestation. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/security/pif/PlayIntegritySpoofService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 0329adb5ac36..31f784418d34 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -281,6 +281,7 @@ public void spoofBuildFields(String processName) { } if (mSpoofVendingBuild && !mSpoofVendingSdk) { for (Map.Entry entry : mBuildFields.entrySet()) { + if ("SDK_INT".equals(entry.getKey())) continue; spoofField(entry.getKey(), entry.getValue(), "PS"); } } From 58270a7284ccec6cab4bd50bc8c69836262a1b28 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:49:18 +0800 Subject: [PATCH 1095/1315] services: Adding support for service injector Change-Id: Ie4324d38e5b8f6d6400b48d774fa71406b2e248d Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/AxExtServiceFactory.java | 75 +++++++++++++++ .../android/server/IAxExtServiceFactory.java | 32 +++++++ .../com/android/server/NtServiceInjector.java | 92 +++++++++++++++++++ .../server/am/ActivityManagerService.java | 7 ++ .../java/com/android/server/SystemServer.java | 6 ++ 5 files changed, 212 insertions(+) create mode 100644 services/core/java/com/android/server/AxExtServiceFactory.java create mode 100644 services/core/java/com/android/server/IAxExtServiceFactory.java create mode 100644 services/core/java/com/android/server/NtServiceInjector.java diff --git a/services/core/java/com/android/server/AxExtServiceFactory.java b/services/core/java/com/android/server/AxExtServiceFactory.java new file mode 100644 index 000000000000..79c5e28845ea --- /dev/null +++ b/services/core/java/com/android/server/AxExtServiceFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server; + +import android.content.Context; + +import com.android.server.am.*; +import com.android.server.pm.*; +import com.android.server.wm.WindowManagerService; + +public class AxExtServiceFactory { + private static AxExtServiceFactory sInstance = null; + + private static final Object sLock = new Object(); + + private AxExtServiceFactory(Context context) { + NtServiceInjector.get().setCtx(context); + } + + public static synchronized AxExtServiceFactory init(Context context) { + if (sInstance == null) { + sInstance = new AxExtServiceFactory(context); + } + return sInstance; + } + + public static AxExtServiceFactory get() { + if (sInstance == null) { + throw new IllegalStateException("AxExtServiceFactory not initialized"); + } + return sInstance; + } + + public static void injectActivityManagerService(ActivityManagerService ams) { + NtServiceInjector.get().setActivityManagerService(ams); + } + + public static void injectWindowManagerService(WindowManagerService wms) { + NtServiceInjector.get().setWindowManagerService(wms); + } + + public static void injectPackageManagerservice(PackageManagerService pm) { + NtServiceInjector.get().setPackageManagerService(pm); + } + + @SuppressWarnings("unchecked") + public static T getOrCreate(IAxExtServiceFactory.ExtType type) { + Object instance; + switch (type) { + default: + throw new IllegalArgumentException("Unknown ExtType: " + type); + } + + return (T) type.getClazz().cast(instance); + } + + public static void systemReady() { + } + + public static void onLateSystemReady() { + } +} diff --git a/services/core/java/com/android/server/IAxExtServiceFactory.java b/services/core/java/com/android/server/IAxExtServiceFactory.java new file mode 100644 index 000000000000..0406a49982de --- /dev/null +++ b/services/core/java/com/android/server/IAxExtServiceFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server; + +import com.android.server.am.*; + +public interface IAxExtServiceFactory { + enum ExtType { + private final Class clazz; + + ExtType(Class clazz) { + this.clazz = clazz; + } + + public Class getClazz() { + return clazz; + } + } +} diff --git a/services/core/java/com/android/server/NtServiceInjector.java b/services/core/java/com/android/server/NtServiceInjector.java new file mode 100644 index 000000000000..41da908c46b9 --- /dev/null +++ b/services/core/java/com/android/server/NtServiceInjector.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server; + +import android.content.Context; +import com.android.server.am.ActivityManagerService; +import com.android.server.pm.PackageManagerService; +import com.android.server.wm.ActivityTaskManagerService; +import com.android.server.wm.WindowManagerService; + +public class NtServiceInjector { + private static NtServiceInjector instance = null; + private ActivityManagerService mService; + private Context ctx; + private WindowManagerService mWindowService; + private PackageManagerService mPackageService; + + private NtServiceInjector() { + } + + public static synchronized NtServiceInjector get() { + if (instance == null) { + instance = new NtServiceInjector(); + } + return instance; + } + + void setCtx(Context context) { + ctx = context; + } + + void setActivityManagerService(ActivityManagerService activityManagerService) { + mService = activityManagerService; + } + + void setWindowManagerService(WindowManagerService windowManagerService) { + mWindowService = windowManagerService; + } + + void setPackageManagerService(PackageManagerService pm) { + mPackageService = pm; + } + + public WindowManagerService getWindowManagerService() { + return mWindowService; + } + + public ActivityTaskManagerService getActivityTaskManagerService() { + return mService.mActivityTaskManager; + } + + public ActivityManagerService getActivityManagerService() { + return mService; + } + + public PackageManagerService getPackageManagerService() { + return mPackageService; + } + + public Context getContext() { + return ctx; + } + + public static Context getCtx() { + return get().getContext(); + } + + public static WindowManagerService getWm() { + return get().getWindowManagerService(); + } + + public static ActivityManagerService getAm() { + return get().getActivityManagerService(); + } + + public static PackageManagerService getPm() { + return get().getPackageManagerService(); + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 77e02df698ec..e6907d5806a1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -445,6 +445,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.AlarmManagerInternal; +import com.android.server.AxExtServiceFactory; import com.android.server.BootReceiver; import com.android.server.DeviceIdleInternal; import com.android.server.DisplayThread; @@ -5376,6 +5377,10 @@ public void onReceive(Context context, Intent intent) { // Start PSI monitoring in LMKD if it was skipped earlier. ProcessList.startPsiMonitoringAfterBoot(); + mHandler.postDelayed(() -> { + AxExtServiceFactory.onLateSystemReady(); + }, 5000); + mUserController.onBootComplete( new IIntentReceiver.Stub() { @Override @@ -9266,6 +9271,8 @@ public void systemReady(final Runnable goingCallback, @NonNull TimingsTraceAndSl mComponentAliasResolver.onSystemReady(mConstants.mEnableComponentAlias, mConstants.mComponentAliasOverrides); t.traceEnd(); // componentAlias + + AxExtServiceFactory.systemReady(); t.traceEnd(); // PhaseActivityManagerReady } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 9773df476e30..96b4ddfc39e2 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -998,6 +998,8 @@ private void run() { LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); + AxExtServiceFactory.init(mSystemContext); + // Lazily load the pre-installed system font map in SystemServer only if we're not doing // the optimized font loading in the FontManagerService. if (!com.android.text.flags.Flags.useOptimizedBoottimeFontLoading() @@ -1296,6 +1298,7 @@ private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { mActivityManagerService.setSystemServiceManager(mSystemServiceManager); mActivityManagerService.setInstaller(installer); mWindowManagerGlobalLock = atm.getGlobalLock(); + AxExtServiceFactory.injectActivityManagerService(mActivityManagerService); t.traceEnd(); // Data loader manager service needs to be started before package manager @@ -1403,6 +1406,8 @@ private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { SystemClock.elapsedRealtime()); } + AxExtServiceFactory.injectPackageManagerservice(mPackageManagerService); + if (Build.IS_ARC) { t.traceBegin("StartArcSystemHealthService"); mSystemServiceManager.startService(ARC_SYSTEM_HEALTH_SERVICE); @@ -1759,6 +1764,7 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startBootPhase(t, SystemService.PHASE_WAIT_FOR_SENSOR_SERVICE); wm = WindowManagerService.main(context, inputManager, !mFirstBoot, new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager); + AxExtServiceFactory.injectWindowManagerService(wm); ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH | DUMP_FLAG_PROTO); From 7d33b5f89fc2ee34d980559762f87ecbdf19ff09 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 18 Apr 2026 15:03:23 +0800 Subject: [PATCH 1096/1315] core: Fix spoofing structure existing design was based on module structure and legacy prop hook spoofing. that design only works with hardcoded spoof values and code injection like lsposed or magisk. for proper aosp integration it falls apart, /data/adb exposes root detection signals and leaks identity to apps because the spoof logic runs under the calling process context instead of system_server. old flow process -> spoof runs inside process, uses process identity -> hits selinux denials and content provider denials if we tried to route through settings provider new flow process -> am binder -> am -> spoof runs in system_server -> no denials, no identity leak [neobuddy89: Import service injector partially.] Change-Id: Id892adfb32d95d9cc7e21baaf26fc2d8952b08f2 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/IActivityManager.aidl | 10 + core/java/android/provider/Settings.java | 25 ++ .../gameprops/GamePropsSpoofService.java | 52 +-- .../pif/PlayIntegritySpoofService.java | 70 +--- .../security/trickystore/KeyBoxManager.java | 94 +++-- .../trickystore/TrickyStoreService.java | 349 +++++++++++------- .../android/server/AxExtServiceFactory.java | 20 + .../android/server/IAxExtServiceFactory.java | 2 + .../server/am/ActivityManagerService.java | 26 ++ .../android/server/spoof/AxSpoofManager.java | 131 +++++++ .../android/server/spoof/IAxSpoofManager.java | 32 ++ .../java/com/android/server/SystemServer.java | 11 - 12 files changed, 554 insertions(+), 268 deletions(-) create mode 100644 services/core/java/com/android/server/spoof/AxSpoofManager.java create mode 100644 services/core/java/com/android/server/spoof/IAxSpoofManager.java diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 6f8f48b7b073..e0adc2411e0b 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1072,4 +1072,14 @@ interface IActivityManager { void releaseMemory(int minAdj, int maxKillCount, boolean includeUIProcesses, boolean skipCamera); void compactAllSystem(); + + String getSpoofPifConfig(); + + String getSpoofGamePropsConfig(); + + String getSpoofTrickyStoreTarget(); + + String getSpoofTrickyStoreKeyBox(); + + String getSpoofTrickyStorePatch(); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index f1736dc32125..1e47f38fee98 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15329,6 +15329,31 @@ public static void setLocationProviderEnabled(ContentResolver cr, */ public static final String IDENTITY_CHECK_NOTIFICATION_VIEW_DETAILS_CLICKED = "identity_check_notification_view_details_clicked"; + + /** + * @hide + */ + public static final String SPOOF_PIF_CONFIG = "spoof_pif_config"; + + /** + * @hide + */ + public static final String SPOOF_GAMEPROPS_CONFIG = "spoof_gameprops_config"; + + /** + * @hide + */ + public static final String SPOOF_TRICKYSTORE_TARGET = "spoof_trickystore_target"; + + /** + * @hide + */ + public static final String SPOOF_TRICKYSTORE_KEYBOX = "spoof_trickystore_keybox"; + + /** + * @hide + */ + public static final String SPOOF_TRICKYSTORE_PATCH = "spoof_trickystore_patch"; } /** diff --git a/core/java/android/security/gameprops/GamePropsSpoofService.java b/core/java/android/security/gameprops/GamePropsSpoofService.java index 3a976dd11207..04a66b7ba5a1 100644 --- a/core/java/android/security/gameprops/GamePropsSpoofService.java +++ b/core/java/android/security/gameprops/GamePropsSpoofService.java @@ -16,13 +16,12 @@ package android.security.gameprops; +import android.app.ActivityManager; import android.os.Build; +import android.os.RemoteException; import android.util.JsonReader; import android.util.Log; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.StringReader; import java.lang.reflect.Field; @@ -35,8 +34,6 @@ */ public final class GamePropsSpoofService { private static final String TAG = "GameProps"; - private static final String CONFIG_PATH = "/data/system/gameprops"; - private static final String CONFIG_FILE = "gameprops.json"; private static GamePropsSpoofService sInstance; @@ -64,51 +61,36 @@ public static synchronized GamePropsSpoofService getInstance() { public void loadConfig() { mGameConfigs.clear(); mEnabled = false; + mConfigLoaded = false; - File configFile = new File(CONFIG_PATH, CONFIG_FILE); - if (!configFile.exists() || !configFile.canRead()) { - Log.w(TAG, "Config file not found or not readable: " + configFile.getAbsolutePath()); - mConfigLoaded = false; + String content; + try { + content = ActivityManager.getService().getSpoofGamePropsConfig(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to fetch gameprops config from system_server", e); return; } - try { - String content = readFile(configFile); - if (content == null || content.isEmpty()) { - mConfigLoaded = false; - return; - } + if (content == null || content.isEmpty()) { + Log.w(TAG, "No gameprops config in Settings.Secure"); + return; + } + try { parseJson(content); mConfigLoaded = true; Log.i(TAG, "Game props config loaded, games=" + mGameConfigs.size() + ", enabled=" + mEnabled); - } catch (Exception e) { - Log.e(TAG, "Failed to load game props config", e); - mConfigLoaded = false; + Log.e(TAG, "Failed to parse game props config", e); } } - private String readFile(File file) { - StringBuilder content = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String line; - while ((line = reader.readLine()) != null) { - content.append(line).append("\n"); - } - } catch (IOException e) { - Log.e(TAG, "Failed to read config file", e); - return null; - } - return content.toString(); - } - private void parseJson(String content) { try (JsonReader reader = new JsonReader(new StringReader(content))) { reader.beginObject(); while (reader.hasNext()) { String key = reader.nextName(); - + if ("enabled".equals(key)) { mEnabled = reader.nextBoolean(); } else if ("debug".equals(key)) { @@ -130,7 +112,7 @@ private void parseGames(JsonReader reader) throws IOException { while (reader.hasNext()) { String packageName = reader.nextName(); Map gameProps = new HashMap<>(); - + reader.beginObject(); while (reader.hasNext()) { String propKey = reader.nextName(); @@ -138,7 +120,7 @@ private void parseGames(JsonReader reader) throws IOException { gameProps.put(propKey, propValue); } reader.endObject(); - + if (!gameProps.isEmpty()) { mGameConfigs.put(packageName, gameProps); if (mDebug) { diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 31f784418d34..9a3e39812441 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -1,5 +1,6 @@ package android.security.pif; +import android.app.ActivityManager; import android.app.ActivityThread; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -7,15 +8,13 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Base64; import android.util.JsonReader; import android.util.Log; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.StringReader; import java.lang.reflect.Field; @@ -27,14 +26,6 @@ /** @hide */ public final class PlayIntegritySpoofService { private static final String TAG = "PIF"; - private static final String CONFIG_PATH = "/data/system/playintegrityfix"; - - private static final String[] PROP_FILES = { - "custom.pif.prop", - "custom.pif.json", - "pif.prop", - "pif.json" - }; private static final String DROIDGUARD_PACKAGE = "com.google.android.gms.unstable"; private static final String VENDING_PACKAGE = "com.android.vending"; @@ -98,65 +89,38 @@ public static synchronized PlayIntegritySpoofService getInstance() { public void loadConfig() { mBuildFields.clear(); mSystemProps.clear(); + mConfigLoaded = false; - File configFile = findConfigFile(); - if (configFile == null) { - Log.w(TAG, "No PIF config file found"); - mConfigLoaded = false; + String content; + try { + content = ActivityManager.getService().getSpoofPifConfig(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to fetch PIF config from system_server", e); return; } - try { - String content = readFile(configFile); - if (content == null || content.isEmpty()) { - mConfigLoaded = false; - return; - } + if (content == null || content.isEmpty()) { + Log.w(TAG, "No PIF config in Settings.Secure"); + return; + } - if (configFile.getName().endsWith(".json")) { + try { + String trimmed = content.trim(); + if (trimmed.startsWith("{")) { parseJson(content); } else { parseProp(content); } mConfigLoaded = true; - Log.i(TAG, "PIF config loaded from " + configFile.getAbsolutePath() - + ", fields=" + mBuildFields.size() + ", props=" + mSystemProps.size()); + Log.i(TAG, "PIF config loaded, fields=" + mBuildFields.size() + + ", props=" + mSystemProps.size()); } catch (Exception e) { Log.e(TAG, "Failed to load PIF config", e); } } - private File findConfigFile() { - File dir = new File(CONFIG_PATH); - if (!dir.exists()) { - return null; - } - - for (String fileName : PROP_FILES) { - File file = new File(dir, fileName); - if (file.exists() && file.canRead()) { - return file; - } - } - return null; - } - - private String readFile(File file) { - StringBuilder content = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String line; - while ((line = reader.readLine()) != null) { - content.append(line).append("\n"); - } - } catch (IOException e) { - Log.e(TAG, "Failed to read config file", e); - return null; - } - return content.toString(); - } - private void parseProp(String content) { String[] lines = content.split("\n"); for (String line : lines) { diff --git a/core/java/android/security/trickystore/KeyBoxManager.java b/core/java/android/security/trickystore/KeyBoxManager.java index b915bc8061c9..b7796b39c6c9 100644 --- a/core/java/android/security/trickystore/KeyBoxManager.java +++ b/core/java/android/security/trickystore/KeyBoxManager.java @@ -35,6 +35,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @hide @@ -43,6 +45,8 @@ public class KeyBoxManager { private static final String TAG = "KeyBoxManager"; private final Map mKeyboxes = new ConcurrentHashMap<>(); + + private static final Pattern PEM_HEADER = Pattern.compile("-----BEGIN ([^-]+)-----"); /** @hide */ public static class KeyBox { @@ -151,6 +155,7 @@ private void processKeybox(String algorithm, String privateKeyPem, List try { String normalizedAlgorithm; switch (algorithm.toLowerCase()) { + case "ec": case "ecdsa": normalizedAlgorithm = "EC"; break; @@ -183,53 +188,80 @@ private void processKeybox(String algorithm, String privateKeyPem, List } private PrivateKey parsePrivateKey(String pem, String algorithm) throws Exception { + String pemType = detectPemType(pem); byte[] keyBytes = parsePemContent(pem); - - ASN1InputStream asn1In = new ASN1InputStream(keyBytes); - ASN1Primitive asn1 = asn1In.readObject(); - asn1In.close(); - - if ("EC".equals(algorithm)) { + + if ("EC PRIVATE KEY".equals(pemType) + || ("EC".equals(algorithm) && pemType == null)) { + return parseEcPrivateKey(keyBytes, algorithm); + } + + if ("RSA PRIVATE KEY".equals(pemType) + || ("RSA".equals(algorithm) && pemType == null)) { + return parseRsaPrivateKey(keyBytes); + } + + if ("PRIVATE KEY".equals(pemType) || "ENCRYPTED PRIVATE KEY".equals(pemType)) { + PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(keyBytes); + if ("RSA".equals(algorithm)) { + return new KeyFactorySpi().generatePrivate(pkInfo); + } + if ("EC".equals(algorithm)) { + return new EC().generatePrivate(pkInfo); + } + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + return KeyFactory.getInstance(algorithm).generatePrivate(spec); + } + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + return KeyFactory.getInstance(algorithm).generatePrivate(spec); + } + + private PrivateKey parseEcPrivateKey(byte[] keyBytes, String algorithm) throws Exception { + try (ASN1InputStream asn1In = new ASN1InputStream(keyBytes)) { + ASN1Primitive asn1 = asn1In.readObject(); try { ECPrivateKey ecPrivateKey = ECPrivateKey.getInstance(asn1); X9ECParameters ecParams = ECNamedCurveTable.getByOID( - (ASN1ObjectIdentifier) ecPrivateKey.getParameters()); + (ASN1ObjectIdentifier) ecPrivateKey.getParameters()); ECDomainParameters domainParams = new ECDomainParameters( - ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH()); + ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH()); ECPrivateKeyParameters privParams = new ECPrivateKeyParameters( - ecPrivateKey.getKey(), domainParams); - BCECPrivateKey bcKey = new BCECPrivateKey(algorithm, privParams, null); - return bcKey; + ecPrivateKey.getKey(), domainParams); + return new BCECPrivateKey(algorithm, privParams, null); } catch (Exception e) { PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(asn1); - EC ecFactory = new EC(); - return ecFactory.generatePrivate(pkInfo); + return new EC().generatePrivate(pkInfo); } - } else if ("RSA".equals(algorithm)) { + } + } + + private PrivateKey parseRsaPrivateKey(byte[] keyBytes) throws Exception { + try (ASN1InputStream asn1In = new ASN1InputStream(keyBytes)) { + ASN1Primitive asn1 = asn1In.readObject(); try { RSAPrivateKey rsaPrivateKey = RSAPrivateKey.getInstance(asn1); RSAPrivateCrtKeySpec rsaSpec = new RSAPrivateCrtKeySpec( - rsaPrivateKey.getModulus(), - rsaPrivateKey.getPublicExponent(), - rsaPrivateKey.getPrivateExponent(), - rsaPrivateKey.getPrime1(), - rsaPrivateKey.getPrime2(), - rsaPrivateKey.getExponent1(), - rsaPrivateKey.getExponent2(), - rsaPrivateKey.getCoefficient() - ); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return keyFactory.generatePrivate(rsaSpec); + rsaPrivateKey.getModulus(), + rsaPrivateKey.getPublicExponent(), + rsaPrivateKey.getPrivateExponent(), + rsaPrivateKey.getPrime1(), + rsaPrivateKey.getPrime2(), + rsaPrivateKey.getExponent1(), + rsaPrivateKey.getExponent2(), + rsaPrivateKey.getCoefficient()); + return KeyFactory.getInstance("RSA").generatePrivate(rsaSpec); } catch (Exception e) { PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(asn1); - KeyFactorySpi rsaFactory = new KeyFactorySpi(); - return rsaFactory.generatePrivate(pkInfo); + return new KeyFactorySpi().generatePrivate(pkInfo); } } - - PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(algorithm); - return keyFactory.generatePrivate(spec); + } + + private String detectPemType(String pem) { + if (pem == null) return null; + Matcher m = PEM_HEADER.matcher(pem); + return m.find() ? m.group(1).trim().toUpperCase() : null; } private byte[] parsePemContent(String pem) { diff --git a/core/java/android/security/trickystore/TrickyStoreService.java b/core/java/android/security/trickystore/TrickyStoreService.java index 321897905c62..c2d6f928cae6 100644 --- a/core/java/android/security/trickystore/TrickyStoreService.java +++ b/core/java/android/security/trickystore/TrickyStoreService.java @@ -1,17 +1,35 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.security.trickystore; -import android.os.FileObserver; +import android.app.ActivityManager; +import android.os.RemoteException; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; +import android.util.JsonReader; import android.util.Log; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.spec.ECGenParameterSpec; +import java.util.Base64; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -21,10 +39,6 @@ */ public class TrickyStoreService { private static final String TAG = "TrickyStoreService"; - private static final String CONFIG_PATH = "/data/system/tricky_store"; - private static final String TARGET_FILE = "target.txt"; - private static final String KEYBOX_FILE = "keybox.xml"; - private static final String PATCHLEVEL_FILE = "security_patch.txt"; private static TrickyStoreService sInstance; @@ -34,9 +48,9 @@ public class TrickyStoreService { private volatile Boolean mTeeBroken = null; private volatile CustomPatchLevel mCustomPatchLevel = null; + private volatile String mLastKeyboxFingerprint = null; private final KeyBoxManager mKeyBoxManager; - private ConfigObserver mConfigObserver; /** @hide */ public enum Mode { @@ -71,142 +85,228 @@ public static synchronized TrickyStoreService getInstance() { } public void initialize() { - File root = new File(CONFIG_PATH); - if (!root.exists()) { - root.mkdirs(); - } - - File scopeFile = new File(root, TARGET_FILE); - if (scopeFile.exists()) { - updateTargetPackages(scopeFile); - } else { - Log.w(TAG, "target.txt not found at " + scopeFile.getAbsolutePath()); - } + refreshTargets(); + refreshKeyBox(); + refreshPatchLevel(); + Log.i(TAG, "TrickyStoreService initialized"); + } - File keyboxFile = new File(root, KEYBOX_FILE); - if (keyboxFile.exists()) { - updateKeyBox(keyboxFile); - } else { - Log.w(TAG, "keybox.xml not found at " + keyboxFile.getAbsolutePath()); + private String fetchFromAms(Fetcher fetcher) { + try { + return fetcher.fetch(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to fetch trickystore config from system_server", e); + return null; } + } - File patchFile = new File(root, PATCHLEVEL_FILE); - updatePatchLevel(patchFile.exists() ? patchFile : null); - - mConfigObserver = new ConfigObserver(root); - mConfigObserver.startWatching(); - - Log.i(TAG, "TrickyStoreService initialized"); + private interface Fetcher { + String fetch() throws RemoteException; } - private void updateTargetPackages(File file) { + public void refreshTargets() { + String content = fetchFromAms(() -> ActivityManager.getService().getSpoofTrickyStoreTarget()); mHackPackages.clear(); mGeneratePackages.clear(); mPackageModes.clear(); - if (file == null || !file.exists()) { + if (content == null || content.isEmpty()) { return; } - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } + String trimmed = content.trim(); + try { + if (trimmed.startsWith("[") || trimmed.startsWith("{")) { + parseTargetsJson(trimmed); + } else { + parseTargetsText(trimmed); + } + Log.i(TAG, "Updated target packages: hack=" + mHackPackages + + ", generate=" + mGeneratePackages + ", modes=" + mPackageModes); + } catch (Exception e) { + Log.e(TAG, "Failed to parse target packages", e); + } + } + + private void parseTargetsText(String content) { + for (String raw : content.split("\n")) { + String line = raw.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } - if (line.endsWith("!")) { - String pkg = line.substring(0, line.length() - 1).trim(); - mGeneratePackages.add(pkg); - mPackageModes.put(pkg, Mode.GENERATE); - } else if (line.endsWith("?")) { - String pkg = line.substring(0, line.length() - 1).trim(); - mHackPackages.add(pkg); - mPackageModes.put(pkg, Mode.LEAF_HACK); - } else { - mPackageModes.put(line, Mode.AUTO); + if (line.endsWith("!")) { + String pkg = line.substring(0, line.length() - 1).trim(); + mGeneratePackages.add(pkg); + mPackageModes.put(pkg, Mode.GENERATE); + } else if (line.endsWith("?")) { + String pkg = line.substring(0, line.length() - 1).trim(); + mHackPackages.add(pkg); + mPackageModes.put(pkg, Mode.LEAF_HACK); + } else { + mPackageModes.put(line, Mode.AUTO); + } + } + } + + private void parseTargetsJson(String content) throws IOException { + try (JsonReader reader = new JsonReader(new StringReader(content))) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + String pkg = null; + String modeStr = "AUTO"; + while (reader.hasNext()) { + String key = reader.nextName(); + if ("package".equals(key)) { + pkg = reader.nextString(); + } else if ("mode".equals(key)) { + modeStr = reader.nextString(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + if (pkg == null) continue; + Mode mode; + try { + mode = Mode.valueOf(modeStr.toUpperCase()); + } catch (IllegalArgumentException e) { + mode = Mode.AUTO; } + mPackageModes.put(pkg, mode); + if (mode == Mode.LEAF_HACK) mHackPackages.add(pkg); + if (mode == Mode.GENERATE) mGeneratePackages.add(pkg); } - Log.i(TAG, "Updated target packages: hack=" + mHackPackages + - ", generate=" + mGeneratePackages + ", modes=" + mPackageModes); - } catch (IOException e) { - Log.e(TAG, "Failed to update target packages", e); + reader.endArray(); } } - private void updateKeyBox(File file) { - if (file == null || !file.exists()) { + public void refreshKeyBox() { + String raw = fetchFromAms(() -> ActivityManager.getService().getSpoofTrickyStoreKeyBox()); + if (raw == null || raw.isEmpty()) { mKeyBoxManager.clear(); + mLastKeyboxFingerprint = null; + return; + } + String fingerprint = Integer.toHexString(raw.hashCode()) + ":" + raw.length(); + if (fingerprint.equals(mLastKeyboxFingerprint)) { + return; + } + String xml = decodeKeybox(raw); + if (xml == null) { + Log.e(TAG, "Keybox payload not recognised as XML or base64-encoded XML"); return; } - try { - StringBuilder content = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String line; - while ((line = reader.readLine()) != null) { - content.append(line).append("\n"); - } - } - mKeyBoxManager.parseKeybox(content.toString()); + mKeyBoxManager.parseKeybox(xml); + mLastKeyboxFingerprint = fingerprint; Log.i(TAG, "Keybox updated successfully"); } catch (Exception e) { Log.e(TAG, "Failed to update keybox", e); } } - private void updatePatchLevel(File file) { - if (file == null || !file.exists()) { + private String decodeKeybox(String payload) { + String trimmed = payload.trim(); + if (trimmed.startsWith("<")) { + return trimmed; + } + try { + byte[] decoded = Base64.getDecoder().decode(trimmed); + String asXml = new String(decoded, StandardCharsets.UTF_8).trim(); + if (asXml.startsWith("<")) { + return asXml; + } + } catch (IllegalArgumentException ignored) { + } + return null; + } + + public void refreshPatchLevel() { + String content = fetchFromAms(() -> ActivityManager.getService().getSpoofTrickyStorePatch()); + if (content == null || content.isEmpty()) { mCustomPatchLevel = null; return; } - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - StringBuilder content = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.startsWith("#")) { - content.append(line).append("\n"); - } + try { + String trimmed = content.trim(); + if (trimmed.startsWith("{")) { + parsePatchJson(trimmed); + } else { + parsePatchText(trimmed); } + } catch (Exception e) { + Log.e(TAG, "Failed to parse patch level", e); + } + } - String lines = content.toString().trim(); - if (lines.isEmpty()) { - mCustomPatchLevel = null; - return; + private void parsePatchText(String content) { + StringBuilder filtered = new StringBuilder(); + for (String raw : content.split("\n")) { + String line = raw.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + filtered.append(line).append("\n"); } + } + + String lines = filtered.toString().trim(); + if (lines.isEmpty()) { + mCustomPatchLevel = null; + return; + } + + String[] parts = lines.split("\n"); + if (parts.length == 1 && !parts[0].contains("=")) { + mCustomPatchLevel = new CustomPatchLevel(parts[0], parts[0], parts[0], parts[0]); + return; + } - String[] parts = lines.split("\n"); - if (parts.length == 1 && !parts[0].contains("=")) { - mCustomPatchLevel = new CustomPatchLevel(parts[0], parts[0], parts[0], parts[0]); - return; + String system = null, vendor = null, boot = null, all = null; + for (String part : parts) { + int idx = part.indexOf('='); + if (idx > 0) { + String key = part.substring(0, idx).trim().toLowerCase(); + String value = part.substring(idx + 1).trim(); + switch (key) { + case "system": system = value; break; + case "vendor": vendor = value; break; + case "boot": boot = value; break; + case "all": all = value; break; + } } + } + mCustomPatchLevel = new CustomPatchLevel( + system != null ? system : all, + vendor != null ? vendor : all, + boot != null ? boot : all, + all + ); + } - String system = null, vendor = null, boot = null, all = null; - for (String part : parts) { - int idx = part.indexOf('='); - if (idx > 0) { - String key = part.substring(0, idx).trim().toLowerCase(); - String value = part.substring(idx + 1).trim(); - switch (key) { - case "system": system = value; break; - case "vendor": vendor = value; break; - case "boot": boot = value; break; - case "all": all = value; break; - } + private void parsePatchJson(String content) throws IOException { + String system = null, vendor = null, boot = null, all = null; + try (JsonReader reader = new JsonReader(new StringReader(content))) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + switch (key) { + case "system": system = reader.nextString(); break; + case "vendor": vendor = reader.nextString(); break; + case "boot": boot = reader.nextString(); break; + case "all": all = reader.nextString(); break; + default: reader.skipValue(); break; } } - mCustomPatchLevel = new CustomPatchLevel( - system != null ? system : all, - vendor != null ? vendor : all, - boot != null ? boot : all, - all - ); - } catch (IOException e) { - Log.e(TAG, "Failed to update patch level", e); + reader.endObject(); } + mCustomPatchLevel = new CustomPatchLevel( + system != null ? system : all, + vendor != null ? vendor : all, + boot != null ? boot : all, + all + ); } private void ensureTeeStatus() { @@ -232,14 +332,14 @@ private boolean checkTeeBroken() { .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) .setDigests(KeyProperties.DIGEST_SHA256) .setAttestationChallenge(new byte[16]); - + kpg.initialize(builder.build()); kpg.generateKeyPair(); - + KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); ks.load(null); ks.deleteEntry(alias); - + Log.i(TAG, "TEE verification successful"); return false; } catch (Exception e) { @@ -250,6 +350,7 @@ private boolean checkTeeBroken() { public boolean needHack(int callingUid, String[] packages) { if (packages == null) return false; + refreshTargets(); ensureTeeStatus(); for (String pkg : packages) { Mode mode = mPackageModes.get(pkg); @@ -261,6 +362,7 @@ public boolean needHack(int callingUid, String[] packages) { public boolean needGenerate(int callingUid, String[] packages) { if (packages == null) return false; + refreshTargets(); ensureTeeStatus(); for (String pkg : packages) { Mode mode = mPackageModes.get(pkg); @@ -271,45 +373,16 @@ public boolean needGenerate(int callingUid, String[] packages) { } public KeyBoxManager getKeyBoxManager() { + refreshKeyBox(); return mKeyBoxManager; } public CustomPatchLevel getCustomPatchLevel() { + refreshPatchLevel(); return mCustomPatchLevel; } public boolean hasKeyboxes() { return mKeyBoxManager.hasKeyboxes(); } - - private class ConfigObserver extends FileObserver { - private final File mRoot; - - ConfigObserver(File root) { - super(root, CLOSE_WRITE | DELETE | MOVED_FROM | MOVED_TO); - mRoot = root; - } - - @Override - public void onEvent(int event, String path) { - if (path == null) return; - - File file = null; - if (event == CLOSE_WRITE || event == MOVED_TO) { - file = new File(mRoot, path); - } - - switch (path) { - case TARGET_FILE: - updateTargetPackages(file); - break; - case KEYBOX_FILE: - updateKeyBox(file); - break; - case PATCHLEVEL_FILE: - updatePatchLevel(file); - break; - } - } - } } diff --git a/services/core/java/com/android/server/AxExtServiceFactory.java b/services/core/java/com/android/server/AxExtServiceFactory.java index 79c5e28845ea..e6e7d827fe0e 100644 --- a/services/core/java/com/android/server/AxExtServiceFactory.java +++ b/services/core/java/com/android/server/AxExtServiceFactory.java @@ -19,12 +19,16 @@ import com.android.server.am.*; import com.android.server.pm.*; +import com.android.server.spoof.AxSpoofManager; +import com.android.server.spoof.IAxSpoofManager; import com.android.server.wm.WindowManagerService; public class AxExtServiceFactory { private static AxExtServiceFactory sInstance = null; private static final Object sLock = new Object(); + + private static volatile IAxSpoofManager sAxSpoofManager; private AxExtServiceFactory(Context context) { NtServiceInjector.get().setCtx(context); @@ -60,6 +64,17 @@ public static void injectPackageManagerservice(PackageManagerService pm) { public static T getOrCreate(IAxExtServiceFactory.ExtType type) { Object instance; switch (type) { + case AX_SPOOF_MANAGER: + if (sAxSpoofManager == null) { + synchronized (sLock) { + if (sAxSpoofManager == null) { + sAxSpoofManager = new AxSpoofManager(); + } + } + } + instance = sAxSpoofManager; + break; + default: throw new IllegalArgumentException("Unknown ExtType: " + type); } @@ -68,8 +83,13 @@ public static T getOrCreate(IAxExtServiceFactory.ExtType type) { } public static void systemReady() { + getSpoofManager().systemReady(); } public static void onLateSystemReady() { } + + public static IAxSpoofManager getSpoofManager() { + return getOrCreate(IAxExtServiceFactory.ExtType.AX_SPOOF_MANAGER); + } } diff --git a/services/core/java/com/android/server/IAxExtServiceFactory.java b/services/core/java/com/android/server/IAxExtServiceFactory.java index 0406a49982de..1478c20ea2a2 100644 --- a/services/core/java/com/android/server/IAxExtServiceFactory.java +++ b/services/core/java/com/android/server/IAxExtServiceFactory.java @@ -16,9 +16,11 @@ package com.android.server; import com.android.server.am.*; +import com.android.server.spoof.IAxSpoofManager; public interface IAxExtServiceFactory { enum ExtType { + AX_SPOOF_MANAGER(IAxSpoofManager.class); private final Class clazz; ExtType(Class clazz) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e6907d5806a1..1631f20cc942 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19753,6 +19753,32 @@ Freezer getFreezer() { return mFreezer; } + + @Override + public String getSpoofPifConfig() { + return AxExtServiceFactory.getSpoofManager().getPifConfig(); + } + + @Override + public String getSpoofGamePropsConfig() { + return AxExtServiceFactory.getSpoofManager().getGamePropsConfig(); + } + + @Override + public String getSpoofTrickyStoreTarget() { + return AxExtServiceFactory.getSpoofManager().getTrickyStoreTarget(); + } + + @Override + public String getSpoofTrickyStoreKeyBox() { + return AxExtServiceFactory.getSpoofManager().getTrickyStoreKeyBox(); + } + + @Override + public String getSpoofTrickyStorePatch() { + return AxExtServiceFactory.getSpoofManager().getTrickyStorePatch(); + } + // Set of IntentCreatorToken objects that are currently active. private static final Map> sIntentCreatorTokenCache = new ConcurrentHashMap<>(); diff --git a/services/core/java/com/android/server/spoof/AxSpoofManager.java b/services/core/java/com/android/server/spoof/AxSpoofManager.java new file mode 100644 index 000000000000..db6f93cc3b35 --- /dev/null +++ b/services/core/java/com/android/server/spoof/AxSpoofManager.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.spoof; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import com.android.server.NtServiceInjector; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +public class AxSpoofManager implements IAxSpoofManager { + private static final String TAG = "AxSpoofManager"; + + private static final String[] WATCHED_KEYS = { + Settings.Secure.SPOOF_PIF_CONFIG, + Settings.Secure.SPOOF_GAMEPROPS_CONFIG, + Settings.Secure.SPOOF_TRICKYSTORE_TARGET, + Settings.Secure.SPOOF_TRICKYSTORE_KEYBOX, + Settings.Secure.SPOOF_TRICKYSTORE_PATCH, + }; + + private final Map mCache = new ConcurrentHashMap<>(); + private final HandlerThread mHandlerThread; + private final Handler mHandler; + + private Context mContext; + private ContentResolver mResolver; + private ContentObserver mObserver; + private volatile boolean mReady = false; + + public AxSpoofManager() { + mHandlerThread = new HandlerThread("AxSpoofManager"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + + @Override + public void systemReady() { + mContext = NtServiceInjector.getCtx(); + if (mContext == null) { + Log.w(TAG, "Context unavailable, deferring init"); + return; + } + mResolver = mContext.getContentResolver(); + + for (String key : WATCHED_KEYS) { + refreshKey(key); + } + + mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri == null) return; + final String last = uri.getLastPathSegment(); + if (last == null) return; + refreshKey(last); + Log.i(TAG, "Spoof config refreshed: " + last); + } + }; + for (String key : WATCHED_KEYS) { + mResolver.registerContentObserver( + Settings.Secure.getUriFor(key), false, mObserver, UserHandle.USER_ALL); + } + + mReady = true; + Log.i(TAG, "AxSpoofManager ready"); + } + + private void refreshKey(String key) { + if (mResolver == null) return; + final String value = Settings.Secure.getStringForUser( + mResolver, key, UserHandle.USER_SYSTEM); + if (value == null) { + mCache.remove(key); + } else { + mCache.put(key, value); + } + } + + private String getCached(String key) { + return mCache.get(key); + } + + @Override + public String getPifConfig() { + return getCached(Settings.Secure.SPOOF_PIF_CONFIG); + } + + @Override + public String getGamePropsConfig() { + return getCached(Settings.Secure.SPOOF_GAMEPROPS_CONFIG); + } + + @Override + public String getTrickyStoreTarget() { + return getCached(Settings.Secure.SPOOF_TRICKYSTORE_TARGET); + } + + @Override + public String getTrickyStoreKeyBox() { + return getCached(Settings.Secure.SPOOF_TRICKYSTORE_KEYBOX); + } + + @Override + public String getTrickyStorePatch() { + return getCached(Settings.Secure.SPOOF_TRICKYSTORE_PATCH); + } +} diff --git a/services/core/java/com/android/server/spoof/IAxSpoofManager.java b/services/core/java/com/android/server/spoof/IAxSpoofManager.java new file mode 100644 index 000000000000..08982f8baff0 --- /dev/null +++ b/services/core/java/com/android/server/spoof/IAxSpoofManager.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.spoof; + +public interface IAxSpoofManager { + default void systemReady() { + } + + String getPifConfig(); + + String getGamePropsConfig(); + + String getTrickyStoreTarget(); + + String getTrickyStoreKeyBox(); + + String getTrickyStorePatch(); +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 96b4ddfc39e2..5a8d2df980f1 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1447,17 +1447,6 @@ private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(new OverlayManagerService(mSystemContext)); t.traceEnd(); - // latency test ONLY - // this is probably no-op, intializes the tricky store instance for system server - // for system use cases - nte: maybe not needed at all, since keystore/cert requests are per-app context - t.traceBegin("StartTrickyStoreService"); - try { - android.security.trickystore.TrickyStoreService.getInstance().initialize(); - } catch (Throwable e) { - Slog.e(TAG, "Failed to initialize TrickyStoreService", e); - } - t.traceEnd(); - t.traceBegin("InitVBMetaDigest"); try { android.security.trickystore.AttestationUtils.initBootHash(); From 14b7b76e1a3f0ef81fd504075e73f8bdd084c0c6 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 19 Apr 2026 12:40:53 +0530 Subject: [PATCH 1097/1315] core: Improve parsing keybox xml with StringBuilder * Accumulate TEXT into a StringBuilder and flush on END_TAG. * Only log successful when keybox manager actually has keybox loaded. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../security/trickystore/KeyBoxManager.java | 32 +++++++++++++++---- .../trickystore/TrickyStoreService.java | 10 ++++-- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/core/java/android/security/trickystore/KeyBoxManager.java b/core/java/android/security/trickystore/KeyBoxManager.java index b7796b39c6c9..8731429830be 100644 --- a/core/java/android/security/trickystore/KeyBoxManager.java +++ b/core/java/android/security/trickystore/KeyBoxManager.java @@ -89,6 +89,8 @@ public void parseKeybox(String xmlContent) { String currentAlgorithm = null; String privateKeyPem = null; List certificatePems = new ArrayList<>(); + StringBuilder privateKeyBuilder = null; + StringBuilder certBuilder = null; boolean inKey = false; boolean inPrivateKey = false; boolean inCertificateChain = false; @@ -106,30 +108,48 @@ public void parseKeybox(String xmlContent) { currentAlgorithm = parser.getAttributeValue(null, "algorithm"); privateKeyPem = null; certificatePems.clear(); + privateKeyBuilder = null; + certBuilder = null; } else if ("PrivateKey".equals(tagName) && inKey) { inPrivateKey = true; + privateKeyBuilder = new StringBuilder(); } else if ("CertificateChain".equals(tagName) && inKey) { inCertificateChain = true; } else if ("Certificate".equals(tagName) && inCertificateChain) { inCertificate = true; + certBuilder = new StringBuilder(); } break; case XmlPullParser.TEXT: - String text = parser.getText().trim(); - if (!text.isEmpty()) { - if (inPrivateKey) { - privateKeyPem = text; - } else if (inCertificate) { - certificatePems.add(text); + String text = parser.getText(); + if (text != null) { + if (inPrivateKey && privateKeyBuilder != null) { + privateKeyBuilder.append(text); + } else if (inCertificate && certBuilder != null) { + certBuilder.append(text); } } break; case XmlPullParser.END_TAG: if ("PrivateKey".equals(tagName)) { + if (privateKeyBuilder != null) { + String pem = privateKeyBuilder.toString().trim(); + if (!pem.isEmpty()) { + privateKeyPem = pem; + } + privateKeyBuilder = null; + } inPrivateKey = false; } else if ("Certificate".equals(tagName)) { + if (certBuilder != null) { + String pem = certBuilder.toString().trim(); + if (!pem.isEmpty()) { + certificatePems.add(pem); + } + certBuilder = null; + } inCertificate = false; } else if ("CertificateChain".equals(tagName)) { inCertificateChain = false; diff --git a/core/java/android/security/trickystore/TrickyStoreService.java b/core/java/android/security/trickystore/TrickyStoreService.java index c2d6f928cae6..fd9c3ebcf387 100644 --- a/core/java/android/security/trickystore/TrickyStoreService.java +++ b/core/java/android/security/trickystore/TrickyStoreService.java @@ -200,9 +200,15 @@ public void refreshKeyBox() { } try { mKeyBoxManager.parseKeybox(xml); - mLastKeyboxFingerprint = fingerprint; - Log.i(TAG, "Keybox updated successfully"); + if (mKeyBoxManager.hasKeyboxes()) { + mLastKeyboxFingerprint = fingerprint; + Log.i(TAG, "Keybox updated successfully"); + } else { + mLastKeyboxFingerprint = null; + Log.e(TAG, "Keybox parse produced no usable entries"); + } } catch (Exception e) { + mLastKeyboxFingerprint = null; Log.e(TAG, "Failed to update keybox", e); } } From 16814727c54091c98de6e84d1dac34ebd5e8bb25 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 19 Apr 2026 12:50:18 +0530 Subject: [PATCH 1098/1315] core: Fix and remove deprecated api in certificate generator * GET_SIGNATURES has been deprecated since API 28. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../trickystore/CertificateGenerator.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/java/android/security/trickystore/CertificateGenerator.java b/core/java/android/security/trickystore/CertificateGenerator.java index 3c949c9817fa..5628adb01f2f 100644 --- a/core/java/android/security/trickystore/CertificateGenerator.java +++ b/core/java/android/security/trickystore/CertificateGenerator.java @@ -5,6 +5,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.pm.SigningInfo; import android.util.Log; import com.android.internal.org.bouncycastle.asn1.ASN1Boolean; @@ -278,6 +279,17 @@ private static Extension buildAttestExtension(KeyGenParameters params, int secur } } + private static Signature[] extractSignatures(PackageInfo info) { + SigningInfo signingInfo = info.signingInfo; + if (signingInfo != null) { + if (signingInfo.hasMultipleSigners()) { + return signingInfo.getApkContentsSigners(); + } + return signingInfo.getSigningCertificateHistory(); + } + return info.signatures; + } + private static DEROctetString createApplicationId(int uid) throws Throwable { Context context = ActivityThread.currentApplication(); if (context == null) { @@ -301,14 +313,15 @@ private static DEROctetString createApplicationId(int uid) throws Throwable { for (int i = 0; i < size; i++) { String name = packages[i]; - PackageInfo info = pm.getPackageInfo(name, PackageManager.GET_SIGNATURES); + PackageInfo info = pm.getPackageInfo(name, PackageManager.GET_SIGNING_CERTIFICATES); ASN1Encodable[] arr = new ASN1Encodable[2]; arr[0] = new DEROctetString(name.getBytes(StandardCharsets.UTF_8)); arr[1] = new ASN1Integer(info.getLongVersionCode()); packageInfoAA[i] = new DERSequence(arr); - if (info.signatures != null) { - for (Signature s : info.signatures) { + Signature[] pkgSigs = extractSignatures(info); + if (pkgSigs != null) { + for (Signature s : pkgSigs) { signatures.add(ByteBuffer.wrap(dg.digest(s.toByteArray()))); } } From 40085e1fe0c3f90674748807570e04c9c7955784 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 19 Apr 2026 12:40:53 +0530 Subject: [PATCH 1099/1315] core: Sanitize keybox xml before loading * Some notorious providers are unnecessarily adding comments to show off. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/security/trickystore/KeyBoxManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/security/trickystore/KeyBoxManager.java b/core/java/android/security/trickystore/KeyBoxManager.java index 8731429830be..b0b3f247f07f 100644 --- a/core/java/android/security/trickystore/KeyBoxManager.java +++ b/core/java/android/security/trickystore/KeyBoxManager.java @@ -300,6 +300,7 @@ private String sanitizeXml(String content) { content = content.substring(bom.length()); } } + content = content.replaceAll("(?s)", ""); return content.trim(); } } From 2909a73dc02d108e2f4b5437a60227dd0b6ce159 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 23 Apr 2026 15:18:07 +0900 Subject: [PATCH 1100/1315] core: PIF: align spoofing behavior with upstream PlayIntegrityFix - Always skip SDK_INT for Vending; spoofVendingSdk now only applies to DroidGuard, preventing API 34+ receiver crashes in Play Store - Spoof all other build fields to Vending to match the configured fingerprint, mirroring upstream behavior - Update spoofSdkInt() to read SDK_INT from config (fallback 32) and fix log suffix from "PS" to "DG" - Sync SECURITY_PATCH to ro.build.version.security_patch and ro.vendor.build.security_patch on config load so direct prop reads see the spoofed date, matching upstream resetprop behavior Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../pif/PlayIntegritySpoofService.java | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/core/java/android/security/pif/PlayIntegritySpoofService.java b/core/java/android/security/pif/PlayIntegritySpoofService.java index 9a3e39812441..3c28e450cbbb 100644 --- a/core/java/android/security/pif/PlayIntegritySpoofService.java +++ b/core/java/android/security/pif/PlayIntegritySpoofService.java @@ -116,6 +116,13 @@ public void loadConfig() { Log.i(TAG, "PIF config loaded, fields=" + mBuildFields.size() + ", props=" + mSystemProps.size()); + // Sync SECURITY_PATCH to system props so apps reading these directly + // see the spoofed date, matching what the upstream module does via resetprop. + String secPatch = mBuildFields.get("SECURITY_PATCH"); + if (secPatch != null && !secPatch.isEmpty()) { + mSystemProps.put("ro.build.version.security_patch", secPatch); + mSystemProps.put("ro.vendor.build.security_patch", secPatch); + } } catch (Exception e) { Log.e(TAG, "Failed to load PIF config", e); } @@ -240,14 +247,13 @@ public void spoofBuildFields(String processName) { if (!isDroidGuard && !isVending) return; if (isVending) { - if (mSpoofVendingSdk) { - spoofSdkInt(); + if (!mSpoofVendingBuild) { + if (mVerboseLogs > 0) Log.d(TAG, "Vending build spoofing disabled"); + return; } - if (mSpoofVendingBuild && !mSpoofVendingSdk) { - for (Map.Entry entry : mBuildFields.entrySet()) { - if ("SDK_INT".equals(entry.getKey())) continue; - spoofField(entry.getKey(), entry.getValue(), "PS"); - } + for (Map.Entry entry : mBuildFields.entrySet()) { + if ("SDK_INT".equals(entry.getKey())) continue; + spoofField(entry.getKey(), entry.getValue(), "PS"); } return; } @@ -260,6 +266,13 @@ public void spoofBuildFields(String processName) { for (Map.Entry entry : mBuildFields.entrySet()) { spoofField(entry.getKey(), entry.getValue(), "DG"); } + + // spoofVendingSdk when enabled applies an additional SDK_INT override + // specifically for DroidGuard to match legacy attestation paths. + // It has no effect on Vending. + if (mSpoofVendingSdk) { + spoofSdkInt(); + } } public void spoofSignature() { @@ -329,14 +342,23 @@ private Field findField(Class clazz, String fieldName) throws NoSuchFieldExce } private void spoofSdkInt() { + // Only called for DroidGuard. Reads SDK_INT from config so it stays + // consistent with the spoofed fingerprint. Falls back to 32 if unset. + String configSdk = mBuildFields.get("SDK_INT"); + int targetSdk; + try { + targetSdk = (configSdk != null) ? Integer.parseInt(configSdk) : 32; + } catch (NumberFormatException e) { + targetSdk = 32; + } + try { Field field = Build.VERSION.class.getDeclaredField("SDK_INT"); field.setAccessible(true); int oldValue = field.getInt(null); - int targetSdk = Math.min(oldValue, 32); if (oldValue != targetSdk) { field.set(null, targetSdk); - Log.d(TAG + "/Java:PS", "[SDK_INT]: " + oldValue + " -> " + targetSdk); + Log.d(TAG + "/Java:DG", "[SDK_INT]: " + oldValue + " -> " + targetSdk); } field.setAccessible(false); } catch (Exception e) { @@ -351,8 +373,8 @@ private void spoofField(String fieldName, String value, String logSuffix) { } try { - Field field = null; - String oldValue = null; + Field field; + String oldValue; if (hasField(Build.class, fieldName)) { field = Build.class.getDeclaredField(fieldName); From 9dba9b98bbf5c7c5ef90598c03970b1fde98efaf Mon Sep 17 00:00:00 2001 From: Joey Date: Sat, 25 Apr 2026 20:10:08 +0900 Subject: [PATCH 1101/1315] GamePropsSpoofService: absorb PerAppsPropsUtils Consolidates per-app device spoofing into GamePropsSpoofService, eliminating PerAppsPropsUtils as a separate class. GamePropsSpoofService now handles two independent spoof sources: 1. JSON game-props config (existing behaviour, keyed by package name with arbitrary Build field overrides, stored via system_server). 2. Per-app device profile map (formerly PerAppsPropsUtils), which reads Settings.Secure.PER_APPS_DEVICE_SPOOF and applies a named built-in or custom profile. Custom profiles are loaded from Settings.Secure.CUSTOM_SPOOF_PROFILES at runtime. Source 1 takes priority; if a package has a JSON game-props entry the per-app path is skipped for that package. Built-in device profiles are now stored as a public static map (BUILTIN_PROFILES) inside GamePropsSpoofService so both the enforcement layer and the UI layer share a single source of truth. The call site in ActivityThread#handleBindApplication is simplified to an unconditional spoofForPackage(packageName, appContext); all enable/disable gating is handled internally by the service. PerAppsPropsUtils.java is deleted. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 5 +- core/java/android/app/Instrumentation.java | 3 - .../gameprops/GamePropsSpoofService.java | 320 ++++++++++++--- .../util/matrixx/PerAppsPropsUtils.java | 375 ------------------ 4 files changed, 257 insertions(+), 446 deletions(-) delete mode 100644 core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e6e5f8616657..47df7a65b2b4 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -8016,10 +8016,7 @@ private void handleBindApplication(AppBindData data) { final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); mConfigurationController.updateLocaleListFromAppContext(appContext); - GamePropsSpoofService gamePropsService = GamePropsSpoofService.getInstance(); - if (gamePropsService.isEnabled()) { - gamePropsService.spoofForPackage(data.appInfo.packageName); - } + GamePropsSpoofService.getInstance().spoofForPackage(data.appInfo.packageName, appContext); PlayIntegritySpoofService pifService = PlayIntegritySpoofService.getInstance(); if (pifService.shouldSpoof(data.processName)) { diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 6cb0124c02af..ef6154fb5e9d 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -81,7 +81,6 @@ import java.util.concurrent.TimeoutException; import com.android.internal.util.matrixx.PixelPropsUtils; -import com.android.internal.util.matrixx.PerAppsPropsUtils; /** * Base class for implementing application instrumentation code. When running @@ -1363,7 +1362,6 @@ public Application newApplication(ClassLoader cl, String className, Context cont .instantiateApplication(cl, className); app.attach(context); PixelPropsUtils.setProps(context); - PerAppsPropsUtils.setProps(context); return app; } @@ -1383,7 +1381,6 @@ static public Application newApplication(Class clazz, Context context) Application app = (Application)clazz.newInstance(); app.attach(context); PixelPropsUtils.setProps(context); - PerAppsPropsUtils.setProps(context); return app; } diff --git a/core/java/android/security/gameprops/GamePropsSpoofService.java b/core/java/android/security/gameprops/GamePropsSpoofService.java index 04a66b7ba5a1..a6cbb8ae86ff 100644 --- a/core/java/android/security/gameprops/GamePropsSpoofService.java +++ b/core/java/android/security/gameprops/GamePropsSpoofService.java @@ -17,11 +17,18 @@ package android.security.gameprops; import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; import android.os.Build; import android.os.RemoteException; +import android.provider.Settings; +import android.text.TextUtils; import android.util.JsonReader; import android.util.Log; +import org.json.JSONArray; +import org.json.JSONObject; + import java.io.IOException; import java.io.StringReader; import java.lang.reflect.Field; @@ -30,23 +37,46 @@ import java.util.concurrent.ConcurrentHashMap; /** + * Unified device-props spoofing service. + * + * Handles two independent sources: + * + * 1. JSON game-props config (stored in Settings.Secure via system_server) — the original + * GamePropsSpoofService behaviour, keyed by package name with arbitrary Build field maps. + * + * 2. Per-app spoof map (stored in Settings.Secure.PER_APPS_DEVICE_SPOOF) — formerly + * PerAppsPropsUtils, maps a package name to a named device profile (e.g. "ROG8P"). + * Requires a Context to read Settings.Secure at spoof time. + * + * Source (1) takes priority: if a package has a JSON game-props entry that entry is applied + * and per-app lookup is skipped for that package. + * * @hide */ public final class GamePropsSpoofService { + private static final String TAG = "GameProps"; - private static GamePropsSpoofService sInstance; + // ------------------------------------------------------------------------- + // Settings keys (mirrors Settings.Secure constants used by the UI) + // ------------------------------------------------------------------------- - private volatile boolean mEnabled = false; - private volatile boolean mDebug = false; - private final Map> mGameConfigs = new ConcurrentHashMap<>(); - private volatile boolean mConfigLoaded = false; + /** Comma-separated "pkg:profileId" pairs for the active per-app spoof map. */ + private static final String SETTING_PER_APPS = "per_apps_device_spoof"; + /** Master enable flag for per-app spoofing (int, default 1). */ + private static final String SETTING_PER_APPS_ENABLED = "per_apps_device_spoof_enabled"; + /** JSON array of custom user-defined profiles. */ + private static final String SETTING_CUSTOM_PROFILES = "custom_spoof_profiles"; + + // ------------------------------------------------------------------------- + // Singleton + // ------------------------------------------------------------------------- + + private static GamePropsSpoofService sInstance; private GamePropsSpoofService() {} - /** - * @hide - */ + /** @hide */ public static synchronized GamePropsSpoofService getInstance() { if (sInstance == null) { sInstance = new GamePropsSpoofService(); @@ -55,12 +85,78 @@ public static synchronized GamePropsSpoofService getInstance() { return sInstance; } + // ------------------------------------------------------------------------- + // JSON game-props state (source 1) + // ------------------------------------------------------------------------- + + private volatile boolean mEnabled = false; + private volatile boolean mDebug = false; + private volatile boolean mConfigLoaded = false; + + /** packageName → { fieldName → value } from JSON game-props config */ + private final Map> mGameConfigs = new ConcurrentHashMap<>(); + + // ------------------------------------------------------------------------- + // Built-in device profiles (formerly in PerAppsPropsUtils static block) + // ------------------------------------------------------------------------- + /** + * Built-in named profiles. Keys match the profile IDs used by + * UserSelectedAppSpoofSettings / PerAppsPropsUtils. + * * @hide */ + public static final Map> BUILTIN_PROFILES; + + static { + BUILTIN_PROFILES = new HashMap<>(); + BUILTIN_PROFILES.put("BS4C", profile("Black Shark", "2SM-X706B", "Xiaomi", "2SM-X706B", "BlackShark/PRS-H0/Black Shark 4:13/TQ3A.230805.001/20230315:user/release-keys", "2SM-X706B")); + BUILTIN_PROFILES.put("F5", profile("Xiaomi", "23049PCD8G", "Xiaomi", "marble", "Xiaomi/marble_global/marble:14/UKQ1.230917.001/V816.0.2.0.UMRMIXM:user/release-keys", "marble")); + BUILTIN_PROFILES.put("GZF5", profile("samsung", "SM-F9460", "samsung", "Galaxy Z Fold 5", "samsung/q2qzh/q2q:15/UP1A.231005.007/F946BXXU1BWK4:user/release-keys", "SM-F9460")); + BUILTIN_PROFILES.put("HMV2R", profile("HONOR", "VER-N49DP", "HONOR", "Honor Magic V2 RSR", "HONOR/VER-N49DP/VER:13/ENG.20240918.123456:user/release-keys", "VER-N49DP")); + BUILTIN_PROFILES.put("LY700", profile("Lenovo", "Lenovo TB-9707F", "Lenovo", "Lenovo Y700", null, null)); + BUILTIN_PROFILES.put("LY70023", profile("Lenovo", "TB-9707F", "Lenovo", "Legion Y700 (2023)", "Lenovo/TB-9707F/Lenovo TB-9707F:13/TQ3A.230805.001/20230901:user/release-keys", "TB-9707F")); + BUILTIN_PROFILES.put("MI11TP", profile("Xiaomi", "2107113SG", "Xiaomi", "Xiaomi 11T Pro", "Xiaomi/2107113SI/Mi 11T Pro:13/RKQ1.211001.001/20230410:user/release-keys", "2107113SG")); + BUILTIN_PROFILES.put("MI13", profile("Xiaomi", "2211133G", "Xiaomi", "Xiaomi 13", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys", "2211133G")); + BUILTIN_PROFILES.put("MI13P", profile("Xiaomi", "2210132G", "Xiaomi", "Xiaomi 13 Pro", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys", "2210132G")); + BUILTIN_PROFILES.put("MI14P", profile("Xiaomi", "23116PN5BC", "Xiaomi", "houji", "Xiaomi/houji/houji:14/UKQ1.230917.001/V816.0.2.0.UNBCNXM:user/release-keys", "houji")); + BUILTIN_PROFILES.put("OP12", profile("OnePlus", "CPH2581", "OnePlus", "OP594DL1", "OnePlus/OP594DL1/OP594DL1:14/UKQ1.230917.001/1702951307528:user/release-keys", "OP594DL1")); + BUILTIN_PROFILES.put("OP13", profile("OnePlus", "PJZ110", "OnePlus", "OnePlus 13", "OnePlus/PJZ110/OP5D0DL1:15/AP3A.240617.008/V.1bd19a1-1-2:user/release-keys", "PJZ110")); + BUILTIN_PROFILES.put("OP8P5G", profile("OnePlus", "IN2023", "OnePlus", "OnePlus 8 Pro 5G", "OnePlus/IN2023/OnePlus8Pro:13/RKQ1.211119.001/20230501:user/release-keys", "IN2023")); + BUILTIN_PROFILES.put("PXL", profile("google", "marlin", "Google", "Pixel XL", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys", "marlin")); + BUILTIN_PROFILES.put("PXL10PXL",profile("google", "Pixel 10 Pro XL", "Google", "mustang", "google/mustang/mustang:16/CP1A.260305.018/14887507:user/release-keys", "mustang")); + BUILTIN_PROFILES.put("RM9P", profile("nubia", "NX769J", "ZTE", "REDMAGIC 9 Pro", "nubia/NX769J/NX769J:14/UKQ1.230917.001/20240813.173312:user/release-keys", "NX769J")); + BUILTIN_PROFILES.put("RM10P", profile("nubia", "NX789J", "ZTE", "RedMagic 10 Pro", "nubia/NX789J-UN/NX789J:15/AQ3A.240812.002/20241212.194919:user/release-keys", "NX789J")); + BUILTIN_PROFILES.put("RM15P5G", profile("realme", "RMX5101", "realme", "Realme 15 Pro 5G", "realme/RMX5101IN/RE60B4L1:15/AP3A.240617.008/V.R4T2.26cec0e-80bb4e-80b757:user/release-keys", "RMX5101")); + BUILTIN_PROFILES.put("RMX14", profile("realme", "RMX5070", "realme", "Realme 14", null, null)); + BUILTIN_PROFILES.put("RMP35G", profile("realme", "RMX5070", "realme", "Realme P3 5G", "realme/RMX5070/RMX5070:15/SKQ1.230119.001/eng.user.20250415.155201:user/release-keys", "RMX5070")); + BUILTIN_PROFILES.put("ROG6DU", profile("ASUS", "AI2203", "ASUS", "ROG Phone 6D Ultimate", "ASUS/AI2203/ROG Phone 6D:14/UP1A.231005.007/20240315:user/release-keys", "AI2203")); + BUILTIN_PROFILES.put("ROG8P", profile("asus", "ASUS_AI2401_D", "asus", "ASUS_AI2401_D", "asus/ASUS_AI2401_D/ASUS_AI2401:14/UKQ1.230804.001/34.0210.0210.222-0:user/release-keys", "ASUS_AI2401_D")); + BUILTIN_PROFILES.put("ROG9P", profile("Asus", "ASUS_AI2501", "Asus", "ROG Phone 9 PRO", null, null)); + BUILTIN_PROFILES.put("S25U", profile("Samsung", "SM-S938B", "samsung", "Samsung S25 Ultra", null, null)); + } + + /** Convenience builder for a profile map. Null fp/product are skipped. */ + private static Map profile(String brand, String model, String manufacturer, + String device, String fingerprint, String product) { + Map m = new HashMap<>(); + m.put("BRAND", brand); + m.put("MODEL", model); + m.put("MANUFACTURER", manufacturer); + m.put("DEVICE", device); + if (fingerprint != null) m.put("FINGERPRINT", fingerprint); + if (product != null) m.put("PRODUCT", product); + return m; + } + + // ------------------------------------------------------------------------- + // JSON config loading (source 1) + // ------------------------------------------------------------------------- + + /** @hide */ public void loadConfig() { mGameConfigs.clear(); - mEnabled = false; + mEnabled = false; mConfigLoaded = false; String content; @@ -79,7 +175,8 @@ public void loadConfig() { try { parseJson(content); mConfigLoaded = true; - Log.i(TAG, "Game props config loaded, games=" + mGameConfigs.size() + ", enabled=" + mEnabled); + Log.i(TAG, "Game props config loaded, games=" + mGameConfigs.size() + + ", enabled=" + mEnabled); } catch (Exception e) { Log.e(TAG, "Failed to parse game props config", e); } @@ -90,7 +187,6 @@ private void parseJson(String content) { reader.beginObject(); while (reader.hasNext()) { String key = reader.nextName(); - if ("enabled".equals(key)) { mEnabled = reader.nextBoolean(); } else if ("debug".equals(key)) { @@ -112,61 +208,169 @@ private void parseGames(JsonReader reader) throws IOException { while (reader.hasNext()) { String packageName = reader.nextName(); Map gameProps = new HashMap<>(); - reader.beginObject(); while (reader.hasNext()) { - String propKey = reader.nextName(); - String propValue = reader.nextString(); - gameProps.put(propKey, propValue); + gameProps.put(reader.nextName(), reader.nextString()); } reader.endObject(); - if (!gameProps.isEmpty()) { mGameConfigs.put(packageName, gameProps); - if (mDebug) { - Log.d(TAG, "Loaded config for " + packageName + ": " + gameProps.size() + " props"); - } + if (mDebug) Log.d(TAG, "Loaded config for " + packageName + + ": " + gameProps.size() + " props"); } } reader.endObject(); } + // ------------------------------------------------------------------------- + // Public spoof entry points + // ------------------------------------------------------------------------- + + /** + * Apply spoofing for the given package. + * + * Checks JSON game-props first (source 1). If no entry is found there, + * falls back to the per-app Settings.Secure map (source 2) when a Context + * is supplied. + * + * @hide + */ + public void spoofForPackage(String packageName, Context context) { + if (packageName == null) return; + + // Source 1 — JSON game-props config + if (mEnabled && mConfigLoaded) { + Map gameProps = mGameConfigs.get(packageName); + if (gameProps != null && !gameProps.isEmpty()) { + if (mDebug) Log.d(TAG, "Spoofing via game-props for: " + packageName); + for (Map.Entry entry : gameProps.entrySet()) { + spoofField(entry.getKey(), entry.getValue(), packageName); + } + return; // source 1 wins; skip per-app lookup + } + } + + // Source 2 — per-app Settings.Secure map (formerly PerAppsPropsUtils) + if (context != null) { + applyPerAppSpoof(packageName, context); + } + } + /** + * Convenience overload for callers that only have the JSON game-props path + * (no Context available). + * * @hide */ public void spoofForPackage(String packageName) { - if (!mEnabled || !mConfigLoaded || packageName == null) { + spoofForPackage(packageName, null); + } + + // ------------------------------------------------------------------------- + // Per-app spoof logic (source 2 — formerly PerAppsPropsUtils.setProps) + // ------------------------------------------------------------------------- + + private void applyPerAppSpoof(String packageName, Context context) { + // Check master enable flag + try { + int enabled = Settings.Secure.getInt( + context.getContentResolver(), SETTING_PER_APPS_ENABLED, 1); + if (enabled == 0) return; + } catch (Exception e) { + if (mDebug) Log.d(TAG, "Could not read per-apps enabled flag: " + e.getMessage()); return; } - Map gameProps = mGameConfigs.get(packageName); - if (gameProps == null || gameProps.isEmpty()) { - if (mDebug) { - Log.d(TAG, "No config found for package: " + packageName); + // Read active map: "pkg1:profileId1,pkg2:profileId2,..." + String spoofedApps; + try { + spoofedApps = Settings.Secure.getString( + context.getContentResolver(), SETTING_PER_APPS); + } catch (Exception e) { + if (mDebug) Log.d(TAG, "Failed to read per-apps setting: " + e.getMessage()); + return; + } + + if (TextUtils.isEmpty(spoofedApps)) return; + + // Find this package's assigned profile ID + String profileId = null; + for (String entry : spoofedApps.split(",")) { + String[] parts = entry.split(":"); + if (parts.length == 2 && packageName.equals(parts[0])) { + profileId = parts[1]; + break; } + } + if (profileId == null) return; + + // Build combined profile map: built-ins + custom user profiles + Map> allProfiles = new HashMap<>(BUILTIN_PROFILES); + loadCustomProfiles(context, allProfiles); + + Map props = allProfiles.get(profileId); + if (props == null) { + if (mDebug) Log.d(TAG, "Unknown profile id '" + profileId + + "' for package " + packageName); return; } - if (mDebug) { - Log.d(TAG, "Spoofing props for game: " + packageName); + if (mDebug) Log.d(TAG, "Per-app spoof: " + packageName + " → " + profileId); + for (Map.Entry prop : props.entrySet()) { + Object v = prop.getValue(); + if (v != null) spoofField(prop.getKey(), v.toString(), packageName); } + } - for (Map.Entry entry : gameProps.entrySet()) { - spoofField(entry.getKey(), entry.getValue(), packageName); + /** + * Reads custom profiles from Settings.Secure and merges them into {@code out}. + * Any profile whose id collides with a built-in is skipped (built-ins win). + */ + private void loadCustomProfiles(Context context, Map> out) { + String json; + try { + json = Settings.Secure.getString( + context.getContentResolver(), SETTING_CUSTOM_PROFILES); + } catch (Exception e) { + return; + } + if (TextUtils.isEmpty(json)) return; + + try { + JSONArray arr = new JSONArray(json); + for (int i = 0; i < arr.length(); i++) { + JSONObject obj = arr.getJSONObject(i); + String id = obj.getString("id"); + if (out.containsKey(id)) continue; // built-in takes priority + + Map props = new HashMap<>(); + props.put("BRAND", obj.optString("brand", "")); + props.put("MANUFACTURER", obj.optString("manufacturer", "")); + props.put("DEVICE", obj.optString("device", "")); + props.put("MODEL", obj.optString("model", "")); + String fp = obj.optString("fingerprint", ""); + String prod = obj.optString("product", ""); + if (!TextUtils.isEmpty(fp)) props.put("FINGERPRINT", fp); + if (!TextUtils.isEmpty(prod)) props.put("PRODUCT", prod); + out.put(id, props); + } + } catch (Exception e) { + Log.e(TAG, "Failed to parse custom spoof profiles", e); } } + // ------------------------------------------------------------------------- + // Reflection helpers + // ------------------------------------------------------------------------- + private void spoofField(String fieldName, String value, String packageName) { if (value == null || value.isEmpty()) { if (mDebug) Log.d(TAG, fieldName + " is empty, skipping"); return; } - try { - Field field = getField(Build.class, fieldName); - if (field == null) { - field = getField(Build.VERSION.class, fieldName); - } + Field field = getDeclaredField(Build.class, fieldName); + if (field == null) field = getDeclaredField(Build.VERSION.class, fieldName); if (field == null) { if (mDebug) Log.d(TAG, "Field not found: " + fieldName); return; @@ -174,71 +378,59 @@ private void spoofField(String fieldName, String value, String packageName) { field.setAccessible(true); String oldValue = String.valueOf(field.get(null)); - if (value.equals(oldValue)) { if (mDebug) Log.d(TAG, "[" + fieldName + "]: " + value + " (unchanged)"); return; } - Class fieldType = field.getType(); + Class type = field.getType(); Object newValue; - - if (fieldType == String.class) { - newValue = value; - } else if (fieldType == int.class) { - newValue = Integer.parseInt(value); - } else if (fieldType == long.class) { - newValue = Long.parseLong(value); - } else if (fieldType == boolean.class) { - newValue = Boolean.parseBoolean(value); - } else { - Log.w(TAG, "Unsupported field type: " + fieldType); + if (type == String.class) newValue = value; + else if (type == int.class) newValue = Integer.parseInt(value); + else if (type == long.class) newValue = Long.parseLong(value); + else if (type == boolean.class) newValue = Boolean.parseBoolean(value); + else { + Log.w(TAG, "Unsupported field type: " + type); return; } field.set(null, newValue); - - if (mDebug) { - Log.d(TAG, "[" + packageName + "][" + fieldName + "]: " + oldValue + " -> " + value); - } + if (mDebug) Log.d(TAG, "[" + packageName + "][" + fieldName + "]: " + + oldValue + " → " + value); } catch (Exception e) { Log.e(TAG, "Failed to spoof " + fieldName + " for " + packageName, e); } } - private Field getField(Class clazz, String fieldName) { + private static Field getDeclaredField(Class clazz, String name) { try { - return clazz.getDeclaredField(fieldName); + return clazz.getDeclaredField(name); } catch (NoSuchFieldException e) { return null; } } - /** - * @hide - */ + // ------------------------------------------------------------------------- + // Accessors (unchanged public API) + // ------------------------------------------------------------------------- + + /** @hide */ public boolean isEnabled() { return mEnabled && mConfigLoaded; } - /** - * @hide - */ + /** @hide */ public boolean hasConfigForPackage(String packageName) { return mGameConfigs.containsKey(packageName); } - /** - * @hide - */ + /** @hide */ public Map> getAllGameConfigs() { return new ConcurrentHashMap<>(mGameConfigs); } - /** - * @hide - */ + /** @hide */ public boolean isConfigLoaded() { return mConfigLoaded; } diff --git a/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java b/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java deleted file mode 100644 index e31e2b6cca5e..000000000000 --- a/core/java/com/android/internal/util/matrixx/PerAppsPropsUtils.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 kenway214 - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.android.internal.util.matrixx; - -import android.content.Context; -import android.content.ContentResolver; -import android.os.Build; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * @hide - * Per-apps device spoofing - */ -public final class PerAppsPropsUtils { - - private static final Map> propsToChange = new HashMap<>(); - - static { - propsToChange.put("BS4C", createBS4CProps()); - propsToChange.put("F5", createF5Props()); - propsToChange.put("GZF5", createGZF5Props()); - propsToChange.put("HMV2R", createHMV2RProps()); - propsToChange.put("LY700", createLY700Props()); - propsToChange.put("LY70023", createLY70023Props()); - propsToChange.put("MI11TP", createMI11TPProps()); - propsToChange.put("MI13", createMI13Props()); - propsToChange.put("MI13P", createMI13PProps()); - propsToChange.put("MI14P", createMI14PProps()); - propsToChange.put("OP12", createOP12Props()); - propsToChange.put("OP13", createOP13Props()); - propsToChange.put("OP8P5G", createOP8P5GProps()); - propsToChange.put("PXL", createPXLProps()); - propsToChange.put("PXL10PXL", createPXL10PXLProps()); - propsToChange.put("RM9P", createRM9PProps()); - propsToChange.put("RM10P", createRM10PProps()); - propsToChange.put("RM15P5G", createRM15P5GProps()); - propsToChange.put("RMX14", createRMX14Props()); - propsToChange.put("RMP35G", createRMP35GProps()); - propsToChange.put("ROG6DU", createROG6DUProps()); - propsToChange.put("ROG8P", createROG8PProps()); - propsToChange.put("ROG9P", createROG9PProps()); - propsToChange.put("S25U", createS25UProps()); - } - - private static Map createBS4CProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Black Shark"); - props.put("DEVICE", "Black Shark 4 (China)"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("MODEL", "2SM-X706B"); - props.put("FINGERPRINT", "BlackShark/PRS-H0/Black Shark 4:13/TQ3A.230805.001/20230315:user/release-keys"); - props.put("PRODUCT", "2SM-X706B"); - return props; - } - - private static Map createF5Props() { - Map props = new HashMap<>(); - props.put("BRAND", "Xiaomi"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("DEVICE", "marble"); - props.put("MODEL", "23049PCD8G"); - props.put("FINGERPRINT", "Xiaomi/marble_global/marble:14/UKQ1.230917.001/V816.0.2.0.UMRMIXM:user/release-keys"); - props.put("PRODUCT", "marble"); - return props; - } - - private static Map createGZF5Props() { - Map props = new HashMap<>(); - props.put("BRAND", "samsung"); - props.put("DEVICE", "Galaxy Z Fold 5"); - props.put("MANUFACTURER", "samsung"); - props.put("MODEL", "SM-F9460"); - props.put("FINGERPRINT", "samsung/q2qzh/q2q:15/UP1A.231005.007/F946BXXU1BWK4:user/release-keys"); - props.put("PRODUCT", "SM-F9460"); - return props; - } - - private static Map createHMV2RProps() { - Map props = new HashMap<>(); - props.put("BRAND", "HONOR"); - props.put("DEVICE", "Honor Magic V2 RSR"); - props.put("MANUFACTURER", "HONOR"); - props.put("MODEL", "VER-N49DP"); - props.put("FINGERPRINT", "HONOR/VER-N49DP/VER:13/ENG.20240918.123456:user/release-keys"); - props.put("PRODUCT", "VER-N49DP"); - return props; - } - - private static Map createLY700Props() { - Map props = new HashMap<>(); - props.put("BRAND", "Lenovo"); - props.put("DEVICE", "Lenovo Y700"); - props.put("MANUFACTURER", "Lenovo"); - props.put("MODEL", "Lenovo TB-9707F"); - return props; - } - - private static Map createLY70023Props() { - Map props = new HashMap<>(); - props.put("BRAND", "Lenovo"); - props.put("DEVICE", "Legion Y700 (2023)"); - props.put("MANUFACTURER", "Lenovo"); - props.put("MODEL", "TB-9707F"); - props.put("FINGERPRINT", "Lenovo/TB-9707F/Lenovo TB-9707F:13/TQ3A.230805.001/20230901:user/release-keys"); - props.put("PRODUCT", "TB-9707F"); - return props; - } - - private static Map createMI11TPProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Xiaomi"); - props.put("DEVICE", "Xiaomi 11T Pro"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("MODEL", "2107113SG"); - props.put("FINGERPRINT", "Xiaomi/2107113SI/Mi 11T Pro:13/RKQ1.211001.001/20230410:user/release-keys"); - props.put("PRODUCT", "2107113SG"); - return props; - } - - private static Map createMI13Props() { - Map props = new HashMap<>(); - props.put("BRAND", "Xiaomi"); - props.put("DEVICE", "Xiaomi 13"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("MODEL", "2211133G"); - props.put("FINGERPRINT", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys"); - props.put("PRODUCT", "2211133G"); - return props; - } - - private static Map createMI13PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Xiaomi"); - props.put("DEVICE", "Xiaomi 13 Pro"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("MODEL", "2210132G"); - props.put("FINGERPRINT", "Xiaomi/fuxi_eea/fuxi:13/TKQ1.221114.001/OS2.0.102.0.VMCEUXM:user/release-keys"); - props.put("PRODUCT", "2210132G"); - return props; - } - - private static Map createMI14PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Xiaomi"); - props.put("MANUFACTURER", "Xiaomi"); - props.put("DEVICE", "houji"); - props.put("MODEL", "23116PN5BC"); - props.put("FINGERPRINT", "Xiaomi/houji/houji:14/UKQ1.230917.001/V816.0.2.0.UNBCNXM:user/release-keys"); - props.put("PRODUCT", "houji"); - return props; - } - - private static Map createOP12Props() { - Map props = new HashMap<>(); - props.put("BRAND", "OnePlus"); - props.put("MANUFACTURER", "OnePlus"); - props.put("DEVICE", "OP594DL1"); - props.put("MODEL", "CPH2581"); - props.put("FINGERPRINT", "OnePlus/OP594DL1/OP594DL1:14/UKQ1.230917.001/1702951307528:user/release-keys"); - props.put("PRODUCT", "OP594DL1"); - return props; - } - - private static Map createOP13Props() { - Map props = new HashMap<>(); - props.put("BRAND", "OnePlus"); - props.put("MANUFACTURER", "OnePlus"); - props.put("DEVICE", "OnePlus 13"); - props.put("MODEL", "PJZ110"); - props.put("FINGERPRINT", "OnePlus/PJZ110/OP5D0DL1:15/AP3A.240617.008/V.1bd19a1-1-2:user/release-keys"); - props.put("PRODUCT", "PJZ110"); - return props; - } - - private static Map createOP8P5GProps() { - Map props = new HashMap<>(); - props.put("BRAND", "OnePlus"); - props.put("DEVICE", "OnePlus 8 Pro 5G"); - props.put("MANUFACTURER", "OnePlus"); - props.put("MODEL", "IN2023"); - props.put("FINGERPRINT", "OnePlus/IN2023/OnePlus8Pro:13/RKQ1.211119.001/20230501:user/release-keys"); - props.put("PRODUCT", "IN2023"); - return props; - } - - private static Map createPXLProps() { - Map props = new HashMap<>(); - props.put("BRAND", "google"); - props.put("DEVICE", "Pixel XL"); - props.put("MANUFACTURER", "Google"); - props.put("MODEL", "marlin"); - props.put("FINGERPRINT", "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys"); - props.put("PRODUCT", "marlin"); - return props; - } - - private static Map createPXL10PXLProps() { - Map props = new HashMap<>(); - props.put("BRAND", "google"); - props.put("MANUFACTURER", "Google"); - props.put("DEVICE", "mustang"); - props.put("MODEL", "Pixel 10 Pro XL"); - props.put("FINGERPRINT", "google/mustang/mustang:16/CP1A.260305.018/14887507:user/release-keys"); - props.put("PRODUCT", "mustang"); - return props; - } - - private static Map createRM9PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "nubia"); - props.put("DEVICE", "REDMAGIC 9 Pro"); - props.put("MANUFACTURER", "ZTE"); - props.put("MODEL", "NX769J"); - props.put("FINGERPRINT", "nubia/NX769J/NX769J:14/UKQ1.230917.001/20240813.173312:user/release-keys"); - props.put("PRODUCT", "NX769J"); - return props; - } - - private static Map createRM10PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "nubia"); - props.put("DEVICE", "RedMagic 10 Pro"); - props.put("MANUFACTURER", "ZTE"); - props.put("MODEL", "NX789J"); - props.put("FINGERPRINT", "nubia/NX789J-UN/NX789J:15/AQ3A.240812.002/20241212.194919:user/release-keys"); - props.put("PRODUCT", "NX789J"); - return props; - } - - private static Map createRM15P5GProps() { - Map props = new HashMap<>(); - props.put("BRAND", "realme"); - props.put("DEVICE", "Realme 15 Pro 5G"); - props.put("MANUFACTURER", "realme"); - props.put("MODEL", "RMX5101"); - props.put("FINGERPRINT", "realme/RMX5101IN/RE60B4L1:15/AP3A.240617.008/V.R4T2.26cec0e-80bb4e-80b757:user/release-keys"); - props.put("PRODUCT", "RMX5101"); - return props; - } - - private static Map createRMX14Props() { - Map props = new HashMap<>(); - props.put("BRAND", "realme"); - props.put("DEVICE", "Realme 14"); - props.put("MANUFACTURER", "realme"); - props.put("MODEL", "RMX5070"); - return props; - } - - private static Map createRMP35GProps() { - Map props = new HashMap<>(); - props.put("BRAND", "realme"); - props.put("DEVICE", "Realme P3 5G"); - props.put("MANUFACTURER", "realme"); - props.put("MODEL", "RMX5070"); - props.put("FINGERPRINT", "realme/RMX5070/RMX5070:15/SKQ1.230119.001/eng.user.20250415.155201:user/release-keys"); - props.put("PRODUCT", "RMX5070"); - return props; - } - - private static Map createROG6DUProps() { - Map props = new HashMap<>(); - props.put("BRAND", "ASUS"); - props.put("DEVICE", "ROG Phone 6D Ultimate"); - props.put("MANUFACTURER", "ASUS"); - props.put("MODEL", "AI2203"); - props.put("FINGERPRINT", "ASUS/AI2203/ROG Phone 6D:14/UP1A.231005.007/20240315:user/release-keys"); - props.put("PRODUCT", "AI2203"); - return props; - } - - private static Map createROG8PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "asus"); - props.put("MANUFACTURER", "asus"); - props.put("DEVICE", "ASUS_AI2401_D"); - props.put("MODEL", "ASUS_AI2401_D"); - props.put("FINGERPRINT", "asus/ASUS_AI2401_D/ASUS_AI2401:14/UKQ1.230804.001/34.0210.0210.222-0:user/release-keys"); - props.put("PRODUCT", "ASUS_AI2401_D"); - return props; - } - - private static Map createROG9PProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Asus"); - props.put("DEVICE", "ROG Phone 9 PRO"); - props.put("MANUFACTURER", "Asus"); - props.put("MODEL", "ASUS_AI2501"); - return props; - } - - private static Map createS25UProps() { - Map props = new HashMap<>(); - props.put("BRAND", "Samsung"); - props.put("DEVICE", "Samsung S25 Ultra"); - props.put("MANUFACTURER", "samsung"); - props.put("MODEL", "SM-S938B"); - return props; - } - - public static void setProps(Context context) { - final String packageName = context.getPackageName(); - - if (TextUtils.isEmpty(packageName)) { - return; - } - - final String spoofedApps; - try { - final ContentResolver contentResolver = context.getContentResolver(); - spoofedApps = Settings.Secure.getString(contentResolver, Settings.Secure.PER_APPS_DEVICE_SPOOF); - } catch (Exception e) { - PixelPropsUtils.dlog("PerAppsPropsUtils: Failed to read spoofed apps setting: " + e.getMessage()); - return; - } - - if (TextUtils.isEmpty(spoofedApps)) { - return; - } - - Map> allProps = new HashMap<>(propsToChange); - try { - final ContentResolver contentResolver = context.getContentResolver(); - String customProfilesJson = Settings.Secure.getString(contentResolver, Settings.Secure.CUSTOM_SPOOF_PROFILES); - if (!TextUtils.isEmpty(customProfilesJson)) { - org.json.JSONArray jsonArray = new org.json.JSONArray(customProfilesJson); - for (int i = 0; i < jsonArray.length(); i++) { - org.json.JSONObject obj = jsonArray.getJSONObject(i); - String id = obj.getString("id"); - Map props = new HashMap<>(); - props.put("BRAND", obj.optString("brand", "")); - props.put("MANUFACTURER", obj.optString("manufacturer", "")); - props.put("DEVICE", obj.optString("device", "")); - props.put("MODEL", obj.optString("model", "")); - String fp = obj.optString("fingerprint", ""); - if (!TextUtils.isEmpty(fp)) props.put("FINGERPRINT", fp); - String prod = obj.optString("product", ""); - if (!TextUtils.isEmpty(prod)) props.put("PRODUCT", prod); - allProps.put(id, props); - } - } - } catch (Exception e) { - PixelPropsUtils.dlog("PerAppsPropsUtils: Failed to parse custom profiles: " + e.getMessage()); - } - - String[] apps = spoofedApps.split(","); - for (String app : apps) { - String[] values = app.split(":"); - if (values.length != 2) { - continue; - } - String pkg = values[0]; - String device = values[1]; - if (pkg.equals(packageName)) { - if (allProps.containsKey(device)) { - PixelPropsUtils.dlog("PerAppsPropsUtils: Applying profile for: " + packageName + " as " + device); - Map props = allProps.get(device); - for (Map.Entry prop : props.entrySet()) { - PixelPropsUtils.setPropValue(prop.getKey(), prop.getValue()); - } - } - break; - } - } - } -} From ec835f9492984874052db25fd4f02213206efde3 Mon Sep 17 00:00:00 2001 From: Joey Date: Wed, 29 Apr 2026 13:12:36 +0900 Subject: [PATCH 1102/1315] core: cache spoof settings via ContentObserver, avoid hot-path IPC reads getSystemAvailableFeatures(), hasSystemFeature(), and setProps() previously performed live Settings.Secure reads on every invocation, each crossing into SettingsProvider over Binder IPC. These values only change when the user flips a toggle, making per-call reads unnecessary overhead. Replace all per-call reads with static volatile fields populated once at startup and refreshed via ContentObserver. Safe defaults (tensor disabled, photos/pixel props spoof on, snapchat off) ensure correct behavior during early boot before SettingsProvider is available. ApplicationPackageManager: - Cache sTensorGlobalEnabled, sTensorTargets, sPhotosSpoofEnabled - Skip observer registration for isolated processes - Use getApplicationContext() for ContentResolver to avoid context leak PixelPropsUtils: - Cache sPhotosSpoofEnabled, sSnapchatSpoofEnabled, sPixelPropsSpoofEnabled - Compute sIsMainlineDevice once at class load - Add one-shot init(Context) called from setProps() - Use getApplicationContext() for ContentResolver to avoid context leak Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../app/ApplicationPackageManager.java | 73 ++++++++++-- .../util/matrixx/PixelPropsUtils.java | 108 ++++++++++-------- 2 files changed, 125 insertions(+), 56 deletions(-) diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index ea453082a1eb..2c38de7a837a 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -207,6 +207,12 @@ public class ApplicationPackageManager extends PackageManager { private final boolean mUseSystemFeaturesCache; + // Spoof setting caches — refreshed by ContentObserver, avoids per-call Settings reads + private static volatile boolean sTensorGlobalEnabled = false; + private static volatile Set sTensorTargets = null; + private static volatile boolean sPhotosSpoofEnabled = true; + private static boolean sSpoofObserverRegistered = false; + UserManager getUserManager() { if (mUserManager == null) { mUserManager = UserManager.get(mContext); @@ -795,10 +801,15 @@ public FeatureInfo[] getSystemAvailableFeatures() { final List list = new ArrayList<>(parceledList.getList()); // Inject Tensor features when toggle is enabled - final boolean forceTensor = !Process.isIsolated() && Settings.Secure.getInt( - mContext.getContentResolver(), Settings.Secure.PI_TENSOR_SPOOF, 0) == 1; + final String callingPkg = ActivityThread.currentPackageName(); + final Set targets = sTensorTargets; + final boolean forceTensor = !IS_TENSOR_DEVICE + && sTensorGlobalEnabled + && callingPkg != null + && targets != null + && targets.contains(callingPkg); - if (forceTensor && !IS_TENSOR_DEVICE) { + if (forceTensor) { for (String feature : FEATURES_TENSOR) { boolean exists = false; @@ -948,8 +959,7 @@ public boolean hasSystemFeature(String name, int version) { if (name != null && pkg != null && PRIV_PKGS.contains(pkg)) { final boolean photosSpoof = !Process.isIsolated() && "com.google.android.apps.photos".equals(pkg) - && (Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1); + && sPhotosSpoofEnabled; if (photosSpoof) { if (FEATURES_PIXEL.contains(name)) return false; if (FEATURES_PIXEL_OTHERS.contains(name)) return true; @@ -964,21 +974,21 @@ public boolean hasSystemFeature(String name, int version) { } if (name != null && FEATURES_TENSOR.contains(name)) { - final boolean forceTensor = !Process.isIsolated() && Settings.Secure.getInt( - mContext.getContentResolver(), Settings.Secure.PI_TENSOR_SPOOF, 0) == 1; - // Do not interfere with real Tensor devices if (IS_TENSOR_DEVICE) { return mHasSystemFeatureCache.query( new HasSystemFeatureQuery(name, version)); } - // Only override if user explicitly enabled the toggle - if (forceTensor) { + // Check cached tensor targets — populated once and refreshed via ContentObserver + final Set targets = sTensorTargets; + if (sTensorGlobalEnabled + && pkg != null + && targets != null + && targets.contains(pkg)) { return true; } - // Otherwise, behave like stock return mHasSystemFeatureCache.query( new HasSystemFeatureQuery(name, version)); } @@ -2446,6 +2456,47 @@ protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) { mContext = context; mPM = pm; mUseSystemFeaturesCache = isSystemFeaturesCacheAvailable(); + if (!Process.isIsolated()) { + registerSpoofSettingsObserver(); + } + } + + private void registerSpoofSettingsObserver() { + synchronized (ApplicationPackageManager.class) { + if (sSpoofObserverRegistered) return; + sSpoofObserverRegistered = true; + } + final ContentResolver cr = mContext.getContentResolver(); + final Runnable refresh = () -> { + try { + sTensorGlobalEnabled = Settings.Secure.getInt( + cr, "pi_tensor_spoof", 0) == 1; + final String raw = Settings.Secure.getString(cr, "tensor_targets"); + sTensorTargets = (raw == null || raw.isEmpty()) + ? Collections.emptySet() + : new ArraySet<>(Arrays.asList(raw.split(","))); + sPhotosSpoofEnabled = Settings.Secure.getInt( + cr, Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; + } catch (Throwable t) { + // Settings provider not ready yet; cache stays at safe defaults + } + }; + try { + final android.database.ContentObserver observer = + new android.database.ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { refresh.run(); } + }; + cr.registerContentObserver( + Settings.Secure.getUriFor("pi_tensor_spoof"), false, observer); + cr.registerContentObserver( + Settings.Secure.getUriFor("tensor_targets"), false, observer); + cr.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.PI_PHOTOS_SPOOF), false, observer); + } catch (Throwable t) { + // Observer registration failed; feature will remain disabled + } + refresh.run(); } private static boolean isSystemFeaturesCacheAvailable() { diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index 2eb544aa60df..dc25e6909838 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -2,7 +2,8 @@ * Copyright (C) 2020 The Pixel Experience Project * 2022 StatiXOS * 2021-2022 crDroid Android Project - * 2019-2026 Evolution X + * SPDX-FileCopyrightText: Evolution X + * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,8 +68,6 @@ public final class PixelPropsUtils { private static final String TAG = PixelPropsUtils.class.getSimpleName(); private static final boolean DEBUG = false; - private static final String sDeviceModel = - SystemProperties.get("ro.product.model", Build.MODEL); private static final String sDeviceFingerprint = SystemProperties.get("ro.product.fingerprint", Build.FINGERPRINT); @@ -148,6 +147,13 @@ public final class PixelPropsUtils { private static volatile String sProcessName; private static final boolean sIsCustomForkBuild = detectCustomFork(); + private static final boolean sIsMainlineDevice = detectMainlinePixelDevice(); + + // Cached spoof settings — refreshed by ContentObserver, avoids per-call Settings reads + private static volatile boolean sPhotosSpoofEnabled = true; + private static volatile boolean sSnapchatSpoofEnabled = false; + private static volatile boolean sPixelPropsSpoofEnabled = true; + private static volatile boolean sInitialized = false; private static boolean detectCustomFork() { char[] k = new char[]{'d','e','v','o','l','u','t','i','o','n'}; @@ -218,39 +224,59 @@ public static String getDeviceName(String fingerprint) { return ""; } + public static void init(Context context) { + if (sInitialized || Process.isIsolated() || context == null) return; + sInitialized = true; + registerSpoofSettingsObserver(context); + } + + private static void registerSpoofSettingsObserver(Context context) { + final ContentResolver cr = context.getContentResolver(); + final Runnable refresh = () -> { + try { + sPhotosSpoofEnabled = Settings.Secure.getInt( + cr, Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; + sSnapchatSpoofEnabled = Settings.Secure.getInt( + cr, Settings.Secure.PI_SNAPCHAT_SPOOF, 0) == 1; + sPixelPropsSpoofEnabled = Settings.Secure.getInt( + cr, Settings.Secure.PI_PP_SPOOF, 1) == 1; + } catch (Throwable t) { + // Settings provider not ready yet; cache stays at safe defaults + } + }; + try { + final android.database.ContentObserver observer = + new android.database.ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { refresh.run(); } + }; + cr.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.PI_PHOTOS_SPOOF), false, observer); + cr.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.PI_SNAPCHAT_SPOOF), false, observer); + cr.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.PI_PP_SPOOF), false, observer); + } catch (Throwable t) { + // Observer registration failed; cached defaults remain + } + refresh.run(); + } + private static boolean isGoogleCameraPackage(String packageName) { return packageName.contains("GoogleCamera") || customGoogleCameraPackages.contains(packageName); } - private static void applyAppSpecificProps(Context context, String packageName) { - if (context == null) return; - ContentResolver resolver = context.getContentResolver(); - if (resolver == null) return; - + private static void applyAppSpecificProps(String packageName) { if (packageName.equals(PACKAGE_PHOTOS)) { - boolean enabled = true; - try { - enabled = Settings.Secure.getInt( - resolver, - Settings.Secure.PI_PHOTOS_SPOOF, 1) == 1; - } catch (Throwable ignored) {} - - if (enabled) { + if (sPhotosSpoofEnabled) { sPixelXLProps.forEach(PixelPropsUtils::setPropValue); } return; } if (packageName.equals(PACKAGE_SNAPCHAT)) { - boolean enabled = false; - try { - enabled = Settings.Secure.getInt( - resolver, - Settings.Secure.PI_SNAPCHAT_SPOOF, 0) == 1; - } catch (Throwable ignored) {} - - if (enabled) { + if (sSnapchatSpoofEnabled) { sPixelXLProps.forEach(PixelPropsUtils::setPropValue); } } @@ -275,19 +301,18 @@ public static void setProps(Context context) { sProcessName = processName; + init(context); + Map propsToChange = new HashMap<>(); propsToChangeGeneric.forEach((k, v) -> setPropValue(k, v)); sIsExcluded = isGoogleCameraPackage(packageName); - boolean isMainlineDevice = isMainlinePixelDevice(); - boolean isPixelPropsEnabled = getSecureIntSafe(context, Settings.Secure.PI_PP_SPOOF, 1) == 1; - if (!sIsExcluded && packagesToChangeRecentPixel.contains(packageName) - && !isMainlineDevice - && isPixelPropsEnabled) { + && !sIsMainlineDevice + && sPixelPropsSpoofEnabled) { if (isDeviceTablet(context)) { propsToChange.putAll(propsToChangePixelTablet); @@ -317,7 +342,7 @@ public static void setProps(Context context) { setPropValue("FINGERPRINT", sDeviceFingerprint); return; } - applyAppSpecificProps(context, packageName); + applyAppSpecificProps(packageName); } private static boolean isDeviceTablet(Context context) { @@ -503,30 +528,23 @@ public static boolean shouldBypassBroadcastReceiverValidation(String packageName return false; } - private static int getSecureIntSafe(Context context, String key, int def) { - try { - if (context == null) return def; - ContentResolver resolver = context.getContentResolver(); - if (resolver == null) return def; - return Settings.Secure.getInt(resolver, key, def); - } catch (Throwable t) { - return def; - } + private static boolean detectMainlinePixelDevice() { + String model = SystemProperties.get("ro.product.model", "").trim(); + boolean isPixelSoC = "Google".equalsIgnoreCase( + SystemProperties.get("ro.soc.manufacturer")); + return isPixelSoC && MAINLINE_PIXEL_PATTERN.matcher(model).matches(); } public static boolean isMainlinePixelDevice() { - String model = SystemProperties.get("ro.product.model", ""); - boolean isPixelSoC = "Google".equalsIgnoreCase( - SystemProperties.get("ro.soc.manufacturer")); - return isPixelSoC && MAINLINE_PIXEL_PATTERN.matcher(model.trim()).matches(); + return sIsMainlineDevice; } public static boolean isTensorPixelDevice() { - String model = SystemProperties.get("ro.product.model", ""); + String model = SystemProperties.get("ro.product.model", "").trim(); // Tensor devices are always Google SoC boolean isPixelSoC = "Google".equalsIgnoreCase( SystemProperties.get("ro.soc.manufacturer")); - return isPixelSoC && TENSOR_PIXEL_PATTERN.matcher(model.trim()).matches(); + return isPixelSoC && TENSOR_PIXEL_PATTERN.matcher(model).matches(); } public static boolean isSupportedPixelDevice() { From 67b09d869dad22ac68ed33c31095f8c1a1d916af Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 5 May 2026 19:09:44 +0000 Subject: [PATCH 1103/1315] backport fix for stuck IME input from May 2026 Pixel update Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../inputmethod/InputMethodManagerService.java | 7 +++++++ .../android/server/wm/WindowManagerInternal.java | 2 ++ .../android/server/wm/WindowManagerService.java | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c7766ccca7b0..58161bf65e50 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1945,6 +1945,8 @@ InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boo session.mMethod.startInput(startInputToken, userData.mCurInputConnection, userData.mCurEditorInfo, restarting, navButtonFlags, userData.mCurImeBackCallbackReceiver); + final boolean isStale = focusedWindow != null && + mWindowManagerInternal.isImeInputTargetStaleForUpdate(focusedWindow); if (Flags.optimizeImeInputTargetUpdate()) { if (focusedWindow != null) { mWindowManagerInternal.updateImeTargetWindow(focusedWindow); @@ -1959,6 +1961,11 @@ InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boo SoftInputShowHideReason.ATTACH_NEW_INPUT, userId); userData.mCurStatsToken = null; showCurrentInputInternal(focusedWindow, statsToken); + } else if (isStale) { + var statsToken = createStatsTokenForFocusedClient(false, + SoftInputShowHideReason.HIDE_SOFT_INPUT, userId); + hideCurrentInputLocked(focusedWindow, statsToken, + SoftInputShowHideReason.HIDE_SOFT_INPUT, userId); } final var curId = bindingController.getCurId(); diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 4216ada8dd65..726c4eb3271b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -919,6 +919,8 @@ public abstract void setHomeSupportedOnDisplay( */ public abstract boolean isHomeSupportedOnDisplay(int displayId); + public abstract boolean isImeInputTargetStaleForUpdate(IBinder windowToken); + /** * Sets whether the relevant display content ignores fixed orientation, aspect ratio * and resizability of apps. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 2cf181d1aefe..0ee5de6840db 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8922,6 +8922,22 @@ public boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) { return WindowManagerService.this.shouldRestoreImeVisibility(imeTargetWindowToken); } + @Override + public boolean isImeInputTargetStaleForUpdate(IBinder windowToken) { + synchronized (mGlobalLock) { + InputTarget target = getInputTargetFromWindowTokenLocked(windowToken); + if (target != null && target.getDisplayContent() != null + && target.getDisplayContent().getImeInputTarget() != null + && target.getDisplayContent().getImeInputTarget() != target) { + WindowState ws = target.getDisplayContent().getImeInputTarget().getWindowState(); + if (ws != null && (ws.mRemoved || ws.mDestroying)) { + return true; + } + } + } + return false; + } + @Override public void addTrustedTaskOverlay(int taskId, SurfaceControlViewHost.SurfacePackage overlay) { From 1db9ca4545036cb18d7bcfb975caaac516a02814 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 7 May 2026 00:08:31 +0900 Subject: [PATCH 1104/1315] PixelPropsUtils: Update fingerprints to May 2026 release Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/internal/util/matrixx/PixelPropsUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java index dc25e6909838..666e9fe0426c 100644 --- a/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java +++ b/core/java/com/android/internal/util/matrixx/PixelPropsUtils.java @@ -192,8 +192,8 @@ public static boolean isCustomForkBuild() { propsToChangeRecentPixel.put("PRODUCT", "mustang"); propsToChangeRecentPixel.put("HARDWARE", "mustang"); propsToChangeRecentPixel.put("MODEL", "Pixel 10 Pro XL"); - propsToChangeRecentPixel.put("ID", "CP1A.260405.005"); - propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/CP1A.260405.005/15001963:user/release-keys"); + propsToChangeRecentPixel.put("ID", "CP1A.260505.005"); + propsToChangeRecentPixel.put("FINGERPRINT", "google/mustang/mustang:16/CP1A.260505.005/15081906:user/release-keys"); propsToChangePixelTablet = new HashMap<>(); propsToChangePixelTablet.put("BRAND", "google"); propsToChangePixelTablet.put("BOARD", "tangorpro"); @@ -202,8 +202,8 @@ public static boolean isCustomForkBuild() { propsToChangePixelTablet.put("PRODUCT", "tangorpro"); propsToChangePixelTablet.put("HARDWARE", "tangorpro"); propsToChangePixelTablet.put("MODEL", "Pixel Tablet"); - propsToChangePixelTablet.put("ID", "CP1A.260405.005"); - propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/CP1A.260405.005/15001963:user/release-keys"); + propsToChangePixelTablet.put("ID", "CP1A.260505.005"); + propsToChangePixelTablet.put("FINGERPRINT", "google/tangorpro/tangorpro:16/CP1A.260505.005/15081906:user/release-keys"); } public static String getBuildID(String fingerprint) { From 1ee478e472245b80dfcf0e064e7bc551cb76fba3 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:40:15 +0800 Subject: [PATCH 1105/1315] SystemUI: Adding platform hooks adding hooks to reduce ax platform apps boilerplate [neobuddy89: Hook in SystemUICoreStartableModule.] Change-Id: Ibdbd59a11ea8375c8698bef65a971a6c522fbe3e Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Mrick343 --- packages/SystemUI/Android.bp | 1 + packages/SystemUI/AndroidManifest.xml | 13 + .../ShadeCarrierGroupControllerTest.java | 2 +- .../connectivity/CallbackHandlerTest.java | 2 +- .../systemui/ax/AxPlatformBinderService.kt | 26 + .../ax/AxPlatformFeatureController.kt | 482 ++++++++++ .../systemui/ax/AxPlatformFeatureMapper.kt | 144 +++ .../systemui/ax/AxPlatformObservers.kt | 898 ++++++++++++++++++ .../systemui/ax/AxPlatformServiceImpl.kt | 121 +++ .../systemui/ax/AxPlatformStateManager.kt | 173 ++++ .../dagger/SystemUICoreStartableModule.kt | 5 + .../connectivity/MobileSignalController.java | 4 +- .../statusbar/connectivity/SignalCallback.kt | 2 + .../connectivity/WifiSignalController.java | 4 +- 14 files changed, 1873 insertions(+), 4 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformBinderService.kt create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureMapper.kt create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformServiceImpl.kt create mode 100644 packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 1e40813cbafb..259cd3c110ff 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -517,6 +517,7 @@ android_library { static_libs: [ "//frameworks/libs/systemui:compilelib", "com.android.systemui.pods-api-aosp-handheld", + "ax_platform_hooks", "SystemUI-res", "WifiTrackerLib", "WindowManager-Shell", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 39dc916ba076..411859e5db98 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -98,6 +98,12 @@ + + + + @@ -465,6 +471,13 @@ + + + + + diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index 7f43130f3e0a..672ecfb04c52 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -324,7 +324,7 @@ public void testSetMobileDataIndicators_invalidSim() { MobileDataIndicators indicators = new MobileDataIndicators( mock(IconState.class), mock(IconState.class), - 0, 0, true, true, "", "", "", 0, true, true); + 0, 0, true, true, "", "", "", 0, true, true, false, 0); mSignalCallback.setMobileDataIndicators(indicators); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java index ffd5efac8c6c..f99296385577 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java @@ -120,7 +120,7 @@ public void testSignalCallback_setMobileDataIndicators() { boolean roaming = true; MobileDataIndicators indicators = new MobileDataIndicators( status, qs, type, qsType, in, out, typeDescription, - typeDescriptionHtml, description, subId, roaming, true); + typeDescriptionHtml, description, subId, roaming, true, false, 0); mHandler.setMobileDataIndicators(indicators); waitForCallbacks(); diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformBinderService.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformBinderService.kt new file mode 100644 index 000000000000..e849a187a7fb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformBinderService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class AxPlatformBinderService : Service() { + + override fun onBind(intent: Intent?): IBinder? = AxPlatformServiceImpl.sBinder +} diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureController.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureController.kt new file mode 100644 index 000000000000..fdced0ad0c84 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureController.kt @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.app.UiModeManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.hardware.SensorPrivacyManager +import android.hardware.display.ColorDisplayManager +import android.os.Handler +import android.os.Looper +import android.view.WindowManager +import com.android.internal.util.ScreenshotHelper +import android.media.projection.StopReason +import android.net.TetheringManager +import android.nfc.NfcAdapter +import android.os.PowerManager +import android.os.RemoteException +import android.os.ServiceManager +import android.os.UserHandle +import android.provider.Settings +import android.service.dreams.IDreamManager +import android.util.Log +import com.android.axion.platform.AxPlatformClient +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.connectivity.AccessPointController +import com.android.systemui.statusbar.connectivity.NetworkController +import com.android.systemui.statusbar.phone.ManagedProfileController +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.BluetoothController +import com.android.systemui.statusbar.policy.CastController +import com.android.systemui.statusbar.policy.DataSaverController +import com.android.systemui.statusbar.policy.FlashlightController +import com.android.systemui.statusbar.policy.HotspotController +import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController +import com.android.systemui.statusbar.policy.LocationController +import com.android.systemui.statusbar.policy.RotationLockController +import com.android.systemui.screenrecord.ScreenRecordUxController +import com.android.systemui.statusbar.policy.SecurityController +import com.android.systemui.statusbar.policy.ZenModeController +import com.android.wifitrackerlib.WifiEntry +import lineageos.app.ProfileManager +import lineageos.hardware.LineageHardwareManager +import lineageos.providers.LineageSettings +import javax.inject.Inject + +@SysUISingleton +class AxPlatformFeatureController @Inject constructor( + private val context: Context, + private val stateManager: AxPlatformStateManager, + private val networkController: NetworkController, + private val accessPointController: AccessPointController, + private val bluetoothController: BluetoothController, + private val hotspotController: HotspotController, + private val flashlightController: FlashlightController, + private val locationController: LocationController, + private val rotationLockController: RotationLockController, + private val batteryController: BatteryController, + private val zenModeController: ZenModeController, + private val dataSaverController: DataSaverController, + private val localBluetoothManager: LocalBluetoothManager?, + private val sensorPrivacyController: IndividualSensorPrivacyController, + private val managedProfileController: ManagedProfileController, + private val securityController: SecurityController, + private val castController: CastController, + private val screenRecordUxController: ScreenRecordUxController, + powerManager: PowerManager +) { + + internal val wakeLock: PowerManager.WakeLock = + powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "AxPlatform:Caffeine") + private val profileManager: ProfileManager? = try { + ProfileManager.getInstance(context) + } catch (e: Exception) { null } + + private val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(context) + private val uiModeManager: UiModeManager = context.getSystemService(UiModeManager::class.java) + private val colorDisplayManager: ColorDisplayManager = + context.getSystemService(ColorDisplayManager::class.java) + private val tetheringManager: TetheringManager? = + context.getSystemService(TetheringManager::class.java) + internal val dreamManager: IDreamManager? = try { + IDreamManager.Stub.asInterface(ServiceManager.getService("dreams")) + } catch (e: Exception) { null } + internal val lineageHardware: LineageHardwareManager? = try { + LineageHardwareManager.getInstance(context) + } catch (e: Exception) { null } + + private val screenshotHelper = ScreenshotHelper(context) + private val screenshotHandler = Handler(Looper.getMainLooper()) + + var latestAccessPoints: List = emptyList() + + val supportedFeatures: Array by lazy { + buildList { + addAll(BASE_FEATURES) + if (nfcAdapter != null) + add(AxPlatformClient.FEATURE_NFC) + if (sensorPrivacyController.supportsSensorToggle(SensorPrivacyManager.Sensors.CAMERA)) + add(AxPlatformClient.FEATURE_CAMERA_PRIVACY) + if (sensorPrivacyController.supportsSensorToggle(SensorPrivacyManager.Sensors.MICROPHONE)) + add(AxPlatformClient.FEATURE_MIC_PRIVACY) + add(AxPlatformClient.FEATURE_WORK_PROFILE) + if (tetheringManager?.isTetheringSupported == true) + add(AxPlatformClient.FEATURE_USB_TETHER) + if (dreamManager != null) + add(AxPlatformClient.FEATURE_DREAM) + if (lineageHardware?.isSupported(LineageHardwareManager.FEATURE_READING_ENHANCEMENT) == true) + add(AxPlatformClient.FEATURE_READING_MODE) + if (batteryController.isReverseSupported) + add(AxPlatformClient.FEATURE_POWER_SHARE) + add(AxPlatformClient.FEATURE_CAFFEINE) + add(AxPlatformClient.FEATURE_VPN) + add(AxPlatformClient.FEATURE_CAST) + add(AxPlatformClient.FEATURE_SMART_PIXELS) + if (profileManager != null) + add(AxPlatformClient.FEATURE_PROFILES) + add(AxPlatformClient.FEATURE_SCREEN_RECORD) + add(AxPlatformClient.FEATURE_SCREENSHOT) + }.toTypedArray() + } + + fun toggle(feature: String) { + when (feature) { + AxPlatformClient.FEATURE_WIFI -> { + val current = stateManager.getState(feature).getBoolean("enabled", false) + networkController.setWifiEnabled(!current) + } + AxPlatformClient.FEATURE_MOBILE_DATA -> { + val ctrl = networkController.mobileDataController ?: return + ctrl.isMobileDataEnabled = !ctrl.isMobileDataEnabled + } + AxPlatformClient.FEATURE_BLUETOOTH -> + bluetoothController.setBluetoothEnabled(!bluetoothController.isBluetoothEnabled) + AxPlatformClient.FEATURE_HOTSPOT -> { + val current = stateManager.getState(feature).getBoolean("enabled", false) + hotspotController.setHotspotEnabled(!current) + } + AxPlatformClient.FEATURE_FLASHLIGHT -> { + if (flashlightController.hasFlashlight()) + flashlightController.setFlashlight(!flashlightController.isEnabled) + } + AxPlatformClient.FEATURE_LOCATION -> + locationController.setLocationEnabled(!locationController.isLocationEnabled) + AxPlatformClient.FEATURE_ROTATION -> + rotationLockController.setRotationLocked( + !rotationLockController.isRotationLocked, TAG + ) + AxPlatformClient.FEATURE_BATTERY_SAVER -> + batteryController.setPowerSaveMode(!batteryController.isPowerSave) + AxPlatformClient.FEATURE_ZEN -> { + val current = zenModeController.zen + zenModeController.setZen(if (current == 0) 1 else 0, null, TAG) + } + AxPlatformClient.FEATURE_DATA_SAVER -> + dataSaverController.setDataSaverEnabled(!dataSaverController.isDataSaverEnabled) + AxPlatformClient.FEATURE_AOD -> + stateManager.toggleSecure(Settings.Secure.DOZE_ALWAYS_ON) + AxPlatformClient.FEATURE_AIRPLANE_MODE -> { + val enabled = !stateManager.getGlobalBool(Settings.Global.AIRPLANE_MODE_ON) + stateManager.setGlobalBool(Settings.Global.AIRPLANE_MODE_ON, enabled) + context.sendBroadcastAsUser( + Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED).putExtra("state", enabled), + UserHandle.ALL + ) + } + AxPlatformClient.FEATURE_NFC -> nfcAdapter?.let { + if (it.isEnabled) it.disable() else it.enable() + } + AxPlatformClient.FEATURE_DARK_MODE -> + uiModeManager.setNightModeActivated( + !isDarkMode(context.resources.configuration) + ) + AxPlatformClient.FEATURE_NIGHT_LIGHT -> + colorDisplayManager.setNightDisplayActivated( + !colorDisplayManager.isNightDisplayActivated + ) + AxPlatformClient.FEATURE_COLOR_INVERSION -> + stateManager.toggleSecure(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) + AxPlatformClient.FEATURE_COLOR_CORRECTION -> + stateManager.toggleSecure(Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED) + AxPlatformClient.FEATURE_REDUCE_BRIGHTNESS -> + stateManager.toggleSecure(SETTING_REDUCE_BRIGHT) + AxPlatformClient.FEATURE_ONE_HANDED_MODE -> + stateManager.toggleSecure(SETTING_ONE_HANDED) + AxPlatformClient.FEATURE_HEADS_UP -> + stateManager.toggleGlobal(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED) + AxPlatformClient.FEATURE_AUTO_SYNC -> + ContentResolver.setMasterSyncAutomatically( + !ContentResolver.getMasterSyncAutomatically() + ) + AxPlatformClient.FEATURE_CAMERA_PRIVACY -> + sensorPrivacyController.setSensorBlocked( + SensorPrivacyManager.Sources.QS_TILE, + SensorPrivacyManager.Sensors.CAMERA, + !sensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA) + ) + AxPlatformClient.FEATURE_MIC_PRIVACY -> + sensorPrivacyController.setSensorBlocked( + SensorPrivacyManager.Sources.QS_TILE, + SensorPrivacyManager.Sensors.MICROPHONE, + !sensorPrivacyController.isSensorBlocked( + SensorPrivacyManager.Sensors.MICROPHONE + ) + ) + AxPlatformClient.FEATURE_WORK_PROFILE -> + managedProfileController.setWorkModeEnabled( + !managedProfileController.isWorkModeEnabled + ) + AxPlatformClient.FEATURE_USB_TETHER -> { + val current = stateManager.getState(feature).getBoolean("active", false) + tetheringManager?.setUsbTethering(!current) + } + AxPlatformClient.FEATURE_DREAM -> try { + dreamManager?.let { if (it.isDreaming) it.awaken() else it.dream() } + } catch (e: RemoteException) { + Log.w(TAG, "Dream toggle failed", e) + } + AxPlatformClient.FEATURE_READING_MODE -> lineageHardware?.let { + val current = it.get(LineageHardwareManager.FEATURE_READING_ENHANCEMENT) + it.set(LineageHardwareManager.FEATURE_READING_ENHANCEMENT, !current) + stateManager.broadcastBool(feature, !current) + } + AxPlatformClient.FEATURE_POWER_SHARE -> + batteryController.setReverseState(!batteryController.isReverseOn) + AxPlatformClient.FEATURE_CAFFEINE -> { + if (wakeLock.isHeld) { + wakeLock.release() + } else { + wakeLock.acquire(CAFFEINE_DURATION_MS) + } + stateManager.broadcastBool(feature, wakeLock.isHeld) + } + AxPlatformClient.FEATURE_VPN -> { + if (securityController.isVpnEnabled) { + securityController.disconnectPrimaryVpn() + } + } + AxPlatformClient.FEATURE_CAST -> { + val active = castController.castDevices.firstOrNull { it.isCasting } + active?.let { castController.stopCasting(it, StopReason.STOP_QS_TILE) } + } + AxPlatformClient.FEATURE_PROFILES -> { + val current = profilesEnabled() + setProfilesEnabled(!current) + stateManager.broadcastBool(feature, !current) + } + AxPlatformClient.FEATURE_SMART_PIXELS -> + stateManager.toggleSecure(SETTING_SMART_PIXELS) + AxPlatformClient.FEATURE_SCREEN_RECORD -> { + if (screenRecordUxController.isStarting) { + screenRecordUxController.cancelCountdown() + } else if (screenRecordUxController.isRecording) { + screenRecordUxController.stopRecording(StopReason.STOP_QS_TILE) + } else { + screenRecordUxController.createScreenRecordDialog(null).show() + } + } + AxPlatformClient.FEATURE_SCREENSHOT -> { + screenshotHandler.postDelayed({ + screenshotHelper.takeScreenshot( + WindowManager.TAKE_SCREENSHOT_FULLSCREEN, + WindowManager.ScreenshotSource.SCREENSHOT_OTHER, + screenshotHandler, + null + ) + }, SCREENSHOT_DELAY_MS) + } + else -> Log.w(TAG, "Unknown toggle: $feature") + } + } + + fun setEnabled(feature: String, enabled: Boolean) { + when (feature) { + AxPlatformClient.FEATURE_WIFI -> networkController.setWifiEnabled(enabled) + AxPlatformClient.FEATURE_MOBILE_DATA -> + networkController.mobileDataController?.let { it.isMobileDataEnabled = enabled } + AxPlatformClient.FEATURE_BLUETOOTH -> bluetoothController.setBluetoothEnabled(enabled) + AxPlatformClient.FEATURE_HOTSPOT -> hotspotController.setHotspotEnabled(enabled) + AxPlatformClient.FEATURE_FLASHLIGHT -> { + if (flashlightController.hasFlashlight()) flashlightController.setFlashlight(enabled) + } + AxPlatformClient.FEATURE_LOCATION -> locationController.setLocationEnabled(enabled) + AxPlatformClient.FEATURE_ROTATION -> + rotationLockController.setRotationLocked(!enabled, TAG) + AxPlatformClient.FEATURE_BATTERY_SAVER -> batteryController.setPowerSaveMode(enabled) + AxPlatformClient.FEATURE_ZEN -> + zenModeController.setZen(if (enabled) 1 else 0, null, TAG) + AxPlatformClient.FEATURE_DATA_SAVER -> dataSaverController.setDataSaverEnabled(enabled) + AxPlatformClient.FEATURE_AOD -> + stateManager.setSecureBool(Settings.Secure.DOZE_ALWAYS_ON, enabled) + AxPlatformClient.FEATURE_AIRPLANE_MODE -> { + stateManager.setGlobalBool(Settings.Global.AIRPLANE_MODE_ON, enabled) + context.sendBroadcastAsUser( + Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED).putExtra("state", enabled), + UserHandle.ALL + ) + } + AxPlatformClient.FEATURE_NFC -> nfcAdapter?.let { + if (enabled) it.enable() else it.disable() + } + AxPlatformClient.FEATURE_DARK_MODE -> uiModeManager.setNightModeActivated(enabled) + AxPlatformClient.FEATURE_NIGHT_LIGHT -> + colorDisplayManager.setNightDisplayActivated(enabled) + AxPlatformClient.FEATURE_COLOR_INVERSION -> + stateManager.setSecureBool( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, enabled + ) + AxPlatformClient.FEATURE_COLOR_CORRECTION -> + stateManager.setSecureBool( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, enabled + ) + AxPlatformClient.FEATURE_REDUCE_BRIGHTNESS -> + stateManager.setSecureBool(SETTING_REDUCE_BRIGHT, enabled) + AxPlatformClient.FEATURE_ONE_HANDED_MODE -> + stateManager.setSecureBool(SETTING_ONE_HANDED, enabled) + AxPlatformClient.FEATURE_HEADS_UP -> + stateManager.setGlobalBool( + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, enabled + ) + AxPlatformClient.FEATURE_AUTO_SYNC -> + ContentResolver.setMasterSyncAutomatically(enabled) + AxPlatformClient.FEATURE_CAMERA_PRIVACY -> + sensorPrivacyController.setSensorBlocked( + SensorPrivacyManager.Sources.QS_TILE, + SensorPrivacyManager.Sensors.CAMERA, + enabled + ) + AxPlatformClient.FEATURE_MIC_PRIVACY -> + sensorPrivacyController.setSensorBlocked( + SensorPrivacyManager.Sources.QS_TILE, + SensorPrivacyManager.Sensors.MICROPHONE, + enabled + ) + AxPlatformClient.FEATURE_WORK_PROFILE -> + managedProfileController.setWorkModeEnabled(enabled) + AxPlatformClient.FEATURE_USB_TETHER -> tetheringManager?.setUsbTethering(enabled) + AxPlatformClient.FEATURE_DREAM -> try { + dreamManager?.let { if (enabled) it.dream() else it.awaken() } + } catch (e: RemoteException) { + Log.w(TAG, "Dream setEnabled failed", e) + } + AxPlatformClient.FEATURE_READING_MODE -> lineageHardware?.let { + it.set(LineageHardwareManager.FEATURE_READING_ENHANCEMENT, enabled) + stateManager.broadcastBool(feature, enabled) + } + AxPlatformClient.FEATURE_POWER_SHARE -> + batteryController.setReverseState(enabled) + AxPlatformClient.FEATURE_CAFFEINE -> { + if (enabled && !wakeLock.isHeld) { + wakeLock.acquire(CAFFEINE_DURATION_MS) + } else if (!enabled && wakeLock.isHeld) { + wakeLock.release() + } + stateManager.broadcastBool(feature, wakeLock.isHeld) + } + AxPlatformClient.FEATURE_VPN -> { + if (!enabled && securityController.isVpnEnabled) { + securityController.disconnectPrimaryVpn() + } + } + AxPlatformClient.FEATURE_CAST -> { + if (!enabled) { + castController.castDevices.firstOrNull { it.isCasting } + ?.let { castController.stopCasting(it, StopReason.STOP_QS_TILE) } + } + } + AxPlatformClient.FEATURE_PROFILES -> { + setProfilesEnabled(enabled) + stateManager.broadcastBool(feature, enabled) + } + AxPlatformClient.FEATURE_SMART_PIXELS -> + stateManager.setSecureBool(SETTING_SMART_PIXELS, enabled) + AxPlatformClient.FEATURE_SCREEN_RECORD -> { + if (!enabled && screenRecordUxController.isRecording) { + screenRecordUxController.stopRecording(StopReason.STOP_QS_TILE) + } + } + AxPlatformClient.FEATURE_SCREENSHOT -> { + if (enabled) toggle(feature) + } + else -> Log.w(TAG, "Unknown setEnabled: $feature") + } + } + + fun setValue(feature: String, value: Int) { + when (feature) { + AxPlatformClient.FEATURE_ZEN -> { + if (zenModeController.zen != value) + zenModeController.setZen(value, null, TAG) + } + else -> Log.w(TAG, "Unknown setValue: $feature") + } + } + + fun performAction(feature: String, param: String) { + when (feature) { + AxPlatformClient.ACTION_WIFI_CONNECT -> + latestAccessPoints + .find { it.getKey() == param || it.getTitle() == param } + ?.let { accessPointController.connect(it) } + AxPlatformClient.ACTION_BT_CONNECT -> + getAllBluetoothDevices().find { it.getAddress() == param }?.let { + if (it.isConnected()) it.disconnect() else it.connect(true) + } + else -> Log.w(TAG, "Unknown performAction: $feature") + } + } + + fun getAllBluetoothDevices(): Collection = + localBluetoothManager?.cachedDeviceManager?.cachedDevicesCopy + ?: bluetoothController.connectedDevices + + private fun profilesEnabled(): Boolean = + LineageSettings.System.getIntForUser( + context.contentResolver, + LineageSettings.System.SYSTEM_PROFILES_ENABLED, + 1, UserHandle.USER_CURRENT + ) == 1 + + private fun setProfilesEnabled(enabled: Boolean) { + LineageSettings.System.putIntForUser( + context.contentResolver, + LineageSettings.System.SYSTEM_PROFILES_ENABLED, + if (enabled) 1 else 0, UserHandle.USER_CURRENT + ) + } + + companion object { + private const val TAG = "AxPlatformFeatureCtrl" + private const val CAFFEINE_DURATION_MS = 5L * 60 * 1000 + private const val SCREENSHOT_DELAY_MS = 500L + const val SETTING_NIGHT_DISPLAY = "night_display_activated" + const val SETTING_REDUCE_BRIGHT = "reduce_bright_colors_activated" + const val SETTING_ONE_HANDED = "one_handed_mode_enabled" + const val SETTING_SMART_PIXELS = "ax_smart_pixel_filter_enabled" + + fun isDarkMode(config: Configuration): Boolean = + (config.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + + private val BASE_FEATURES = arrayOf( + AxPlatformClient.FEATURE_WIFI, + AxPlatformClient.FEATURE_MOBILE_DATA, + AxPlatformClient.FEATURE_BLUETOOTH, + AxPlatformClient.FEATURE_HOTSPOT, + AxPlatformClient.FEATURE_FLASHLIGHT, + AxPlatformClient.FEATURE_LOCATION, + AxPlatformClient.FEATURE_ROTATION, + AxPlatformClient.FEATURE_BATTERY_SAVER, + AxPlatformClient.FEATURE_ZEN, + AxPlatformClient.FEATURE_AOD, + AxPlatformClient.FEATURE_DATA_SAVER, + AxPlatformClient.FEATURE_AIRPLANE_MODE, + AxPlatformClient.FEATURE_DARK_MODE, + AxPlatformClient.FEATURE_NIGHT_LIGHT, + AxPlatformClient.FEATURE_COLOR_INVERSION, + AxPlatformClient.FEATURE_COLOR_CORRECTION, + AxPlatformClient.FEATURE_REDUCE_BRIGHTNESS, + AxPlatformClient.FEATURE_ONE_HANDED_MODE, + AxPlatformClient.FEATURE_HEADS_UP, + AxPlatformClient.FEATURE_AUTO_SYNC + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureMapper.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureMapper.kt new file mode 100644 index 000000000000..bc484383d459 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformFeatureMapper.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.content.Context +import android.os.Bundle +import com.android.axion.platform.AxPlatformClient +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.ConfigurationController +import javax.inject.Inject + +@SysUISingleton +class AxPlatformFeatureMapper @Inject constructor( + private val context: Context, + private val batteryController: BatteryController, + configurationController: ConfigurationController +) : AxPlatformStateManager.LabelProvider { + + private val labelCache = mutableMapOf() + + init { + configurationController.addCallback(object : ConfigurationController.ConfigurationListener { + override fun onLocaleListChanged() { + labelCache.clear() + } + }) + } + + override fun getLabel(feature: String): String? { + labelCache[feature]?.let { return it } + val label = when { + FEATURE_LABEL_RES.containsKey(feature) -> context.getString(FEATURE_LABEL_RES[feature]!!) + feature == AxPlatformClient.FEATURE_SMART_PIXELS -> LABEL_SMART_PIXELS + else -> return null + } + labelCache[feature] = label + return label + } + + override fun getSecondaryLabel(feature: String, state: Bundle): String? = when (feature) { + AxPlatformClient.FEATURE_WIFI -> { + val ssid = state.getString("ssid") + when { + state.getBoolean("connected") && !ssid.isNullOrEmpty() -> ssid + else -> null + } + } + AxPlatformClient.FEATURE_BLUETOOTH -> { + @Suppress("DEPRECATION") + val devices = state.getParcelableArrayList("devices") + devices?.firstOrNull { it.getBoolean("isConnected") }?.getString("name") + } + AxPlatformClient.FEATURE_HOTSPOT -> { + val num = state.getInt("numDevices", 0) + if (num > 0) "$num ${if (num == 1) "device" else "devices"}" else null + } + AxPlatformClient.FEATURE_MOBILE_DATA -> { + state.getString("description")?.takeIf { it.isNotEmpty() } + } + AxPlatformClient.FEATURE_ZEN -> { + val mode = state.getInt("mode", 0) + if (mode != 0) context.getString(R.string.zen_mode_on) else null + } + AxPlatformClient.FEATURE_POWER_SHARE -> { + if (batteryController.isAodPowerSave) + context.getString(R.string.quick_settings_powershare_off_powersave_label) + else null + } + AxPlatformClient.FEATURE_VPN -> { + state.getString("name")?.takeIf { it.isNotEmpty() } + } + AxPlatformClient.FEATURE_CAST -> { + state.getString("deviceName")?.takeIf { it.isNotEmpty() } + } + AxPlatformClient.FEATURE_PROFILES -> { + state.getString("profileName")?.takeIf { it.isNotEmpty() } + } + AxPlatformClient.FEATURE_SCREEN_RECORD -> { + when { + state.getBoolean("active") -> context.getString(R.string.quick_settings_screen_record_stop) + state.getBoolean("starting") -> context.getString(R.string.quick_settings_screen_record_start) + else -> null + } + } + else -> null + } + + companion object { + private const val LABEL_SMART_PIXELS = "Smart Pixels" + + private val FEATURE_LABEL_RES = mapOf( + AxPlatformClient.FEATURE_WIFI to R.string.quick_settings_wifi_label, + AxPlatformClient.FEATURE_MOBILE_DATA to R.string.quick_settings_internet_label, + AxPlatformClient.FEATURE_BLUETOOTH to R.string.quick_settings_bluetooth_label, + AxPlatformClient.FEATURE_HOTSPOT to R.string.quick_settings_hotspot_label, + AxPlatformClient.FEATURE_FLASHLIGHT to R.string.quick_settings_flashlight_label, + AxPlatformClient.FEATURE_LOCATION to R.string.quick_settings_location_label, + AxPlatformClient.FEATURE_ROTATION to R.string.quick_settings_rotation_unlocked_label, + AxPlatformClient.FEATURE_BATTERY_SAVER to R.string.battery_detail_switch_title, + AxPlatformClient.FEATURE_ZEN to R.string.quick_settings_dnd_label, + AxPlatformClient.FEATURE_AOD to R.string.quick_settings_aod_label, + AxPlatformClient.FEATURE_DATA_SAVER to R.string.data_saver, + AxPlatformClient.FEATURE_AIRPLANE_MODE to R.string.airplane_mode, + AxPlatformClient.FEATURE_NFC to R.string.quick_settings_nfc_label, + AxPlatformClient.FEATURE_DARK_MODE to R.string.quick_settings_ui_mode_night_label, + AxPlatformClient.FEATURE_NIGHT_LIGHT to R.string.quick_settings_night_display_label, + AxPlatformClient.FEATURE_COLOR_INVERSION to R.string.quick_settings_inversion_label, + AxPlatformClient.FEATURE_COLOR_CORRECTION to R.string.quick_settings_color_correction_label, + AxPlatformClient.FEATURE_REDUCE_BRIGHTNESS to com.android.internal.R.string.reduce_bright_colors_feature_name, + AxPlatformClient.FEATURE_ONE_HANDED_MODE to R.string.quick_settings_onehanded_label, + AxPlatformClient.FEATURE_HEADS_UP to R.string.quick_settings_heads_up_label, + AxPlatformClient.FEATURE_AUTO_SYNC to R.string.quick_settings_sync_label, + AxPlatformClient.FEATURE_CAMERA_PRIVACY to R.string.quick_settings_camera_label, + AxPlatformClient.FEATURE_MIC_PRIVACY to R.string.quick_settings_mic_label, + AxPlatformClient.FEATURE_WORK_PROFILE to R.string.quick_settings_work_mode_label, + AxPlatformClient.FEATURE_USB_TETHER to R.string.quick_settings_usb_tether_label, + AxPlatformClient.FEATURE_DREAM to R.string.quick_settings_screensaver_label, + AxPlatformClient.FEATURE_READING_MODE to R.string.quick_settings_reading_mode, + AxPlatformClient.FEATURE_POWER_SHARE to R.string.quick_settings_powershare_label, + AxPlatformClient.FEATURE_CAFFEINE to R.string.quick_settings_caffeine_label, + AxPlatformClient.FEATURE_VPN to R.string.quick_settings_vpn_label, + AxPlatformClient.FEATURE_CAST to R.string.quick_settings_cast_title, + AxPlatformClient.FEATURE_PROFILES to R.string.quick_settings_profiles_label, + AxPlatformClient.FEATURE_SCREEN_RECORD to R.string.quick_settings_screen_record_label, + AxPlatformClient.FEATURE_SCREENSHOT to R.string.quick_settings_screenshot_label + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt new file mode 100644 index 000000000000..a40d22de79b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt @@ -0,0 +1,898 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.database.ExecutorContentObserver +import android.hardware.SensorPrivacyManager +import android.hardware.usb.UsbManager +import android.media.MediaMetadata +import android.media.session.PlaybackState +import android.nfc.NfcAdapter +import android.os.Bundle +import android.os.RemoteException +import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import com.android.axion.platform.AxPlatformClient +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.NotificationMediaManager +import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.quicklook.QuickLookClient +import com.android.systemui.screenrecord.ScreenRecordUxController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.connectivity.AccessPointController +import com.android.systemui.statusbar.connectivity.MobileDataIndicators +import com.android.systemui.statusbar.connectivity.NetworkController +import com.android.systemui.statusbar.connectivity.SignalCallback +import com.android.systemui.statusbar.connectivity.WifiIndicators +import com.android.systemui.statusbar.phone.ManagedProfileController +import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.BluetoothController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.DataSaverController +import com.android.systemui.statusbar.policy.FlashlightController +import com.android.systemui.statusbar.policy.HotspotController +import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.LocationController +import com.android.systemui.statusbar.policy.NextAlarmController +import com.android.systemui.statusbar.policy.RotationLockController +import com.android.systemui.statusbar.policy.CastController +import com.android.systemui.statusbar.policy.CastDevice +import com.android.systemui.statusbar.policy.SecurityController +import com.android.systemui.statusbar.policy.ZenModeController +import com.android.wifitrackerlib.WifiEntry +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import lineageos.app.ProfileManager +import lineageos.hardware.LineageHardwareManager +import lineageos.providers.LineageSettings +import java.util.concurrent.Executor +import javax.inject.Inject + +@SysUISingleton +class AxPlatformObservers @Inject constructor( + private val context: Context, + @Main private val mainExecutor: Executor, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val broadcastDispatcher: BroadcastDispatcher, + private val secureSettings: SecureSettings, + private val stateManager: AxPlatformStateManager, + private val featureController: AxPlatformFeatureController, + private val networkController: NetworkController, + private val accessPointController: AccessPointController, + private val bluetoothController: BluetoothController, + private val hotspotController: HotspotController, + private val flashlightController: FlashlightController, + private val locationController: LocationController, + private val rotationLockController: RotationLockController, + private val batteryController: BatteryController, + private val zenModeController: ZenModeController, + private val dataSaverController: DataSaverController, + private val localBluetoothManager: LocalBluetoothManager?, + private val notificationMediaManager: NotificationMediaManager, + private val nextAlarmController: NextAlarmController, + private val quickLookClient: QuickLookClient, + private val configurationController: ConfigurationController, + private val statusBarStateController: StatusBarStateController, + private val keyguardStateController: KeyguardStateController, + private val batteryInteractor: BatteryInteractor, + private val sensorPrivacyController: IndividualSensorPrivacyController, + private val managedProfileController: ManagedProfileController, + private val securityController: SecurityController, + private val castController: CastController, + private val screenRecordUxController: ScreenRecordUxController +) { + + private val resolver: ContentResolver = context.contentResolver + private var lastMediaState = -1 + private var lastMediaTrack: String? = null + private var lastMediaArtist: String? = null + private var lastMediaPackage: String? = null + + fun registerAll() { + registerControllerCallbacks() + registerSettingsObservers() + registerNfc() + registerBatteryFlow() + registerSensorPrivacy() + registerWorkProfile() + registerUsbTether() + registerDream() + registerReadingMode() + registerPowerShare() + registerCaffeine() + registerVpn() + registerCast() + registerProfiles() + registerSmartPixels() + registerScreenRecord() + registerAmbientIndication() + } + + private fun registerControllerCallbacks() { + networkController.addCallback(signalCallback) + bluetoothController.addCallback(bluetoothCallback) + hotspotController.addCallback(hotspotCallback) + flashlightController.addCallback(flashlightCallback) + locationController.addCallback(locationCallback) + rotationLockController.addCallback(rotationCallback) + batteryController.addCallback(batteryCallback) + zenModeController.addCallback(zenCallback) + dataSaverController.addCallback(dataSaverCallback) + notificationMediaManager.addCallback(mediaListener) + nextAlarmController.addCallback(nextAlarmCallback) + quickLookClient.addCallback(quickLookCallback) + configurationController.addCallback(configurationListener) + statusBarStateController.addCallback(dozeCallback) + keyguardStateController.addCallback(keyguardCallback) + accessPointController.addAccessPointCallback(accessPointCallback) + securityController.addCallback(vpnCallback) + castController.addCallback(castCallback) + } + + private fun registerSettingsObservers() { + stateManager.observeSecure(Settings.Secure.DOZE_ALWAYS_ON, AxPlatformClient.FEATURE_AOD) + stateManager.observeSecure( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, + AxPlatformClient.FEATURE_COLOR_INVERSION + ) + stateManager.observeSecure( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + AxPlatformClient.FEATURE_COLOR_CORRECTION + ) + stateManager.observeSecure( + AxPlatformFeatureController.SETTING_REDUCE_BRIGHT, + AxPlatformClient.FEATURE_REDUCE_BRIGHTNESS + ) + stateManager.observeSecure( + AxPlatformFeatureController.SETTING_NIGHT_DISPLAY, + AxPlatformClient.FEATURE_NIGHT_LIGHT + ) + stateManager.observeSecure( + AxPlatformFeatureController.SETTING_ONE_HANDED, + AxPlatformClient.FEATURE_ONE_HANDED_MODE + ) + stateManager.observeGlobal( + Settings.Global.AIRPLANE_MODE_ON, + AxPlatformClient.FEATURE_AIRPLANE_MODE + ) + stateManager.observeGlobal( + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + AxPlatformClient.FEATURE_HEADS_UP + ) + stateManager.broadcastBool( + AxPlatformClient.FEATURE_AUTO_SYNC, + ContentResolver.getMasterSyncAutomatically() + ) + ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) { + mainExecutor.execute { + stateManager.broadcastBool( + AxPlatformClient.FEATURE_AUTO_SYNC, + ContentResolver.getMasterSyncAutomatically() + ) + } + } + } + + private fun registerNfc() { + val nfcAdapter = NfcAdapter.getDefaultAdapter(context) ?: return + stateManager.broadcastBool(AxPlatformClient.FEATURE_NFC, nfcAdapter.isEnabled) + broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + stateManager.broadcastBool(AxPlatformClient.FEATURE_NFC, nfcAdapter.isEnabled) + } + }, IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)) + } + + private fun registerBatteryFlow() { + scope.launch { + combine( + batteryInteractor.level, + batteryInteractor.isPluggedIn, + batteryInteractor.isCharging, + batteryInteractor.powerSave, + batteryInteractor.batteryTimeRemainingEstimate + ) { level, pluggedIn, charging, powerSave, estimate -> + Bundle().apply { + putInt("level", level ?: -1) + putBoolean("isPluggedIn", pluggedIn) + putBoolean("isCharging", charging) + putBoolean("powerSave", powerSave) + putString("batteryTimeRemainingEstimate", estimate?.toString() ?: "") + } + }.flowOn(bgDispatcher).collect { bundle -> + withContext(mainDispatcher) { + bundle.putBoolean("wireless", batteryController.isWirelessCharging) + stateManager.broadcastState(AxPlatformClient.KEY_BATTERY, bundle) + } + } + } + } + + private fun registerSensorPrivacy() { + sensorPrivacyController.addCallback(sensorPrivacyCallback) + if (sensorPrivacyController.supportsSensorToggle(SensorPrivacyManager.Sensors.CAMERA)) { + stateManager.broadcastBool( + AxPlatformClient.FEATURE_CAMERA_PRIVACY, + sensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA) + ) + } + if (sensorPrivacyController.supportsSensorToggle(SensorPrivacyManager.Sensors.MICROPHONE)) { + stateManager.broadcastBool( + AxPlatformClient.FEATURE_MIC_PRIVACY, + sensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.MICROPHONE) + ) + } + } + + private fun registerWorkProfile() { + managedProfileController.addCallback(managedProfileCallback) + broadcastWorkProfileState() + } + + private fun registerUsbTether() { + broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val connected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false) + val rndis = intent.getBooleanExtra(UsbManager.USB_FUNCTION_RNDIS, false) + val ncm = intent.getBooleanExtra(UsbManager.USB_FUNCTION_NCM, false) + val tethering = rndis || ncm + stateManager.broadcastState( + AxPlatformClient.FEATURE_USB_TETHER, + Bundle().apply { + putBoolean("enabled", tethering) + putBoolean("active", tethering) + putBoolean("usbConnected", connected) + } + ) + } + }, IntentFilter(UsbManager.ACTION_USB_STATE)) + } + + private fun registerDream() { + featureController.dreamManager ?: return + val filter = IntentFilter().apply { + addAction(Intent.ACTION_DREAMING_STARTED) + addAction(Intent.ACTION_DREAMING_STOPPED) + } + broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + broadcastDreamState() + } + }, filter) + secureSettings.registerContentObserverSync( + Settings.Secure.SCREENSAVER_ENABLED, + object : ExecutorContentObserver(mainExecutor) { + override fun onChange(selfChange: Boolean) { + broadcastDreamState() + } + } + ) + broadcastDreamState() + } + + private fun registerReadingMode() { + val hw = featureController.lineageHardware ?: return + if (!hw.isSupported(LineageHardwareManager.FEATURE_READING_ENHANCEMENT)) return + stateManager.broadcastBool( + AxPlatformClient.FEATURE_READING_MODE, + hw.get(LineageHardwareManager.FEATURE_READING_ENHANCEMENT) + ) + } + + private fun registerPowerShare() { + if (!batteryController.isReverseSupported) return + batteryController.addCallback(powerShareCallback) + stateManager.broadcastBool( + AxPlatformClient.FEATURE_POWER_SHARE, + batteryController.isReverseOn + ) + } + + private val signalCallback = object : SignalCallback { + override fun setWifiIndicators(wifiIndicators: WifiIndicators) { + val connected = wifiIndicators.statusIcon?.visible == true + val enabled = wifiIndicators.enabled + stateManager.broadcastState(AxPlatformClient.FEATURE_WIFI, Bundle().apply { + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putBoolean("connected", connected) + putString("ssid", wifiIndicators.description ?: "") + putString("statusLabel", wifiIndicators.statusLabel ?: "") + putBoolean("isDefault", connected && !wifiIndicators.isTransient) + }) + } + + override fun setMobileDataIndicators(mobileDataIndicators: MobileDataIndicators) { + val isEnabled = networkController.mobileDataController?.isMobileDataEnabled == true + stateManager.broadcastState( + AxPlatformClient.FEATURE_MOBILE_DATA, + Bundle().apply { + putBoolean("enabled", isEnabled) + putBoolean("active", isEnabled) + putString( + "type", + mobileDataIndicators.typeContentDescription?.toString() ?: "" + ) + putString("description", mobileDataIndicators.qsDescription?.toString() ?: "") + putBoolean("roaming", mobileDataIndicators.roaming) + putBoolean("isDefault", mobileDataIndicators.isDefault) + putInt("subId", mobileDataIndicators.subId) + putBoolean("activityIn", mobileDataIndicators.activityIn) + putBoolean("activityOut", mobileDataIndicators.activityOut) + putInt("level", mobileDataIndicators.level) + } + ) + } + + override fun setNoSims(show: Boolean, simDetected: Boolean) { + if (!simDetected) { + stateManager.broadcastState( + AxPlatformClient.FEATURE_MOBILE_DATA, + Bundle().apply { + putBoolean("available", false) + putBoolean("enabled", false) + putBoolean("active", false) + } + ) + } + } + } + + private val accessPointCallback = object : AccessPointController.AccessPointCallback { + override fun onAccessPointsChanged(accessPoints: List) { + featureController.latestAccessPoints = accessPoints.toList() + val networks = ArrayList() + accessPoints.forEach { ap -> + if (ap.getLevel() != -1) { + networks.add(Bundle().apply { + putString("title", ap.getTitle()) + putString("key", ap.getKey()) + putBoolean("isConnected", + ap.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) + putBoolean("isSaved", ap.isSaved()) + putInt("level", ap.getLevel()) + putInt("security", ap.getSecurity()) + }) + } + } + stateManager.broadcastState(AxPlatformClient.KEY_WIFI_SCAN, Bundle().apply { + putParcelableArrayList("networks", networks) + }) + } + + override fun onSettingsActivityTriggered(intent: Intent?) {} + override fun onWifiScan(isScan: Boolean) {} + } + + private fun getAllBluetoothDevices() = featureController.getAllBluetoothDevices() + + private val bluetoothCallback = object : BluetoothController.Callback { + override fun onBluetoothStateChange(enabled: Boolean) = broadcastBluetoothState(enabled) + override fun onBluetoothDevicesChanged() = + broadcastBluetoothState(bluetoothController.isBluetoothEnabled) + } + + private fun broadcastBluetoothState(enabled: Boolean) { + val devices = getAllBluetoothDevices() + val deviceBundles = ArrayList() + devices.forEach { device -> + deviceBundles.add(Bundle().apply { + putString("name", device.getName()) + putString("address", device.getAddress()) + putBoolean("isConnected", device.isConnected()) + putInt("connectionState", device.getMaxConnectionState()) + putInt("bondState", device.getBondState()) + putInt("batteryLevel", device.getBatteryLevel()) + }) + } + val hasConnected = devices.any { it.isConnected() } + stateManager.broadcastState(AxPlatformClient.FEATURE_BLUETOOTH, Bundle().apply { + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putBoolean("hasConnectedDevice", hasConnected) + putParcelableArrayList("devices", deviceBundles) + }) + } + + private val hotspotCallback = object : HotspotController.Callback { + override fun onHotspotChanged(enabled: Boolean, numDevices: Int) { + stateManager.broadcastState(AxPlatformClient.FEATURE_HOTSPOT, Bundle().apply { + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putInt("numDevices", numDevices) + }) + } + } + + private val flashlightCallback = object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) { + stateManager.broadcastState(AxPlatformClient.FEATURE_FLASHLIGHT, Bundle().apply { + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putBoolean("available", true) + }) + } + + override fun onFlashlightError() {} + + override fun onFlashlightAvailabilityChanged(available: Boolean) { + if (!available) { + stateManager.broadcastState(AxPlatformClient.FEATURE_FLASHLIGHT, Bundle().apply { + putBoolean("enabled", false) + putBoolean("active", false) + putBoolean("available", false) + }) + } + } + } + + private val rotationCallback = + object : RotationLockController.RotationLockControllerCallback { + override fun onRotationLockStateChanged( + rotationLocked: Boolean, + affordanceVisible: Boolean + ) { + stateManager.broadcastState(AxPlatformClient.FEATURE_ROTATION, Bundle().apply { + putBoolean("locked", rotationLocked) + putBoolean("active", !rotationLocked) + }) + } + } + + private val locationCallback = object : LocationController.LocationChangeCallback { + override fun onLocationSettingsChanged(locationEnabled: Boolean) { + stateManager.broadcastBool(AxPlatformClient.FEATURE_LOCATION, locationEnabled) + } + } + + private val batteryCallback = object : BatteryController.BatteryStateChangeCallback { + override fun onPowerSaveChanged(isPowerSave: Boolean) { + stateManager.broadcastBool(AxPlatformClient.FEATURE_BATTERY_SAVER, isPowerSave) + } + } + + private val zenCallback = object : ZenModeController.Callback { + override fun onZenChanged(zen: Int) { + stateManager.broadcastState(AxPlatformClient.FEATURE_ZEN, Bundle().apply { + putInt("mode", zen) + putBoolean("active", zen != 0) + }) + } + } + + private val dataSaverCallback = object : DataSaverController.Listener { + override fun onDataSaverChanged(isDataSaving: Boolean) { + stateManager.broadcastBool(AxPlatformClient.FEATURE_DATA_SAVER, isDataSaving) + } + } + + private val sensorPrivacyCallback = + object : IndividualSensorPrivacyController.Callback { + override fun onSensorBlockedChanged(sensor: Int, blocked: Boolean) { + when (sensor) { + SensorPrivacyManager.Sensors.CAMERA -> + stateManager.broadcastBool( + AxPlatformClient.FEATURE_CAMERA_PRIVACY, blocked + ) + SensorPrivacyManager.Sensors.MICROPHONE -> + stateManager.broadcastBool( + AxPlatformClient.FEATURE_MIC_PRIVACY, blocked + ) + } + } + } + + private val managedProfileCallback = object : ManagedProfileController.Callback { + override fun onManagedProfileChanged() = broadcastWorkProfileState() + override fun onManagedProfileRemoved() { + stateManager.broadcastState( + AxPlatformClient.FEATURE_WORK_PROFILE, + Bundle().apply { + putBoolean("enabled", false) + putBoolean("active", false) + putBoolean("available", false) + } + ) + } + } + + private fun broadcastWorkProfileState() { + val hasProfile = managedProfileController.hasActiveProfile() + val enabled = hasProfile && managedProfileController.isWorkModeEnabled + stateManager.broadcastState( + AxPlatformClient.FEATURE_WORK_PROFILE, + Bundle().apply { + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putBoolean("available", hasProfile) + } + ) + } + + private fun broadcastDreamState() { + val isDreaming = try { + featureController.dreamManager?.isDreaming == true + } catch (e: RemoteException) { false } + val isEnabled = stateManager.getSecureBool(Settings.Secure.SCREENSAVER_ENABLED) + stateManager.broadcastState( + AxPlatformClient.FEATURE_DREAM, + Bundle().apply { + putBoolean("enabled", isEnabled) + putBoolean("active", isDreaming) + } + ) + } + + private val powerShareCallback = object : BatteryController.BatteryStateChangeCallback { + override fun onReverseChanged(isReverse: Boolean, level: Int, name: String?) { + stateManager.broadcastState( + AxPlatformClient.FEATURE_POWER_SHARE, + Bundle().apply { + putBoolean("enabled", isReverse) + putBoolean("active", isReverse) + } + ) + } + } + + private val mediaListener = object : NotificationMediaManager.MediaListener { + override fun onPrimaryMetadataOrStateChanged( + metadata: MediaMetadata?, + @PlaybackState.State state: Int + ) { + val isPlaying = state == PlaybackState.STATE_PLAYING + val packageName = notificationMediaManager.mediaNotificationKey + ?.split("|")?.getOrNull(1) ?: "" + val track = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + ?: metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "" + val artist = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "" + + if (state == lastMediaState && track == lastMediaTrack + && artist == lastMediaArtist && packageName == lastMediaPackage) return + + lastMediaState = state + lastMediaTrack = track + lastMediaArtist = artist + lastMediaPackage = packageName + + stateManager.broadcastState(AxPlatformClient.KEY_MEDIA, Bundle().apply { + putString("track", track) + putString("artist", artist) + putString("album", metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "") + putBoolean("isPlaying", isPlaying) + putInt("state", state) + putLong("duration", metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L) + putString("packageName", packageName) + }) + } + } + + private val nextAlarmCallback = NextAlarmController.NextAlarmChangeCallback { alarm -> + if (alarm != null) { + stateManager.broadcastState(AxPlatformClient.KEY_ALARM, Bundle().apply { + putLong("triggerTime", alarm.triggerTime) + putString("packageName", alarm.showIntent?.creatorPackage ?: "") + }) + } else { + stateManager.broadcastState(AxPlatformClient.KEY_ALARM, Bundle()) + } + } + + private val quickLookCallback = object : QuickLookClient.Callback { + override fun onClockDataChanged(data: ClockData) { + val cal = data.calendar + if (cal != CalendarSimpleData.EMPTY) { + stateManager.broadcastState(AxPlatformClient.KEY_CALENDAR, Bundle().apply { + putLong("id", cal.id) + putString("title", cal.title ?: "") + putLong("startTime", cal.startTime) + putLong("endTime", cal.endTime) + putString("location", cal.location ?: "") + }) + } else { + stateManager.broadcastState(AxPlatformClient.KEY_CALENDAR, Bundle()) + } + } + } + + private val configurationListener = object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration) = exportConfigInfo(newConfig) + override fun onDensityOrFontScaleChanged() = exportConfigInfo(null) + override fun onUiModeChanged() { + exportConfigInfo(null) + val config = context.resources.configuration + stateManager.broadcastBool( + AxPlatformClient.FEATURE_DARK_MODE, + AxPlatformFeatureController.isDarkMode(config) + ) + } + override fun onThemeChanged() = exportConfigInfo(null) + override fun onLocaleListChanged() = exportConfigInfo(null) + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) = exportConfigInfo(null) + override fun onOrientationChanged(orientation: Int) = exportConfigInfo(null) + } + + private fun exportConfigInfo(config: Configuration?) { + val currentConfig = config ?: context.resources.configuration + stateManager.broadcastState(AxPlatformClient.KEY_CONFIG, Bundle().apply { + putInt("uiMode", currentConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) + putBoolean("isDarkMode", AxPlatformFeatureController.isDarkMode(currentConfig)) + putInt("orientation", currentConfig.orientation) + putFloat("fontScale", currentConfig.fontScale) + putInt("densityDpi", currentConfig.densityDpi) + putInt("smallestScreenWidthDp", currentConfig.smallestScreenWidthDp) + putInt("screenWidthDp", currentConfig.screenWidthDp) + putInt("screenHeightDp", currentConfig.screenHeightDp) + putString("locale", currentConfig.locales.get(0)?.toLanguageTag() ?: "") + putInt("layoutDirection", currentConfig.layoutDirection) + }) + } + + private val dozeCallback = object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) = exportKeyguardInfo() + override fun onDozingChanged(isDozing: Boolean) = exportDozeInfo() + override fun onPulsingChanged(pulsing: Boolean) = exportDozeInfo() + override fun onDozeAmountChanged(linear: Float, eased: Float) = exportDozeInfo() + } + + private val keyguardCallback = object : KeyguardStateController.Callback { + override fun onKeyguardGoingAwayChanged() = exportKeyguardInfo() + override fun onKeyguardShowingChanged() = exportKeyguardInfo() + } + + private fun exportKeyguardInfo() { + val state = statusBarStateController.state + stateManager.broadcastState(AxPlatformClient.KEY_KEYGUARD, Bundle().apply { + putInt("state", state) + putBoolean("isKeyguard", state == StatusBarState.KEYGUARD) + putBoolean("isUnlocked", state == StatusBarState.SHADE) + putBoolean("isShadeLocked", state == StatusBarState.SHADE_LOCKED) + putBoolean("isShowing", keyguardStateController.isShowing) + putBoolean("isGoingAway", keyguardStateController.isKeyguardGoingAway) + }) + } + + private fun exportDozeInfo() { + val aodEnabled = stateManager.getState(AxPlatformClient.FEATURE_AOD).getBoolean("enabled", false) + stateManager.broadcastState(AxPlatformClient.KEY_DOZE, Bundle().apply { + putBoolean("isDozing", statusBarStateController.isDozing) + putBoolean("isPulsing", statusBarStateController.isPulsing) + putFloat("dozeAmount", statusBarStateController.dozeAmount) + putBoolean("aodEnabled", aodEnabled) + putBoolean("isAodPowerSave", batteryController.isAodPowerSave) + }) + } + + private fun registerCaffeine() { + stateManager.broadcastBool( + AxPlatformClient.FEATURE_CAFFEINE, + featureController.wakeLock.isHeld + ) + broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + if (featureController.wakeLock.isHeld) { + featureController.wakeLock.release() + stateManager.broadcastBool(AxPlatformClient.FEATURE_CAFFEINE, false) + } + } + }, IntentFilter(Intent.ACTION_SCREEN_OFF)) + } + + private val vpnCallback = object : SecurityController.SecurityControllerCallback { + override fun onStateChanged() { + stateManager.broadcastState( + AxPlatformClient.FEATURE_VPN, + Bundle().apply { + val enabled = securityController.isVpnEnabled + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putString("name", securityController.primaryVpnName ?: "") + } + ) + } + } + + private fun registerVpn() { + stateManager.broadcastState( + AxPlatformClient.FEATURE_VPN, + Bundle().apply { + val enabled = securityController.isVpnEnabled + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putString("name", securityController.primaryVpnName ?: "") + } + ) + } + + private val castCallback = object : CastController.Callback { + override fun onCastDevicesChanged() = broadcastCastState() + } + + private fun registerCast() { + broadcastCastState() + } + + private fun broadcastCastState() { + val devices = castController.castDevices + val active = devices.any { it.state == CastDevice.CastState.Connected } + val connecting = devices.any { it.state == CastDevice.CastState.Connecting } + val activeName = devices.firstOrNull { it.isCasting }?.name + stateManager.broadcastState( + AxPlatformClient.FEATURE_CAST, + Bundle().apply { + putBoolean("enabled", active || connecting) + putBoolean("active", active) + putBoolean("connecting", connecting) + putString("deviceName", activeName ?: "") + } + ) + } + + private fun registerProfiles() { + val profileManager = try { + ProfileManager.getInstance(context) + } catch (e: Exception) { return } + + val activeProfile = profileManager.activeProfile + stateManager.broadcastState( + AxPlatformClient.FEATURE_PROFILES, + Bundle().apply { + val enabled = profilesEnabled() + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putString("profileName", activeProfile?.name ?: "") + } + ) + + val filter = IntentFilter().apply { + addAction(ProfileManager.INTENT_ACTION_PROFILE_SELECTED) + addAction(ProfileManager.INTENT_ACTION_PROFILE_UPDATED) + } + broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val profile = profileManager.activeProfile + stateManager.broadcastState( + AxPlatformClient.FEATURE_PROFILES, + Bundle().apply { + val enabled = profilesEnabled() + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putString("profileName", profile?.name ?: "") + } + ) + } + }, filter) + + val uri = LineageSettings.System.getUriFor( + LineageSettings.System.SYSTEM_PROFILES_ENABLED + ) + resolver.registerContentObserver(uri, false, object : ExecutorContentObserver(mainExecutor) { + override fun onChange(selfChange: Boolean) { + val profile = profileManager.activeProfile + stateManager.broadcastState( + AxPlatformClient.FEATURE_PROFILES, + Bundle().apply { + val enabled = profilesEnabled() + putBoolean("enabled", enabled) + putBoolean("active", enabled) + putString("profileName", profile?.name ?: "") + } + ) + } + }, UserHandle.USER_CURRENT) + } + + private fun profilesEnabled(): Boolean = + LineageSettings.System.getIntForUser( + resolver, + LineageSettings.System.SYSTEM_PROFILES_ENABLED, + 1, UserHandle.USER_CURRENT + ) == 1 + + private fun registerSmartPixels() { + stateManager.observeSecure( + AxPlatformFeatureController.SETTING_SMART_PIXELS, + AxPlatformClient.FEATURE_SMART_PIXELS + ) + } + + private val screenRecordCallback = object : ScreenRecordUxController.StateChangeCallback { + override fun onCountdown(millisUntilFinished: Long) = broadcastScreenRecordState() + override fun onCountdownEnd() = broadcastScreenRecordState() + override fun onRecordingStart() = broadcastScreenRecordState() + override fun onRecordingEnd() = broadcastScreenRecordState() + } + + private fun registerScreenRecord() { + screenRecordUxController.addCallback(screenRecordCallback) + broadcastScreenRecordState() + } + + private fun broadcastScreenRecordState() { + val recording = screenRecordUxController.isRecording + val starting = screenRecordUxController.isStarting + stateManager.broadcastState( + AxPlatformClient.FEATURE_SCREEN_RECORD, + Bundle().apply { + putBoolean("enabled", recording || starting) + putBoolean("active", recording) + putBoolean("starting", starting) + } + ) + } + + private fun registerAmbientIndication() { + val filter = IntentFilter().apply { + addAction(ACTION_AMBIENT_SHOW) + addAction(ACTION_AMBIENT_EXPAND) + addAction(ACTION_AMBIENT_HIDE) + } + context.registerReceiverAsUser( + object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val action = intent.action ?: return + val bundle = Bundle().apply { + putString("action", action) + intent.extras?.let { putAll(it) } + } + stateManager.broadcastState(AxPlatformClient.KEY_NOW_PLAYING, bundle) + } + }, + UserHandle.ALL, + filter, + PERMISSION_AMBIENT_INDICATION, + null, + Context.RECEIVER_EXPORTED, + ) + } + + companion object { + private const val TAG = "AxPlatformObservers" + + private const val ACTION_AMBIENT_SHOW = + "com.google.android.ambientindication.action.AMBIENT_INDICATION_SHOW" + private const val ACTION_AMBIENT_EXPAND = + "com.google.android.ambientindication.action.AMBIENT_INDICATION_EXPAND" + private const val ACTION_AMBIENT_HIDE = + "com.google.android.ambientindication.action.AMBIENT_INDICATION_HIDE" + private const val PERMISSION_AMBIENT_INDICATION = + "com.google.android.ambientindication.permission.AMBIENT_INDICATION" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformServiceImpl.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformServiceImpl.kt new file mode 100644 index 000000000000..2dc47a287c20 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformServiceImpl.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.content.Context +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.os.Process +import android.util.Log +import com.android.axion.platform.IAxPlatformCallback +import com.android.axion.platform.IAxPlatformService +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@SysUISingleton +class AxPlatformServiceImpl @Inject constructor( + private val context: Context, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val stateManager: AxPlatformStateManager, + private val featureController: AxPlatformFeatureController, + private val featureMapper: AxPlatformFeatureMapper, + private val observers: AxPlatformObservers +) : CoreStartable { + + private val binderStub = object : IAxPlatformService.Stub() { + + override fun toggle(feature: String?) { + enforceSystemCaller() + if (feature.isNullOrEmpty()) return + scope.launch(mainDispatcher) { featureController.toggle(feature) } + } + + override fun setEnabled(feature: String?, enabled: Boolean) { + enforceSystemCaller() + if (feature.isNullOrEmpty()) return + scope.launch(mainDispatcher) { featureController.setEnabled(feature, enabled) } + } + + override fun setValue(feature: String?, value: Int) { + enforceSystemCaller() + if (feature.isNullOrEmpty()) return + scope.launch(mainDispatcher) { featureController.setValue(feature, value) } + } + + override fun performAction(feature: String?, param: String?) { + enforceSystemCaller() + if (feature.isNullOrEmpty() || param.isNullOrEmpty()) return + scope.launch(mainDispatcher) { featureController.performAction(feature, param) } + } + + override fun getState(feature: String?): Bundle = + if (feature != null) stateManager.getState(feature) else Bundle.EMPTY + + override fun getAllStates(): Bundle = stateManager.getAllStates() + + override fun getSupportedFeatures(): Array = featureController.supportedFeatures + + override fun registerCallback(callback: IAxPlatformCallback?) { + callback?.let { stateManager.registerCallback(it) } + } + + override fun unregisterCallback(callback: IAxPlatformCallback?) { + callback?.let { stateManager.unregisterCallback(it) } + } + } + + override fun start() { + stateManager.supportedFeatures = featureController.supportedFeatures + stateManager.labelProvider = featureMapper + sBinder = binderStub + Log.i(TAG, "AxPlatform service ready") + observers.registerAll() + } + + private fun enforceSystemCaller() { + val uid = Binder.getCallingUid() + if (uid == Process.SYSTEM_UID || uid == Process.myUid()) return + val token = Binder.clearCallingIdentity() + try { + val packages = context.packageManager.getPackagesForUid(uid) + val allowed = packages?.any { + it.startsWith("com.android.axion.") || + it == "com.android.systemui" || + it == "io.chaldeaprjkt.gamespace" + } == true + if (!allowed) { + throw SecurityException("AxPlatformService: caller uid=$uid not permitted") + } + } finally { + Binder.restoreCallingIdentity(token) + } + } + + companion object { + private const val TAG = "AxPlatformService" + @Volatile + internal var sBinder: IBinder? = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt new file mode 100644 index 000000000000..edd1d39f9520 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ax + +import android.database.ExecutorContentObserver +import android.os.Binder +import android.os.Bundle +import android.os.RemoteCallbackList +import android.os.RemoteException +import android.os.UserHandle +import android.util.Log +import com.android.axion.platform.AxPlatformClient +import com.android.axion.platform.IAxPlatformCallback +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SecureSettings +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor +import javax.inject.Inject + +@SysUISingleton +class AxPlatformStateManager @Inject constructor( + private val secureSettings: SecureSettings, + private val globalSettings: GlobalSettings, + @Main private val mainExecutor: Executor +) { + + interface LabelProvider { + fun getLabel(feature: String): String? + fun getSecondaryLabel(feature: String, state: Bundle): String? + } + + private val callbacks = RemoteCallbackList() + private val stateCache = ConcurrentHashMap() + + var labelProvider: LabelProvider? = null + var supportedFeatures: Array = emptyArray() + internal set + + fun getState(feature: String): Bundle = stateCache[feature] ?: Bundle.EMPTY + + fun getAllStates(): Bundle = Bundle().also { result -> + stateCache.forEach { (key, bundle) -> result.putBundle(key, bundle) } + } + + fun registerCallback(callback: IAxPlatformCallback) { + callbacks.register(callback) + stateCache.forEach { (key, bundle) -> + try { + callback.onStateChanged(key, bundle) + } catch (e: RemoteException) { + Log.w(TAG, "Failed to send initial state for key=$key", e) + } + } + } + + fun unregisterCallback(callback: IAxPlatformCallback) { + callbacks.unregister(callback) + } + + fun broadcastState(key: String, state: Bundle) { + enrichBundle(key, state) + stateCache[key] = state + synchronized(callbacks) { + val count = callbacks.beginBroadcast() + for (i in 0 until count) { + try { + callbacks.getBroadcastItem(i).onStateChanged(key, state) + } catch (e: RemoteException) { + Log.w(TAG, "Callback failed for key=$key", e) + } + } + callbacks.finishBroadcast() + } + } + + fun broadcastBool(feature: String, active: Boolean) { + broadcastState(feature, Bundle().apply { + putBoolean("enabled", active) + putBoolean("active", active) + }) + } + + private fun enrichBundle(key: String, state: Bundle) { + if (state === Bundle.EMPTY) return + if (!state.containsKey("tileState")) { + state.putInt("tileState", when { + !state.getBoolean("available", true) -> AxPlatformClient.TILE_STATE_UNAVAILABLE + state.getBoolean("active", false) -> AxPlatformClient.TILE_STATE_ACTIVE + else -> AxPlatformClient.TILE_STATE_INACTIVE + }) + } + labelProvider?.let { provider -> + if (!state.containsKey("label")) { + provider.getLabel(key)?.let { state.putString("label", it) } + } + if (!state.containsKey("secondaryLabel")) { + provider.getSecondaryLabel(key, state)?.let { + state.putString("secondaryLabel", it) + } + } + } + } + + fun getSecureBool(key: String, def: Boolean = false): Boolean = + secureSettings.getIntForUser(key, if (def) 1 else 0, UserHandle.USER_CURRENT) == 1 + + fun setSecureBool(key: String, value: Boolean) { + val token = Binder.clearCallingIdentity() + try { + secureSettings.putIntForUser(key, if (value) 1 else 0, UserHandle.USER_CURRENT) + } finally { + Binder.restoreCallingIdentity(token) + } + } + + fun toggleSecure(key: String) = setSecureBool(key, !getSecureBool(key)) + + fun getGlobalBool(key: String, def: Boolean = false): Boolean = + globalSettings.getInt(key, if (def) 1 else 0) == 1 + + fun setGlobalBool(key: String, value: Boolean) { + val token = Binder.clearCallingIdentity() + try { + globalSettings.putInt(key, if (value) 1 else 0) + } finally { + Binder.restoreCallingIdentity(token) + } + } + + fun toggleGlobal(key: String) = setGlobalBool(key, !getGlobalBool(key)) + + fun observeSecure(key: String, feature: String) { + secureSettings.registerContentObserverSync( + key, object : ExecutorContentObserver(mainExecutor) { + override fun onChange(selfChange: Boolean) { + broadcastBool(feature, getSecureBool(key)) + } + } + ) + broadcastBool(feature, getSecureBool(key)) + } + + fun observeGlobal(key: String, feature: String) { + globalSettings.registerContentObserverSync( + key, object : ExecutorContentObserver(mainExecutor) { + override fun onChange(selfChange: Boolean) { + broadcastBool(feature, getGlobalBool(key)) + } + } + ) + broadcastBool(feature, getGlobalBool(key)) + } + + companion object { + private const val TAG = "AxPlatformState" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index b174233895ee..990426366eb8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification +import com.android.systemui.ax.AxPlatformServiceImpl import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.bouncer.domain.startable.BouncerStartable @@ -343,4 +344,8 @@ abstract class SystemUICoreStartableModule { @ClassKey(UsbModePickerDialogDelegate::class) abstract fun bindUsbModePickerDialogDelegate(impl: UsbModePickerDialogDelegate): CoreStartable + @Binds + @IntoMap + @ClassKey(AxPlatformServiceImpl::class) + abstract fun bindAxPlatformService(impl: AxPlatformServiceImpl): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java index 2d6a2e39dcb7..4e14d221bfea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java @@ -334,7 +334,9 @@ public void notifyListeners(SignalCallback callback) { qsInfo.description, mSubscriptionInfo.getSubscriptionId(), mCurrentState.roaming, - sbInfo.showTriangle); + sbInfo.showTriangle, + mCurrentState.isDefault, + mCurrentState.level); callback.setMobileDataIndicators(mobileDataIndicators); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalCallback.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalCallback.kt index cf8240dc1626..2129723b3992 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalCallback.kt @@ -151,6 +151,8 @@ data class MobileDataIndicators( @JvmField val subId: Int, @JvmField val roaming: Boolean, @JvmField val showTriangle: Boolean, + @JvmField val isDefault: Boolean, + @JvmField val level: Int, ) { override fun toString(): String { return java.lang diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java index 9854e27e9d58..0ebf8e6d6f60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java @@ -158,7 +158,9 @@ private void notifyListenersForCarrierWifi(SignalCallback callback) { statusIcon, qsIcon, typeIcon, qsTypeIcon, mCurrentState.activityIn, mCurrentState.activityOut, dataContentDescription, dataContentDescriptionHtml, description, - mCurrentState.subId, /* roaming= */ false, /* showTriangle= */ true + mCurrentState.subId, /* roaming= */ false, /* showTriangle= */ true, + /* isDefault= */ qsIcon != null, + mCurrentState.level ); callback.setMobileDataIndicators(mobileDataIndicators); } From f4e8c761ab479a4e5aaba74db53afc1a55256cc6 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:52:37 +0800 Subject: [PATCH 1106/1315] SystemUI: Adding statusbar dynamic bar Change-Id: Ieec1869bac37104cd8733278865cf44e773b8971 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/ids.xml | 1 + .../SystemUI/res/values/matrixx_strings.xml | 95 ++ packages/SystemUI/res/xml/fileprovider.xml | 1 + .../data/IslandEventRepository.kt | 272 ++++ .../data/source/AppTrackingIslandManager.kt | 178 +++ .../data/source/BiometricIslandManager.kt | 64 + .../data/source/ConnectivityIslandManager.kt | 261 ++++ .../data/source/MediaIslandManager.kt | 343 +++++ .../data/source/NotificationIslandManager.kt | 1027 +++++++++++++ .../data/source/PrivacyIslandManager.kt | 164 +++ .../data/source/ScreenRecordIslandManager.kt | 93 ++ .../data/source/SmartspaceIslandManager.kt | 137 ++ .../data/source/SystemIslandManager.kt | 513 +++++++ .../data/source/TorchIslandManager.kt | 122 ++ .../domain/AxDynamicBarChipsRefiner.kt | 36 + .../domain/AxDynamicBarInteractor.kt | 577 ++++++++ .../domain/AxDynamicBarSettings.kt | 115 ++ .../axdynamicbar/model/IslandEvent.kt | 338 +++++ .../axdynamicbar/model/IslandUiState.kt | 24 + .../axdynamicbar/shared/IslandActions.kt | 58 + .../shared/IslandContentTokens.kt | 569 ++++++++ .../axdynamicbar/shared/IslandEventMapper.kt | 206 +++ .../ui/AxDynamicBarChipViewModel.kt | 206 +++ .../ui/AxDynamicBarExpandedPanel.kt | 396 +++++ .../axdynamicbar/ui/AxDynamicBarManager.kt | 20 + .../ui/compose/AxDynamicBarChip.kt | 367 +++++ .../ui/compose/AxDynamicBarKeyguardChip.kt | 861 +++++++++++ .../ui/compose/AxDynamicBarNowBar.kt | 301 ++++ .../ui/compose/ExpandedAlertContent.kt | 108 ++ .../ui/compose/ExpandedAppHistoryContent.kt | 140 ++ .../ui/compose/ExpandedClipboardContent.kt | 294 ++++ .../ui/compose/ExpandedIslandContent.kt | 245 ++++ .../ui/compose/ExpandedMediaContent.kt | 504 +++++++ .../ui/compose/ExpandedNotificationContent.kt | 718 +++++++++ .../ui/compose/ExpandedNowPlayingContent.kt | 102 ++ .../ui/compose/ExpandedOngoingContent.kt | 284 ++++ .../ui/compose/ExpandedRecordingContent.kt | 372 +++++ .../ui/compose/ExpandedSportsContent.kt | 288 ++++ .../ui/compose/ExpandedSystemContent.kt | 376 +++++ .../ui/compose/ExpandedTimerContent.kt | 312 ++++ .../ui/compose/ExpandedTorchContent.kt | 138 ++ .../ui/compose/KeyguardExpandedContent.kt | 846 +++++++++++ .../ui/compose/MagneticSwipeToDismiss.kt | 185 +++ .../ui/compose/NotificationAlertCard.kt | 692 +++++++++ .../ui/compose/PillIslandContent.kt | 1290 +++++++++++++++++ .../dagger/SystemUICoreStartableModule.kt | 13 + .../blueprints/DefaultKeyguardBlueprint.kt | 3 + .../blueprints/SplitShadeKeyguardBlueprint.kt | 3 + .../AxDynamicBarKeyguardChipSection.kt | 174 +++ .../KeyguardIndicationController.java | 53 + .../CommonVisualInterruptionSuppressors.kt | 10 + ...otificationInterruptStateProviderImpl.java | 11 +- .../VisualInterruptionDecisionProviderImpl.kt | 2 + .../phone/KeyguardIndicationTextView.java | 16 + .../KeyguardStatusBarViewController.java | 19 +- .../shared/ui/composable/StatusBarRoot.kt | 22 +- .../com/android/systemui/util/ScrimUtils.kt | 61 + 57 files changed, 14618 insertions(+), 8 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/BiometricIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SmartspaceIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SystemIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAlertContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAppHistoryContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedClipboardContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNowPlayingContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSportsContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTimerContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTorchContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 09083dbc2c79..02ac38b4147b 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -229,6 +229,7 @@ + diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index ee73c4662c7d..a145aab02880 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -226,4 +226,99 @@ Sleep Mode + + + Timer + Stopwatch + Alarm + Flashlight + Hotspot + Hotspot Active + VPN Active + Screen Recording + Now Playing + Media output + Sound Mode + Silent + Vibrate + Ring + Charging + Wireless + Saver + Music + Copied + Copied! + Image copied + Copy Image + Clipboard image + Image + URL + Dismiss + Open + Stop + Stop Recording + Reset + Clear All + Show more + Show less + Play + Pause + Paused + Saved + Recording + Done + Ringing + Running + ON + Recents + Unlocked + Device Unlocked + Progress + Casting to + Camera & Microphone + Camera + Microphone + Camera & Mic + %d devices + Wireless Charging + until full + Battery Saver + Incoming Call + Connected + Disconnect + No devices + 1 device connected + %d devices connected + VPN Connected + Secured + Connecting\u2026 + 1 device + Bluetooth device + %d apps + Just now + %dm ago + %dh ago + %dd ago + %dw ago + Resume + Clipboard + Previous + Next + Shuffle + Output + Rec + Cast + Active + Biometric + Trust + Alignment + Cam + Mic + Recent Apps + %d running + vs + LIVE + Final + Halftime + Upcoming diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml index 78b7e95e8bfa..d3ed53afd081 100644 --- a/packages/SystemUI/res/xml/fileprovider.xml +++ b/packages/SystemUI/res/xml/fileprovider.xml @@ -19,4 +19,5 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt new file mode 100644 index 000000000000..c008b7a7ecc3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt @@ -0,0 +1,272 @@ +package com.android.systemui.axdynamicbar.data + +import android.util.Log +import com.android.systemui.axdynamicbar.data.source.AppTrackingIslandManager +import com.android.systemui.axdynamicbar.data.source.BiometricIslandManager +import com.android.systemui.axdynamicbar.data.source.ConnectivityIslandManager +import com.android.systemui.axdynamicbar.data.source.MediaIslandManager +import com.android.systemui.axdynamicbar.data.source.NotificationIslandManager +import com.android.systemui.axdynamicbar.data.source.PrivacyIslandManager +import com.android.systemui.axdynamicbar.data.source.ScreenRecordIslandManager +import com.android.systemui.axdynamicbar.data.source.SmartspaceIslandManager +import com.android.systemui.axdynamicbar.data.source.SystemIslandManager +import com.android.systemui.axdynamicbar.data.source.TorchIslandManager +import com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +@SysUISingleton +class IslandEventRepository +@Inject +constructor( + val screenRecord: ScreenRecordIslandManager, + val privacy: PrivacyIslandManager, + val media: MediaIslandManager, + val connectivity: ConnectivityIslandManager, + val system: SystemIslandManager, + val notification: NotificationIslandManager, + val appTracking: AppTrackingIslandManager, + val torch: TorchIslandManager, + val biometric: BiometricIslandManager, + val smartspace: SmartspaceIslandManager, + private val settings: AxDynamicBarSettings, +) { + companion object { + private const val TAG = "IslandEventRepository" + } + + @Volatile private var listenersStarted = false + + private val _indicationEvents = + MutableStateFlow>(emptyMap()) + + fun updateIndicationEvent(event: IslandEvent.KeyguardIndication) { + _indicationEvents.update { it + (event.indicationType.name to event) } + } + + fun clearIndicationEvent(type: IslandEvent.KeyguardIndication.IndicationType) { + _indicationEvents.update { it - type.name } + } + + fun clearAllIndicationEvents() { + _indicationEvents.value = emptyMap() + } + + val events: Flow> = buildEventsFlow() + + private val disabled get() = settings.disabledEventTypes.value + + private fun isTypeEnabled(typeId: String): Boolean = typeId !in disabled + + fun startListening() { + if (listenersStarted) return + listenersStarted = true + Log.d(TAG, "Starting event listeners") + notification.onScreenRecordNotificationTime = { timeMs -> + screenRecord.updateNotificationStartTime(timeMs) + } + syncDisabledTypes() + if (isTypeEnabled("screen_recording")) screenRecord.startListening() + if (isTypeEnabled("privacy")) privacy.startListening() + if (isTypeEnabled("media")) media.startListening() + if (isTypeEnabled("bluetooth")) connectivity.startBluetooth() + if (isTypeEnabled("hotspot")) connectivity.startHotspot() + if (isTypeEnabled("casting")) connectivity.startCast() + if (isTypeEnabled("vpn")) connectivity.startVpn() + if (isTypeEnabled("charging")) system.startCharging() + if (isTypeEnabled("ringer")) system.startRinger() + if (isTypeEnabled("clipboard")) system.startClipboard() + notification.startListening() + if (isTypeEnabled("app_switch")) appTracking.startListening() + if (isTypeEnabled("torch")) torch.startListening() + if (isTypeEnabled("biometric_unlock")) biometric.startListening() + if (isTypeEnabled("media") || isTypeEnabled("sports")) smartspace.startListening() + } + + fun stopListening() { + if (!listenersStarted) return + listenersStarted = false + Log.d(TAG, "Stopping event listeners") + screenRecord.stopListening() + privacy.stopListening() + media.stopListening() + connectivity.stopListening() + system.stopListening() + notification.stopListening() + torch.stopListening() + appTracking.stopListening() + biometric.stopListening() + smartspace.stopListening() + } + + fun refreshListeners() { + if (!listenersStarted) return + syncDisabledTypes() + + if (isTypeEnabled("screen_recording")) screenRecord.startListening() + else screenRecord.stopListening() + if (isTypeEnabled("privacy")) privacy.startListening() + else privacy.stopListening() + if (isTypeEnabled("media")) media.startListening() + else media.stopListening() + + if (isTypeEnabled("bluetooth")) connectivity.startBluetooth() + else connectivity.stopBluetooth() + if (isTypeEnabled("hotspot")) connectivity.startHotspot() + else connectivity.stopHotspot() + if (isTypeEnabled("casting")) connectivity.startCast() + else connectivity.stopCast() + if (isTypeEnabled("vpn")) connectivity.startVpn() + else connectivity.stopVpn() + + if (isTypeEnabled("charging")) system.startCharging() + else system.stopCharging() + if (isTypeEnabled("ringer")) system.startRinger() + else system.stopRinger() + if (isTypeEnabled("clipboard")) system.startClipboard() + else system.stopClipboard() + + if (isTypeEnabled("app_switch")) appTracking.startListening() + else appTracking.stopListening() + if (isTypeEnabled("torch")) torch.startListening() + else torch.stopListening() + if (isTypeEnabled("biometric_unlock")) biometric.startListening() + else biometric.stopListening() + + if (isTypeEnabled("media") || isTypeEnabled("sports")) smartspace.startListening() + else smartspace.stopListening() + } + + private fun syncDisabledTypes() { + notification.disabledTypes = disabled + } + + private fun buildEventsFlow(): Flow> { + + val micCamFiltered = + combine(privacy.micCamEvent, notification.audioRecordingEvent) { micCam, audioRec -> + if (audioRec != null && micCam != null && micCam.isMic && !micCam.isCam) null + else micCam + } + + val castingFiltered = + combine( + connectivity.castingEvent, + screenRecord.screenRecordEvent, + ) { cast, rec -> + if (rec != null) null else cast + } + + val highGroupA = + combine( + screenRecord.screenRecordEvent, + micCamFiltered, + castingFiltered, + ) { rec, micCam, cast -> + listOfNotNull( + rec?.takeIf { isTypeEnabled("screen_recording") }, + micCam?.takeIf { isTypeEnabled("privacy") }, + cast?.takeIf { isTypeEnabled("casting") }, + ) + } + val sportsGroup = combine( + smartspace.sportsEvents, + notification.sportsEvents, + ) { qlSports, notifSports -> + if (!isTypeEnabled("sports")) emptyList() + else qlSports + notifSports.filter { ns -> + qlSports.none { qs -> + qs.team1Name.equals(ns.team1Name, ignoreCase = true) && + qs.team2Name.equals(ns.team2Name, ignoreCase = true) + } + } + } + val promotedGroup = combine( + notification.promotedOngoingEvents, + sportsGroup, + ) { promoted, sports -> + (if (isTypeEnabled("promoted_ongoing")) promoted else emptyList()) + sports + } + val highGroupB = + combine(highGroupA, torch.torchEvent) { events, t -> + events + listOfNotNull(t?.takeIf { isTypeEnabled("torch") }) + } + val highGroup = + combine(highGroupB, biometric.biometricEvent) { events, bio -> + events + listOfNotNull(bio?.takeIf { isTypeEnabled("biometric_unlock") }) + } + val midGroup = + combine( + media.mediaEvent, + connectivity.bluetoothEvent, + connectivity.hotspotEvent, + system.chargingEvent, + notification.alarmEvent, + ) { m, bt, hotspot, charging, alarm -> + listOfNotNull( + m?.takeIf { isTypeEnabled("media") }, + bt?.takeIf { isTypeEnabled("bluetooth") }, + hotspot?.takeIf { isTypeEnabled("hotspot") }, + charging?.takeIf { isTypeEnabled("charging") }, + alarm?.takeIf { isTypeEnabled("alarm") }, + ) + } + val lowGroupA = + combine( + notification.timerEvent, + notification.stopwatchEvent, + system.ringerEvent, + connectivity.vpnEvent, + system.clipboardEvent, + ) { timer, stopwatch, ringer, vpn, clipboard -> + listOfNotNull( + timer?.takeIf { isTypeEnabled("timer") }, + stopwatch?.takeIf { isTypeEnabled("stopwatch") }, + ringer?.takeIf { isTypeEnabled("ringer") }, + vpn?.takeIf { isTypeEnabled("vpn") }, + clipboard?.takeIf { isTypeEnabled("clipboard") }, + ) + } + val lowGroup = + combine( + lowGroupA, + appTracking.appSwitchEvent, + notification.audioRecordingEvent, + smartspace.nowPlayingEvent, + ) { a, appSwitch, audioRec, nowPlaying -> + a + listOfNotNull( + appSwitch?.takeIf { isTypeEnabled("app_switch") }, + audioRec?.takeIf { isTypeEnabled("audio_recording") }, + nowPlaying?.takeIf { isTypeEnabled("media") }, + ) + } + val transientGroup = combine(midGroup, lowGroup) { mid, low -> mid + low } + + val indicationGroup = _indicationEvents.map { it.values.toList() } + + val allEvents = combine( + highGroup, + transientGroup, + promotedGroup, + indicationGroup, + ) { high, transient, promoted, indication -> + high + transient + promoted + indication + } + + return allEvents.map { events -> + events.sorted() + }.distinctUntilChanged { old, new -> + old.size == new.size && old.indices.all { i -> + old[i].withoutDrawables() == new[i].withoutDrawables() + } + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt new file mode 100644 index 000000000000..7529aa5db630 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt @@ -0,0 +1,178 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shared.system.TaskStackChangeListener +import com.android.systemui.shared.system.TaskStackChangeListeners +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class AppTrackingIslandManager @Inject constructor(@Application private val context: Context) { + companion object { + private const val TAG = "AppTrackingIslandManager" + private const val MAX_RECENT = 8 + } + + private val _appSwitchEvent = MutableStateFlow(null) + val appSwitchEvent: StateFlow = _appSwitchEvent.asStateFlow() + + private val recentApps = mutableListOf() + private var listening = false + private var launcherPackage: String? = null + private val activityManager by lazy { + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + } + private var switchesSincePrune = 0 + + @Volatile private var currentForegroundPkg: String? = null + + private fun emitEvent() { + _appSwitchEvent.value = + if (recentApps.isNotEmpty()) + IslandEvent.AppSwitch( + recentApps = recentApps.toList(), + previousApp = _appSwitchEvent.value?.previousApp, + ) + else null + } + + private val listener = + object : TaskStackChangeListener { + override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo) { + val topActivity = taskInfo.topActivity ?: return + val pkg = topActivity.packageName + if ( + pkg == launcherPackage || + pkg == "com.android.systemui" || + pkg == "com.android.launcher3" + ) { + currentForegroundPkg = null + return + } + + val leavingPkg = currentForegroundPkg + currentForegroundPkg = pkg + + val appName = + try { + context.packageManager + .getApplicationLabel(context.packageManager.getApplicationInfo(pkg, 0)) + .toString() + } catch (_: Exception) { + pkg + } + + val appIcon = + try { + context.packageManager.getApplicationIcon(pkg) + } catch (_: Exception) { + null + } + + val app = + IslandEvent.RecentApp( + packageName = pkg, + appName = appName, + appIcon = appIcon, + taskId = taskInfo.taskId, + lastActiveTime = System.currentTimeMillis(), + ) + + synchronized(recentApps) { + recentApps.removeAll { it.packageName == pkg } + recentApps.add(0, app) + while (recentApps.size > MAX_RECENT) recentApps.removeLast() + + if (++switchesSincePrune >= 3) { + switchesSincePrune = 0 + val runningTaskIds = + activityManager.getRunningTasks(MAX_RECENT).map { it.taskId }.toSet() + recentApps.removeAll { it.taskId !in runningTaskIds } + } + + val newPreviousApp = + if (leavingPkg != null && leavingPkg != pkg) { + recentApps.find { it.packageName == leavingPkg } + } else { + _appSwitchEvent.value?.previousApp + } + + _appSwitchEvent.value = + if (recentApps.isNotEmpty()) + IslandEvent.AppSwitch( + recentApps = recentApps.toList(), + previousApp = newPreviousApp, + ) + else null + } + } + + override fun onTaskRemoved(taskId: Int) { + synchronized(recentApps) { + val removed = recentApps.removeAll { it.taskId == taskId } + if (removed) emitEvent() + } + } + } + + fun startListening() { + if (listening) return + listening = true + launcherPackage = resolveLauncherPackage() + TaskStackChangeListeners.getInstance().registerTaskStackListener(listener) + } + + fun stopListening() { + if (!listening) return + listening = false + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(listener) + synchronized(recentApps) { + recentApps.clear() + _appSwitchEvent.value = null + } + } + + fun clear() { + _appSwitchEvent.value = null + } + + fun refreshRecentApps() { + synchronized(recentApps) { + val runningTaskIds = activityManager.getRunningTasks(MAX_RECENT + 4).map { it.taskId }.toSet() + val removed = recentApps.removeAll { it.taskId !in runningTaskIds } + if (removed) emitEvent() + } + } + + fun switchToApp(taskId: Int) { + try { + activityManager.moveTaskToFront(taskId, 0) + } catch (e: Exception) { + Log.w(TAG, "Failed to switch to task $taskId", e) + synchronized(recentApps) { + recentApps.removeAll { it.taskId == taskId } + emitEvent() + } + } + } + + private fun resolveLauncherPackage(): String? = + try { + val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + val resolveInfo = + context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + resolveInfo?.activityInfo?.packageName + } catch (_: Exception) { + null + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/BiometricIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/BiometricIslandManager.kt new file mode 100644 index 000000000000..e3226d74a729 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/BiometricIslandManager.kt @@ -0,0 +1,64 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.hardware.biometrics.BiometricSourceType +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class BiometricIslandManager +@Inject +constructor(private val keyguardUpdateMonitor: KeyguardUpdateMonitor) { + private var listening = false + private val _biometricEvent = MutableStateFlow(null) + val biometricEvent: StateFlow = _biometricEvent.asStateFlow() + + var onBiometricUnlock: ((IslandEvent.BiometricUnlock) -> Unit)? = null + + private val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onBiometricAuthenticated( + userId: Int, + biometricSourceType: BiometricSourceType, + isStrongBiometric: Boolean, + ) { + val name = + when (biometricSourceType) { + BiometricSourceType.FINGERPRINT -> "Fingerprint" + BiometricSourceType.FACE -> "Face" + BiometricSourceType.IRIS -> "Iris" + else -> "Biometric" + } + val event = + IslandEvent.BiometricUnlock( + sourceType = biometricSourceType.ordinal, + sourceName = name, + ) + _biometricEvent.value = event + onBiometricUnlock?.invoke(event) + } + } + + fun startListening() { + if (listening) return + listening = true + keyguardUpdateMonitor.registerCallback(callback) + } + + fun stopListening() { + if (!listening) return + listening = false + keyguardUpdateMonitor.removeCallback(callback) + _biometricEvent.value = null + } + + fun clear() { + _biometricEvent.value = null + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt new file mode 100644 index 000000000000..fa8abe7bff52 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt @@ -0,0 +1,261 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.util.Log +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.MediaRouterChipInteractor +import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel +import com.android.systemui.statusbar.policy.BluetoothController +import com.android.systemui.statusbar.policy.HotspotController +import com.android.systemui.statusbar.policy.vpn.domain.interactor.VpnInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@SysUISingleton +class ConnectivityIslandManager +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val bluetoothController: BluetoothController, + private val hotspotController: HotspotController, + private val mediaRouterChipInteractor: MediaRouterChipInteractor, + private val vpnInteractor: VpnInteractor, +) { + companion object { + private const val TAG = "ConnectivityIslandManager" + } + + private val _bluetoothEvent = MutableStateFlow(null) + val bluetoothEvent: StateFlow = _bluetoothEvent.asStateFlow() + + private val _hotspotEvent = MutableStateFlow(null) + val hotspotEvent: StateFlow = _hotspotEvent.asStateFlow() + + private val _castingEvent = MutableStateFlow(null) + val castingEvent: StateFlow = _castingEvent.asStateFlow() + + private val _vpnEvent = MutableStateFlow(null) + val vpnEvent: StateFlow = _vpnEvent.asStateFlow() + + private val previousBtAddresses = java.util.Collections.synchronizedSet(mutableSetOf()) + private var listening = false + private var wasVpnEnabled = false + private var vpnJob: Job? = null + private var castJob: Job? = null + + private val bluetoothCallback = + object : BluetoothController.Callback { + override fun onBluetoothStateChange(enabled: Boolean) { + if (!enabled) { + previousBtAddresses.clear() + _bluetoothEvent.value = null + } + } + + override fun onBluetoothDevicesChanged() { + val devices = bluetoothController.connectedDevices + val currentAddresses = devices.map { it.getAddress() }.toSet() + val newlyConnected = devices.filter { it.getAddress() !in previousBtAddresses } + previousBtAddresses.clear() + previousBtAddresses.addAll(currentAddresses) + + if (newlyConnected.isNotEmpty()) { + val device = newlyConnected.first() + val iconPair = + try { + device.getDrawableWithDescription() + } catch (_: Exception) { + null + } + val event = + IslandEvent.Bluetooth( + deviceName = device.getName() ?: "Unknown Device", + batteryLevel = device.getBatteryLevel(), + address = device.getAddress(), + deviceIcon = iconPair?.first, + deviceTypeLabel = iconPair?.second ?: "", + ) + _bluetoothEvent.value = event + } else { + + val current = _bluetoothEvent.value + if (current != null && current.address !in currentAddresses) { + _bluetoothEvent.value = null + } + } + } + } + + private val hotspotCallback = + object : HotspotController.Callback { + override fun onHotspotChanged(enabled: Boolean, numDevices: Int) { + if (enabled) { + _hotspotEvent.value = IslandEvent.Hotspot(numDevices = numDevices) + } else { + _hotspotEvent.value = null + } + } + } + + private fun startCastListener() { + castJob?.cancel() + castJob = + applicationScope.launch(backgroundDispatcher) { + mediaRouterChipInteractor.mediaRouterCastingState.collect { state -> + _castingEvent.value = + when (state) { + is MediaRouterCastModel.Casting -> + IslandEvent.Casting(deviceName = state.deviceName ?: "Screen") + is MediaRouterCastModel.DoingNothing -> null + } + } + } + } + + private fun startVpnListener() { + vpnJob?.cancel() + vpnJob = + applicationScope.launch(backgroundDispatcher) { + vpnInteractor.vpnState.collect { state -> + if (state.isEnabled) { + val existing = _vpnEvent.value + _vpnEvent.value = + if (existing != null) { + existing.copy( + isBranded = state.isBranded, + isValidated = state.isValidated, + ) + } else { + IslandEvent.Vpn( + isBranded = state.isBranded, + isValidated = state.isValidated, + ) + } + } else { + _vpnEvent.value = null + } + wasVpnEnabled = state.isEnabled + } + } + } + + private var btListening = false + private var hotspotListening = false + private var castListening = false + private var vpnListening = false + + fun startBluetooth() { + if (btListening) return + btListening = true + previousBtAddresses.clear() + bluetoothController.connectedDevices.forEach { previousBtAddresses.add(it.getAddress()) } + bluetoothController.addCallback(bluetoothCallback) + } + + fun stopBluetooth() { + if (!btListening) return + btListening = false + bluetoothController.removeCallback(bluetoothCallback) + previousBtAddresses.clear() + _bluetoothEvent.value = null + } + + fun startHotspot() { + if (hotspotListening) return + hotspotListening = true + if (hotspotController.isHotspotEnabled) { + _hotspotEvent.value = IslandEvent.Hotspot(hotspotController.getNumConnectedDevices()) + } + hotspotController.addCallback(hotspotCallback) + } + + fun stopHotspot() { + if (!hotspotListening) return + hotspotListening = false + hotspotController.removeCallback(hotspotCallback) + _hotspotEvent.value = null + } + + fun startCast() { + if (castListening) return + castListening = true + startCastListener() + } + + fun stopCast() { + if (!castListening) return + castListening = false + castJob?.cancel() + castJob = null + _castingEvent.value = null + } + + fun startVpn() { + if (vpnListening) return + vpnListening = true + wasVpnEnabled = false + startVpnListener() + } + + fun stopVpn() { + if (!vpnListening) return + vpnListening = false + vpnJob?.cancel() + vpnJob = null + _vpnEvent.value = null + } + + fun startListening() { + if (listening) return + listening = true + startBluetooth() + startHotspot() + startCast() + startVpn() + } + + fun stopListening() { + if (!listening) return + listening = false + stopBluetooth() + stopHotspot() + stopCast() + stopVpn() + } + + fun disconnectBluetooth(address: String) { + if (address.isEmpty()) return + try { + bluetoothController.connectedDevices.find { it.getAddress() == address }?.disconnect() + _bluetoothEvent.value = null + } catch (e: Exception) { + Log.w(TAG, "Failed to disconnect Bluetooth", e) + } + } + + fun clearBluetooth() { + _bluetoothEvent.value = null + } + + fun clearHotspot() { + _hotspotEvent.value = null + } + + fun clearCasting() { + _castingEvent.value = null + } + + fun clearVpn() { + _vpnEvent.value = null + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt new file mode 100644 index 000000000000..36e925bebdff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt @@ -0,0 +1,343 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionManager as SystemMediaSessionManager +import android.media.session.PlaybackState +import android.os.Handler +import android.os.SystemClock +import android.util.Log +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.MediaSessionManager +import com.android.systemui.media.NotificationMediaManager +import com.android.systemui.media.dialog.MediaOutputDialogManager +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class MediaIslandManager +@Inject +constructor( + @Application private val context: Context, + @Main private val mainHandler: Handler, + private val notificationMediaManager: NotificationMediaManager, + private val mediaOutputDialogManager: MediaOutputDialogManager, +) { + companion object { + private const val TAG = "MediaIslandManager" + } + + private val _mediaEvent = MutableStateFlow(null) + val mediaEvent: StateFlow = _mediaEvent.asStateFlow() + + var activeMediaPackage: String? = null + private set + + var onMediaSessionLost: (() -> Unit)? = null + + @Volatile private var listening = false + @Volatile private var sessionMediaColor: Int = 0 + @Volatile private var sessionAlbumArt: Drawable? = null + @Volatile private var sessionAppIcon: Drawable? = null + private val systemMediaSessionManager: SystemMediaSessionManager by lazy { + context.getSystemService(SystemMediaSessionManager::class.java) + } + private var activeMediaController: MediaController? = null + + private val mediaControllerCallback = + object : MediaController.Callback() { + override fun onPlaybackStateChanged(state: PlaybackState?) { + if (state != null) { + updatePosition(state) + } + } + + override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { + + val current = _mediaEvent.value ?: return + _mediaEvent.value = current.copy(outputDeviceName = getOutputDeviceName()) + } + + override fun onSessionDestroyed() { + activeMediaController = null + onMediaSessionLost?.invoke() + } + } + + private val sessionChangedListener = + SystemMediaSessionManager.OnActiveSessionsChangedListener { controllers -> + bindController(controllers) + } + + private val mediaSessionListener = object : MediaSessionManager.MediaDataListener { + override fun onMediaColorsChanged(color: Int) { + sessionMediaColor = color + val current = _mediaEvent.value ?: return + _mediaEvent.value = current.copy(mediaColor = color) + } + + override fun onAlbumArtChanged(drawable: Drawable) { + sessionAlbumArt = drawable + val current = _mediaEvent.value ?: return + _mediaEvent.value = current.copy(albumArt = drawable) + } + + override fun onAppIconChanged(drawable: Drawable) { + sessionAppIcon = drawable + val current = _mediaEvent.value ?: return + _mediaEvent.value = current.copy(appIcon = drawable) + } + } + + private fun bindController(controllers: List?) { + activeMediaController?.unregisterCallback(mediaControllerCallback) + activeMediaController = controllers?.firstOrNull() + activeMediaController?.registerCallback(mediaControllerCallback, mainHandler) + activeMediaController?.playbackState?.let { + updatePosition(it) + } + + if (controllers.isNullOrEmpty() && _mediaEvent.value != null) { + onMediaSessionLost?.invoke() + } + } + + private fun isInMotion(state: PlaybackState): Boolean = + state.state == PlaybackState.STATE_PLAYING || + state.state == PlaybackState.STATE_FAST_FORWARDING || + state.state == PlaybackState.STATE_REWINDING + + private fun computeAccuratePosition(state: PlaybackState): Long { + val basePos = state.position.coerceAtLeast(0L) + if (!isInMotion(state)) return basePos + val updateTime = state.lastPositionUpdateTime + if (updateTime <= 0) return basePos + val elapsed = SystemClock.elapsedRealtime() - updateTime + val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f + val duration = _mediaEvent.value?.duration ?: Long.MAX_VALUE + return (basePos + (elapsed * speed).toLong()).coerceIn(0L, duration) + } + + private fun updatePosition(state: PlaybackState) { + val current = _mediaEvent.value ?: return + val duration = current.duration.takeIf { it > 0L } ?: return + val posMs = computeAccuratePosition(state) + val progress = (posMs.toFloat() / duration.toFloat()).coerceIn(0f, 1f) + val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f + _mediaEvent.value = current.copy( + position = posMs, + progress = progress, + playbackSpeed = speed, + positionUpdateTime = state.lastPositionUpdateTime, + ) + } + + private fun getActiveController(): MediaController? = + try { + systemMediaSessionManager.getActiveSessions(null).firstOrNull() + } catch (_: Exception) { + null + } + + private val mediaListener = + object : NotificationMediaManager.MediaListener { + override fun onPrimaryMetadataOrStateChanged( + metadata: MediaMetadata?, + @PlaybackState.State state: Int, + ) { + val isPlaying = state == PlaybackState.STATE_PLAYING + val track = + metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + ?: metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + ?: "" + val artist = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "" + val duration = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + val albumArt = sessionAlbumArt ?: run { + val bmp = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + bmp?.let { BitmapDrawable(context.resources, it) } + } + + val controller = getActiveController() + val ps = controller?.playbackState + val existingPos = _mediaEvent.value?.position ?: 0L + val posMs = if (ps != null) computeAccuratePosition(ps) else existingPos + val progress = + if (duration > 0L) (posMs.toFloat() / duration.toFloat()).coerceIn(0f, 1f) + else 0f + val outputDevice = getOutputDeviceName() + val customActions = + ps?.customActions?.take(2)?.mapNotNull { ca -> + val lbl = + ca.name?.toString()?.takeIf { it.isNotEmpty() } + ?: return@mapNotNull null + val act = ca.action?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null + IslandEvent.MediaCustomAction(label = lbl, action = act) + } ?: emptyList() + + val pkg = controller?.packageName + val appIcon = sessionAppIcon + + val speed = ps?.playbackSpeed?.takeIf { it > 0f } ?: 1f + val updateTime = ps?.lastPositionUpdateTime ?: 0L + + if (isPlaying) { + activeMediaPackage = pkg + _mediaEvent.value = + IslandEvent.Media( + track = track, + artist = artist, + isPlaying = true, + albumArt = albumArt, + progress = progress, + duration = duration, + position = posMs, + playbackSpeed = speed, + positionUpdateTime = updateTime, + outputDeviceName = outputDevice, + customActions = customActions, + appIcon = appIcon, + packageName = pkg ?: "", + mediaColor = sessionMediaColor, + ) + } else { + val current = _mediaEvent.value + if (current != null) { + _mediaEvent.value = + current.copy( + isPlaying = false, + albumArt = albumArt ?: current.albumArt, + progress = progress, + position = posMs, + playbackSpeed = speed, + positionUpdateTime = updateTime, + ) + } + } + } + } + + fun startListening() { + if (listening) return + listening = true + notificationMediaManager.addCallback(mediaListener) + MediaSessionManager.get().addListener(mediaSessionListener) + try { + bindController(systemMediaSessionManager.getActiveSessions(null)) + systemMediaSessionManager.addOnActiveSessionsChangedListener( + sessionChangedListener, + null, + mainHandler, + ) + } catch (_: Exception) {} + + } + + fun stopListening() { + if (!listening) return + listening = false + notificationMediaManager.removeCallback(mediaListener) + MediaSessionManager.get().removeListener(mediaSessionListener) + try { + systemMediaSessionManager.removeOnActiveSessionsChangedListener(sessionChangedListener) + activeMediaController?.unregisterCallback(mediaControllerCallback) + activeMediaController = null + } catch (_: Exception) {} + _mediaEvent.value = null + activeMediaPackage = null + sessionMediaColor = 0 + sessionAlbumArt = null + sessionAppIcon = null + } + + fun clear() { + _mediaEvent.value = null + } + + fun togglePlayPause() { + val c = getActiveController() ?: return + when (c.playbackState?.state) { + PlaybackState.STATE_PLAYING -> c.transportControls.pause() + else -> c.transportControls.play() + } + } + + fun skipNext() { + getActiveController()?.transportControls?.skipToNext() + } + + fun skipPrev() { + getActiveController()?.transportControls?.skipToPrevious() + } + + fun seekTo(position: Long) { + getActiveController()?.transportControls?.seekTo(position) + } + + fun sendCustomAction(action: String) { + getActiveController()?.transportControls?.sendCustomAction(action, null) + } + + fun openMediaApp() { + val pkg = getActiveController()?.packageName ?: return + val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + context.startActivity(intent) + } catch (e: Exception) { + Log.w(TAG, "Failed to open media app: $pkg", e) + } + } + + private fun getOutputDeviceName(): String = + try { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val outputs = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + + val primary = + outputs.firstOrNull { + it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && + it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + } ?: outputs.firstOrNull() + when (primary?.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> + primary.productName?.toString()?.takeIf { it.isNotEmpty() } ?: "Bluetooth" + AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset" + AudioDeviceInfo.TYPE_USB_DEVICE -> "USB Audio" + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Headphones" + AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Headset" + AudioDeviceInfo.TYPE_HDMI -> "HDMI" + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Speaker" + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "Earpiece" + null -> "Speaker" + else -> primary.productName?.toString()?.takeIf { it.isNotEmpty() } ?: "Speaker" + } + } catch (_: Exception) { + "Speaker" + } + + fun openMediaOutputSwitcher() { + val pkg = getActiveController()?.packageName ?: return + mainHandler.post { + try { + mediaOutputDialogManager.createAndShow(packageName = pkg, aboveStatusBar = true) + } catch (e: Exception) { + Log.w(TAG, "Failed to open media output switcher", e) + } + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt new file mode 100644 index 000000000000..106a06f9fed7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt @@ -0,0 +1,1027 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.app.Notification +import android.content.Context +import com.android.systemui.res.R +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcelable +import android.os.SystemClock +import android.service.notification.StatusBarNotification +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.Chronometer +import android.widget.FrameLayout +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.util.ScrimUtils +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@SysUISingleton +class NotificationIslandManager +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, +) { + companion object { + private const val TAG = "NotificationIslandManager" + + private const val SCREEN_RECORD_PACKAGE = "com.android.systemui" + private val CLOCK_PACKAGES = setOf("com.google.android.deskclock", "com.android.deskclock") + private val ALARM_PACKAGES = setOf("com.google.android.deskclock", "com.android.deskclock") + + private const val GOOGLE_PACKAGE = "com.google.android.googlequicksearchbox" + private val SPORTS_PACKAGES = setOf( + "com.espn.score_center", + "com.fivemobile.thescore", + "com.yahoo.mobile.client.android.sportacular", + "com.sofascore.app", + "com.fotmob", + "com.foxsports.android", + "com.cbs.sports.android", + "com.bleacherreport.android.teamstream", + "com.nbaimd.gametime.nba2011", + "com.nfl.fantasy.core", + "com.mlb.atbat", + "com.nhl.gc1112.free", + "livescore.livesportsmedia.com", + "com.flashscore.en", + "com.365scores.android", + "lunosoftware.sportsalerts", + "com.onefootball.brasil", + ) + + private val SCORE_PATTERN = + Regex("""(.+?)\s+(\d+)\s*[-–—:]\s*(\d+)\s+(.+)""") + private val VS_PATTERN = + Regex("""(.+?)\s+(?:vs\.?|v\.?|at)\s+(.+)""", RegexOption.IGNORE_CASE) + + private const val NOW_PLAYING_PACKAGE = "com.google.android.as" + private const val NOW_PLAYING_CHANNEL = "ambientmusic" + + private val RECORDER_PACKAGES = + setOf("com.google.android.apps.recorder", "com.android.soundrecorder") + + private val SUPPRESSED_PACKAGES: Set = buildSet { + addAll(CLOCK_PACKAGES) + addAll(ALARM_PACKAGES) + addAll(RECORDER_PACKAGES) + } + } + + private val _timerEvent = MutableStateFlow(null) + val timerEvent: StateFlow = _timerEvent.asStateFlow() + + private val _stopwatchEvent = MutableStateFlow(null) + val stopwatchEvent: StateFlow = _stopwatchEvent.asStateFlow() + + private val _alarmEvent = MutableStateFlow(null) + val alarmEvent: StateFlow = _alarmEvent.asStateFlow() + + private val _notificationEvents = MutableStateFlow>(emptyList()) + val notificationEvents: StateFlow> = + _notificationEvents.asStateFlow() + + private val _audioRecordingEvent = MutableStateFlow(null) + val audioRecordingEvent: StateFlow = + _audioRecordingEvent.asStateFlow() + + private val _promotedOngoingEvents = + MutableStateFlow>(emptyList()) + val promotedOngoingEvents: StateFlow> = + _promotedOngoingEvents.asStateFlow() + + private val _sportsEvents = MutableStateFlow>(emptyList()) + val sportsEvents: StateFlow> = _sportsEvents.asStateFlow() + + private val _nowPlayingEvent = MutableStateFlow(null) + val nowPlayingEvent: StateFlow = _nowPlayingEvent.asStateFlow() + + @Volatile var disabledTypes: Set = emptySet() + + private var recorderPackage: String? = null + + private var recorderNotifKey: String? = null + + private var pauseStartMs: Long = 0L + + private var accumulatedPauseMs: Long = 0L + + val notificationFlow = MutableSharedFlow(extraBufferCapacity = 16) + val notificationRemovedFlow = MutableSharedFlow(extraBufferCapacity = 16) + + var activeMediaPackageProvider: (() -> String?)? = null + + private val seenNotificationKeys = mutableSetOf() + + var onTimerEvent: ((IslandEvent.Timer) -> Unit)? = null + var onAlarmEvent: ((IslandEvent.Alarm) -> Unit)? = null + var onNotificationPosted: ((IslandEvent.Notification) -> Unit)? = null + var onScreenRecordNotificationTime: ((Long) -> Unit)? = null + + @Volatile private var listening = false + @Volatile private var timerJob: Job? = null + + private var timerNotificationKey: String? = null + private var timerOriginalDurationMs: Long = 0L + private var stopwatchNotificationKey: String? = null + + private val scrimListener = + object : ScrimUtils.ScrimEventListener { + override fun onNotificationRemoved(sbn: StatusBarNotification) { + val pkg = sbn.packageName ?: return + seenNotificationKeys.remove(sbn.key) + + if (sbn.key == timerNotificationKey) { + timerNotificationKey = null + timerJob?.cancel() + timerJob = null + _timerEvent.value = null + } + if (sbn.key == stopwatchNotificationKey) { + stopwatchNotificationKey = null + _stopwatchEvent.value = null + } + + if (sbn.key == recorderNotifKey) { + recorderNotifKey = null + val currentState = _audioRecordingEvent.value?.state + if (currentState == RecordingState.SAVED) { + recorderPackage = null + _audioRecordingEvent.value = null + pauseStartMs = 0L + accumulatedPauseMs = 0L + } + } + + _promotedOngoingEvents.value = + _promotedOngoingEvents.value.filter { it.sbn.key != sbn.key } + + _sportsEvents.value = + _sportsEvents.value.filter { it.key != sbn.key } + + if (_nowPlayingEvent.value?.key == sbn.key) { + _nowPlayingEvent.value = null + } + + _notificationEvents.value = + _notificationEvents.value.filter { it.sbn.key != sbn.key } + + notificationRemovedFlow.tryEmit(sbn.key) + } + + override fun onNotificationPosted(sbn: StatusBarNotification) { + val pkg = sbn.packageName ?: return + val extras = sbn.notification?.extras ?: return + + if ( + pkg == SCREEN_RECORD_PACKAGE && + sbn.isOngoing && + extras.getBoolean("android.showChronometer", false) && + sbn.notification.`when` > 0L + ) { + onScreenRecordNotificationTime?.invoke(sbn.notification.`when`) + } + + if (pkg in CLOCK_PACKAGES) { + val channelId = sbn.notification?.channelId?.lowercase() ?: "" + val actionLabels = + sbn.notification?.actions?.map { it.title?.toString()?.lowercase() ?: "" } + ?: emptyList() + val hasLap = actionLabels.any { it.contains("lap") } + val isCountDown = extras.getBoolean("android.chronometerCountDown", false) + + val isStopwatch = channelId.contains("stopwatch") || hasLap + val isTimer = + !isStopwatch && + (channelId.contains("timer") || + channelId.contains("firing") || + isCountDown || + actionLabels.any { it.contains("+1") || it.contains("add") }) + + if (isStopwatch && "stopwatch" !in disabledTypes) { + handleStopwatch(sbn, extras, actionLabels) + return + } + if (isTimer && "timer" !in disabledTypes) { + handleTimer(sbn, extras, actionLabels) + return + } + if (isStopwatch || isTimer) return + } + + val isAlarmCategory = sbn.notification?.category == Notification.CATEGORY_ALARM + if ((isAlarmCategory || pkg in ALARM_PACKAGES) && sbn.isOngoing) { + if ("alarm" !in disabledTypes) handleAlarm(sbn, extras) + return + } + + if (sbn.notification?.category == Notification.CATEGORY_CALL) { + val isCallStyle = + extras.containsKey(Notification.EXTRA_ANSWER_INTENT) || + extras.containsKey(Notification.EXTRA_DECLINE_INTENT) || + extras.containsKey(Notification.EXTRA_HANG_UP_INTENT) + if (isCallStyle) { + handleCallNotification(sbn, extras) + return + } + } + + val allActions = sbn.notification?.actions ?: emptyArray() + val isMedia = sbn.notification?.category == Notification.CATEGORY_TRANSPORT || + sbn.notification?.extras?.containsKey( + Notification.EXTRA_MEDIA_SESSION + ) == true + if (sbn.isOngoing && !isMedia && "audio_recording" !in disabledTypes) { + val recActions = + allActions.filter { a -> + val lbl = a.title?.toString()?.lowercase() ?: "" + lbl.contains("stop") || lbl.contains("pause") || lbl.contains("resume") + } + val hasStop = + recActions.any { a -> + (a.title?.toString()?.lowercase() ?: "").contains("stop") + } + if (hasStop && recActions.size >= 2) { + val isPaused = + recActions.any { a -> + (a.title?.toString()?.lowercase() ?: "").contains("resume") + } + val notifActions = + recActions.mapNotNull { a -> + a.title?.let { + IslandEvent.NotificationAction(label = it, action = a) + } + } + val appName = resolveAppName(pkg) + val existing = _audioRecordingEvent.value + val prevState = existing?.state + + val startTime = + if ( + sbn.notification?.extras?.getBoolean("android.showChronometer") == + true + ) + sbn.notification.`when` + else existing?.startTimeMs ?: System.currentTimeMillis() + + val now = System.currentTimeMillis() + if (isPaused && prevState != RecordingState.PAUSED) { + pauseStartMs = now + } else if ( + !isPaused && prevState == RecordingState.PAUSED + ) { + accumulatedPauseMs += (now - pauseStartMs).coerceAtLeast(0L) + pauseStartMs = 0L + } + + recorderPackage = pkg + recorderNotifKey = sbn.key + _audioRecordingEvent.value = + IslandEvent.AudioRecording( + appName = appName, + state = + if (isPaused) RecordingState.PAUSED + else RecordingState.RECORDING, + startTimeMs = startTime, + actions = notifActions, + pausedDurationMs = accumulatedPauseMs, + ) + return + } + } + + if ( + pkg == recorderPackage && _audioRecordingEvent.value != null && !sbn.isOngoing + ) { + val notifActions = + allActions.mapNotNull { a -> + a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } + } + val title = extras.getString("android.title") ?: "" + val existing = _audioRecordingEvent.value ?: return + recorderNotifKey = sbn.key + _audioRecordingEvent.value = + existing.copy( + appName = title.ifEmpty { existing.appName }, + state = RecordingState.SAVED, + actions = notifActions, + ) + return + } + + if (pkg == NOW_PLAYING_PACKAGE) { + val channel = sbn.notification?.channelId ?: "" + if (channel.contains(NOW_PLAYING_CHANNEL)) { + if ("now_playing" !in disabledTypes) handleNowPlaying(sbn, extras) + return + } + } + + if (pkg in SUPPRESSED_PACKAGES) return + + if ("sports" !in disabledTypes) { + if (pkg == GOOGLE_PACKAGE) { + val groupKey = sbn.groupKey ?: "" + val isSportsGroup = groupKey.contains("::sports", ignoreCase = true) + if (isSportsGroup && handleSportsScore(sbn, extras, forceCapture = true)) return + } + if (pkg in SPORTS_PACKAGES && handleSportsScore(sbn, extras, forceCapture = true)) return + } + + if (sbn.isOngoing && isPromotable(sbn, extras)) { + if ("promoted_ongoing" !in disabledTypes) handlePromotedOngoing(sbn, extras, pkg) + return + } + + val indeterminate = extras.getBoolean("android.progressIndeterminate", false) + val progressRaw = extras.getInt("android.progress", -1) + val progressMax = extras.getInt("android.progressMax", 0) + val hasProgress = + indeterminate || + (extras.containsKey("android.progress") && + progressMax > 0 && + progressRaw >= 0) + + if (sbn.isOngoing && hasProgress) { + if ("promoted_ongoing" !in disabledTypes) handlePromotedOngoing(sbn, extras, pkg) + return + } + if (sbn.isOngoing) return + if ("notification" in disabledTypes) return + val category = sbn.notification?.category + if (category == Notification.CATEGORY_TRANSPORT) return + if (category == Notification.CATEGORY_SERVICE && !hasProgress) return + if (sbn.packageName == activeMediaPackageProvider?.invoke()) return + + val notifFlags = sbn.notification?.flags ?: 0 + if (notifFlags and Notification.FLAG_GROUP_SUMMARY != 0) return + val groupKey = if (sbn.isGroup) sbn.groupKey else null + + val title = extras.getString("android.title") + val text = + extras.getCharSequence("android.bigText")?.toString()?.takeIf { + it.isNotEmpty() + } ?: extras.getString("android.text") + + if (!seenNotificationKeys.add(sbn.key)) return + + val icon = + try { + context.packageManager.getApplicationIcon(pkg) + } catch (_: Exception) { + null + } + val appName = + try { + context.packageManager + .getApplicationLabel(context.packageManager.getApplicationInfo(pkg, 0)) + .toString() + } catch (_: Exception) { + pkg + } + + val allNotifActions = sbn.notification?.actions ?: emptyArray() + + val actions = + allNotifActions + .filter { a -> a.remoteInputs.isNullOrEmpty() && a.actionIntent != null } + .take(2) + .mapNotNull { a -> + a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } + } + + val replyAction = + allNotifActions + .firstOrNull { a -> + !a.remoteInputs.isNullOrEmpty() && a.actionIntent != null + } + ?.let { a -> + val remoteInput = a.remoteInputs!!.first() + IslandEvent.ReplyAction( + label = a.title ?: remoteInput.label ?: "Reply", + action = a, + remoteInput = remoteInput, + ) + } + + var senderIcon: Drawable? = null + var senderName: String? = null + var latestMessageText: String? = null + var isConversation = false + var isGroupConversation = false + var conversationTitle: String? = null + val notif = sbn.notification + if ( + notif != null && + notif.isStyle(Notification.MessagingStyle::class.java) && + notif.extras != null + ) { + isConversation = true + isGroupConversation = notif.extras.getBoolean( + Notification.EXTRA_IS_GROUP_CONVERSATION, false + ) + conversationTitle = notif.extras.getCharSequence( + Notification.EXTRA_CONVERSATION_TITLE + )?.toString()?.takeIf { it.isNotEmpty() } + val messagesArray = + notif.extras.getParcelableArray( + Notification.EXTRA_MESSAGES, + Parcelable::class.java, + ) + if (messagesArray != null && messagesArray.isNotEmpty()) { + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + messagesArray + ) + val lastMessage = messages.maxByOrNull { it.timestamp } + val sender = lastMessage?.senderPerson + senderName = sender?.name?.toString() + latestMessageText = lastMessage?.text?.toString() + senderIcon = + try { + sender?.icon?.loadDrawable(context) + } catch (_: Exception) { + null + } + } + + if (senderIcon == null) { + senderIcon = + try { + notif.extras + .getParcelable( + Notification.EXTRA_CONVERSATION_ICON, + Icon::class.java, + ) + ?.loadDrawable(context) + } catch (_: Exception) { + null + } + } + + if (senderIcon == null) { + senderIcon = + try { + notif.getLargeIcon()?.loadDrawable(context) + } catch (_: Exception) { + null + } + } + } + + val notificationImage = extractNotificationImage(extras, sbn) + + val event = + IslandEvent.Notification( + sbn = sbn, + title = title, + text = latestMessageText ?: text, + appIcon = icon, + appName = appName, + progress = if (hasProgress && !indeterminate) progressRaw else -1, + progressMax = progressMax.coerceAtLeast(1), + isProgressIndeterminate = indeterminate, + actions = actions, + replyAction = replyAction, + isConversation = isConversation, + isGroupConversation = isGroupConversation, + conversationTitle = conversationTitle, + senderIcon = senderIcon, + senderName = senderName, + groupKey = groupKey, + notificationImage = notificationImage, + ) + applicationScope.launch { notificationFlow.emit(event) } + onNotificationPosted?.invoke(event) + } + } + + fun startListening() { + if (listening) return + listening = true + ScrimUtils.get().addListener(scrimListener) + } + + fun stopListening() { + if (!listening) return + listening = false + ScrimUtils.get().removeListener(scrimListener) + seenNotificationKeys.clear() + timerJob?.cancel() + timerJob = null + _timerEvent.value = null + _stopwatchEvent.value = null + _alarmEvent.value = null + _notificationEvents.value = emptyList() + _promotedOngoingEvents.value = emptyList() + _sportsEvents.value = emptyList() + _nowPlayingEvent.value = null + _audioRecordingEvent.value = null + recorderPackage = null + recorderNotifKey = null + pauseStartMs = 0L + accumulatedPauseMs = 0L + } + + fun clearAudioRecording() { + _audioRecordingEvent.value = null + recorderPackage = null + recorderNotifKey = null + pauseStartMs = 0L + accumulatedPauseMs = 0L + } + + fun dismissNotification(event: IslandEvent.Notification) { + _notificationEvents.value = _notificationEvents.value.filter { it.id != event.id } + } + + fun coalesceNotification(event: IslandEvent.Notification) { + val current = _notificationEvents.value.toMutableList() + current.removeAll { it.id == event.id } + current.add(0, event) + _notificationEvents.value = current + } + + fun clearTimer() { + _timerEvent.value = null + timerOriginalDurationMs = 0L + } + + fun clearStopwatch() { + _stopwatchEvent.value = null + } + + fun clearAlarm() { + _alarmEvent.value = null + } + + private fun handleTimer( + sbn: StatusBarNotification, + extras: Bundle, + actionLabels: List = emptyList(), + ) { + val label = extras.getString("android.title") ?: context.getString(R.string.ax_dynamic_bar_timer) + val icon = + try { + context.packageManager.getApplicationIcon(sbn.packageName) + } catch (_: Exception) { + null + } + + val hasPauseAction = actionLabels.any { it.contains("pause") } + val hasResumeAction = actionLabels.any { + it.contains("resume") || it.contains("play") || + (it.contains("start") && !it.contains("stop")) + } + val isPaused = hasResumeAction || (actionLabels.isNotEmpty() && !hasPauseAction) + + var endTimeMs = sbn.notification.`when` + if (endTimeMs <= System.currentTimeMillis()) { + val chronoBase = extractChronometerBase(sbn) + if (chronoBase > 0L) { + val remaining = chronoBase - SystemClock.elapsedRealtime() + endTimeMs = if (remaining > 0L) System.currentTimeMillis() + remaining else 0L + } else { + endTimeMs = 0L + } + } + + if (isPaused && endTimeMs > System.currentTimeMillis()) { + val existing = _timerEvent.value + if (existing != null && existing.endTimeMs > 0L) { + endTimeMs = existing.endTimeMs + } + } + + val actions = extractNotificationActions(sbn) + val isNewTimer = timerNotificationKey != sbn.key + if (isNewTimer && endTimeMs > 0L) { + timerOriginalDurationMs = (endTimeMs - System.currentTimeMillis()).coerceAtLeast(1000L) + } + val event = + IslandEvent.Timer( + label = label, + endTimeMs = endTimeMs, + originalDurationMs = timerOriginalDurationMs, + appIcon = icon, + isPaused = isPaused, + actions = actions, + ) + timerNotificationKey = sbn.key + _timerEvent.value = event + onTimerEvent?.invoke(event) + + timerJob?.cancel() + if (endTimeMs > 0L && !isPaused) { + val remainingMs = endTimeMs - System.currentTimeMillis() + timerJob = + applicationScope.launch { + delay((remainingMs + 3_000L).coerceAtLeast(3_000L)) + _timerEvent.value = null + } + } + } + + private fun handleStopwatch( + sbn: StatusBarNotification, + extras: Bundle, + actionLabels: List = emptyList(), + ) { + val label = extras.getString("android.title") ?: "" + val icon = + try { + context.packageManager.getApplicationIcon(sbn.packageName) + } catch (_: Exception) { + null + } + + val isRunning = actionLabels.any { it.contains("pause") || it.contains("lap") } + + var startTimeMs = System.currentTimeMillis() + val chronoBase = extractChronometerBase(sbn) + if (chronoBase > 0L) { + val elapsed = SystemClock.elapsedRealtime() - chronoBase + if (elapsed > 0L) startTimeMs = System.currentTimeMillis() - elapsed + } + + val actions = extractNotificationActions(sbn) + val event = + IslandEvent.Stopwatch( + label = label, + startTimeMs = startTimeMs, + isRunning = isRunning, + appIcon = icon, + actions = actions, + ) + stopwatchNotificationKey = sbn.key + _stopwatchEvent.value = event + } + + private fun extractChronometerBase(sbn: StatusBarNotification): Long { + try { + val rv = sbn.notification.contentView ?: sbn.notification.bigContentView ?: return 0L + val pkgCtx = context.createPackageContext(sbn.packageName, Context.CONTEXT_RESTRICTED) + val container = FrameLayout(context) + val inflated = rv.apply(pkgCtx, container) ?: return 0L + return findChronometer(inflated)?.base ?: 0L + } catch (e: Exception) { + Log.w(TAG, "Failed to extract chronometer base from ${sbn.packageName}", e) + return 0L + } + } + + private fun findChronometer(view: View): Chronometer? { + if (view is Chronometer) return view + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + findChronometer(view.getChildAt(i))?.let { + return it + } + } + } + return null + } + + private fun extractNotificationActions( + sbn: StatusBarNotification + ): List { + return sbn.notification + ?.actions + ?.filter { a -> a.remoteInputs.isNullOrEmpty() && a.actionIntent != null } + ?.take(3) + ?.mapNotNull { a -> + a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } + } ?: emptyList() + } + + private fun handleAlarm(sbn: StatusBarNotification, extras: Bundle) { + val label = extras.getString("android.title") ?: context.getString(R.string.ax_dynamic_bar_alarm) + val isRinging = sbn.notification.category == Notification.CATEGORY_ALARM + val icon = + try { + context.packageManager.getApplicationIcon(sbn.packageName) + } catch (_: Exception) { + null + } + val event = + IslandEvent.Alarm( + label = label, + triggerTimeMs = sbn.notification.`when`, + isRinging = isRinging, + appIcon = icon, + ) + _alarmEvent.value = event + onAlarmEvent?.invoke(event) + } + + private fun resolveAppName(packageName: String): String = + try { + val ai = context.packageManager.getApplicationInfo(packageName, 0) + context.packageManager.getApplicationLabel(ai).toString() + } catch (_: Exception) { + "" + } + + private fun handleCallNotification(sbn: StatusBarNotification, extras: Bundle) { + val callerName = extras.getString("android.title") + val number = extras.getString("android.text") + val callerPhoto = + try { + sbn.notification?.getLargeIcon()?.loadDrawable(context) + } catch (_: Exception) { + null + } + val allActions = sbn.notification?.actions ?: emptyArray() + val actions = + allActions + .filter { a -> a.remoteInputs.isNullOrEmpty() && a.actionIntent != null } + .take(3) + .mapNotNull { a -> + a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } + } + + val androidCallType = extras.getInt( + Notification.EXTRA_CALL_TYPE, + Notification.CallStyle.CALL_TYPE_UNKNOWN, + ) + val callType = + if (androidCallType == Notification.CallStyle.CALL_TYPE_INCOMING) + "Phone:incoming" + else "Phone:active" + + val icon = + try { + context.packageManager.getApplicationIcon(sbn.packageName) + } catch (_: Exception) { + null + } + + val callWhen = sbn.notification?.`when` ?: 0L + val callStart = if (callWhen > 0L) callWhen else System.currentTimeMillis() + + val event = + IslandEvent.Notification( + sbn = sbn, + title = callerName, + text = number, + appIcon = icon, + appName = callType, + actions = actions, + senderIcon = callerPhoto, + senderName = callerName, + isConversation = false, + callStartTimeMs = callStart, + ) + applicationScope.launch { notificationFlow.emit(event) } + onNotificationPosted?.invoke(event) + } + + private fun isPromotable(sbn: StatusBarNotification, extras: Bundle): Boolean { + val notification = sbn.notification ?: return false + + if (notification.flags and Notification.FLAG_PROMOTED_ONGOING != 0) return true + + return notification.hasPromotableCharacteristics() + } + + private fun handlePromotedOngoing(sbn: StatusBarNotification, extras: Bundle, pkg: String) { + val shortCritical = + try { + sbn.notification?.shortCriticalText?.toString() ?: "" + } catch (_: Exception) { + extras.getString("android.shortCriticalText") ?: "" + } + val title = extras.getString("android.title") ?: "" + val text = extras.getString("android.text") ?: "" + val appName = resolveAppName(pkg) + val icon = loadNotificationIcon(sbn, pkg) + + val actions = extractNotificationActions(sbn) + + val progressRaw = extras.getInt("android.progress", -1) + val progressMax = extras.getInt("android.progressMax", 0) + val indeterminate = extras.getBoolean("android.progressIndeterminate", false) + val progress = + if (progressRaw >= 0 && progressMax > 0) + (progressRaw.toFloat() / progressMax.toFloat()).coerceIn(0f, 1f) + else -1f + + val event = + IslandEvent.PromotedOngoing( + shortText = shortCritical, + title = title, + text = text, + appName = appName, + appIcon = icon, + sbn = sbn, + actions = actions, + progress = progress, + isIndeterminate = indeterminate, + ) + + val current = _promotedOngoingEvents.value.toMutableList() + current.removeAll { it.sbn.key == sbn.key } + current.add(0, event) + _promotedOngoingEvents.value = current + } + + fun clearPromotedOngoing(key: String) { + _promotedOngoingEvents.value = _promotedOngoingEvents.value.filter { it.sbn.key != key } + } + + fun clearSportsEvent(key: String) { + _sportsEvents.value = _sportsEvents.value.filter { it.key != key } + } + + fun clearNowPlaying() { + _nowPlayingEvent.value = null + } + + private fun handleNowPlaying(sbn: StatusBarNotification, extras: Bundle) { + val title = extras.getCharSequence("android.title")?.toString() ?: return + val byMatch = Regex("""(.+?)\s+by\s+(.+)""", RegexOption.IGNORE_CASE).find(title) + val dashParts = if (byMatch == null) title.split(" - ", " – ", limit = 2) else null + val songTitle: String + val artist: String + when { + byMatch != null -> { + songTitle = byMatch.groupValues[1].trim() + artist = byMatch.groupValues[2].trim() + } + dashParts != null && dashParts.size == 2 -> { + songTitle = dashParts[0].trim() + artist = dashParts[1].trim() + } + else -> { + songTitle = title + artist = "" + } + } + + val allActions = sbn.notification?.actions ?: emptyArray() + val notifActions = allActions.mapNotNull { a -> + a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } + } + val appIcon = loadNotificationIcon(sbn, sbn.packageName) + + _nowPlayingEvent.value = IslandEvent.NowPlaying( + songTitle = songTitle, + artist = artist, + key = sbn.key, + sbn = sbn, + appIcon = appIcon, + actions = notifActions, + ) + } + + private fun handleSportsScore( + sbn: StatusBarNotification, + extras: Bundle, + forceCapture: Boolean = false, + ): Boolean { + val title = extras.getCharSequence("android.title")?.toString() ?: return false + val text = extras.getCharSequence("android.text")?.toString() ?: "" + + val allFields = listOf(title.trim(), text.trim()) + val scoreMatch = allFields.firstNotNullOfOrNull { SCORE_PATTERN.find(it) } + + var team1Name: String + var team2Name: String + var score1 = "" + var score2 = "" + + if (scoreMatch != null) { + team1Name = scoreMatch.groupValues[1].trim() + score1 = scoreMatch.groupValues[2] + score2 = scoreMatch.groupValues[3] + team2Name = scoreMatch.groupValues[4].trim() + .replace(Regex("""\s*[·•|].*"""), "") + } else { + val combined = "$title $text" + val vsMatch = VS_PATTERN.find(combined) + if (vsMatch != null) { + team1Name = vsMatch.groupValues[1].trim() + .replace(Regex("""^.*[·•|]\s*"""), "") + team2Name = vsMatch.groupValues[2].trim() + .replace(Regex("""\s*[·•|].*$"""), "") + } else if (forceCapture) { + val parts = text.split("·", "•", "|", " - ", " – ") + .map { it.trim() }.filter { it.isNotEmpty() } + if (parts.size >= 2) { + team1Name = parts[0] + team2Name = parts[1] + } else { + team1Name = text.ifEmpty { title } + team2Name = "" + } + } else { + return false + } + } + + val isOngoing = sbn.isOngoing + val status = when { + score1.isNotEmpty() && !isOngoing -> IslandEvent.GameStatus.FINAL + score1.isNotEmpty() && isOngoing -> IslandEvent.GameStatus.LIVE + isOngoing -> IslandEvent.GameStatus.PRE_GAME + else -> IslandEvent.GameStatus.FINAL + } + + var statusDetail = "" + if (score1.isNotEmpty()) { + val shortCritical = try { + sbn.notification?.shortCriticalText?.toString() ?: "" + } catch (_: Exception) { + extras.getString("android.shortCriticalText") ?: "" + } + statusDetail = shortCritical + .replace(score1, "").replace(score2, "") + .replace("-", "").replace("–", "").replace(":", "") + .trim() + } + + val allText = "$title · $text" + val league = allText.split("·", "•", "|") + .map { it.trim() } + .firstOrNull { part -> + part.length in 2..30 && + !part.any { it.isDigit() } && + part != team1Name && part != team2Name + } ?: "" + + val commentary = extras.getCharSequence("android.bigText")?.toString() + ?: extras.getString("android.subText") ?: "" + + val appIcon = loadNotificationIcon(sbn, sbn.packageName) + + val event = IslandEvent.Sports( + team1Name = team1Name, + team2Name = team2Name, + score1 = score1, + score2 = score2, + status = status, + statusDetail = statusDetail, + league = league, + commentary = commentary, + key = sbn.key, + sbn = sbn, + appIcon = appIcon, + ) + + val current = _sportsEvents.value.toMutableList() + current.removeAll { it.key == sbn.key } + current.add(0, event) + _sportsEvents.value = current + return true + } + + private fun extractNotificationImage(extras: Bundle, sbn: StatusBarNotification): Drawable? { + try { + extras.getParcelable(Notification.EXTRA_PICTURE_ICON, Icon::class.java) + ?.loadDrawable(context)?.let { return it } + } catch (_: Exception) {} + + try { + extras.getParcelable(Notification.EXTRA_PICTURE, Bitmap::class.java) + ?.let { return BitmapDrawable(context.resources, it) } + } catch (_: Exception) {} + + if (!sbn.notification.isStyle(Notification.MessagingStyle::class.java)) { + try { + sbn.notification?.getLargeIcon()?.loadDrawable(context)?.let { return it } + } catch (_: Exception) {} + } + + return null + } + + private fun loadNotificationIcon(sbn: StatusBarNotification, pkg: String): Drawable? { + return try { + sbn.notification?.smallIcon?.loadDrawable(context) + } catch (_: Exception) { + null + } + ?: try { + context.packageManager.getApplicationIcon(pkg) + } catch (_: Exception) { + null + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt new file mode 100644 index 000000000000..6f7d45da47ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt @@ -0,0 +1,164 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.app.AppOpsManager +import android.content.Context +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.MicCamApp +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.privacy.PrivacyItem +import com.android.systemui.privacy.PrivacyItemController +import com.android.systemui.privacy.PrivacyType +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class PrivacyIslandManager +@Inject +constructor( + @Application private val context: Context, + private val privacyItemController: PrivacyItemController, +) { + private val _micCamEvent = MutableStateFlow(null) + val micCamEvent: StateFlow = _micCamEvent.asStateFlow() + + var onCamStarted: ((IslandEvent.MicCamActive) -> Unit)? = null + + private var listening = false + @Volatile private var wasCamActive = false + + @Volatile private var privacyControllerReportedCam = false + + private val appOpsManager: AppOpsManager by lazy { + context.getSystemService(AppOpsManager::class.java) + } + + private val callback = + object : PrivacyItemController.Callback { + override fun onPrivacyItemsChanged(privacyItems: List) { + val isMic = privacyItems.any { it.privacyType == PrivacyType.TYPE_MICROPHONE } + val isCam = privacyItems.any { it.privacyType == PrivacyType.TYPE_CAMERA } + privacyControllerReportedCam = isCam + + if (isMic || isCam) { + val apps: List = + privacyItems + .filter { + it.privacyType == PrivacyType.TYPE_MICROPHONE || + it.privacyType == PrivacyType.TYPE_CAMERA + } + .mapNotNull { item -> + val pkg = item.application.packageName ?: return@mapNotNull null + val name = + try { + context.packageManager + .getApplicationLabel( + context.packageManager.getApplicationInfo(pkg, 0) + ) + .toString() + } catch (_: Exception) { + pkg + } + val icon = + try { + context.packageManager.getApplicationIcon(pkg) + } catch (_: Exception) { + null + } + MicCamApp(packageName = pkg, appName = name, appIcon = icon) + } + .distinctBy { it.packageName } + + emitEvent(isMic = isMic, isCam = isCam, apps = apps) + } else if (!directCamActive) { + wasCamActive = false + _micCamEvent.value = null + } + } + + override fun onFlagAllChanged(flag: Boolean) {} + } + + @Volatile private var directCamActive = false + + private val cameraOpCallback = + AppOpsManager.OnOpActiveChangedListener { op, _, packageName, active -> + if (op != AppOpsManager.OPSTR_CAMERA) return@OnOpActiveChangedListener + directCamActive = active + + if (privacyControllerReportedCam) return@OnOpActiveChangedListener + + if (active) { + val appName = + try { + context.packageManager + .getApplicationLabel( + context.packageManager.getApplicationInfo(packageName, 0) + ) + .toString() + } catch (_: Exception) { + packageName + } + val icon = + try { + context.packageManager.getApplicationIcon(packageName) + } catch (_: Exception) { + null + } + val current = _micCamEvent.value + emitEvent( + isMic = current?.isMic ?: false, + isCam = true, + apps = + listOfNotNull(MicCamApp(packageName, appName, icon)) + + (current?.apps?.filter { it.packageName != packageName } ?: emptyList()), + ) + } else { + val current = _micCamEvent.value ?: return@OnOpActiveChangedListener + if (current.isMic) { + _micCamEvent.value = current.copy(isCam = false) + } else { + wasCamActive = false + _micCamEvent.value = null + } + } + } + + private fun emitEvent(isMic: Boolean, isCam: Boolean, apps: List) { + val appName = apps.firstOrNull()?.appName ?: "" + val event = + IslandEvent.MicCamActive(isMic = isMic, isCam = isCam, appName = appName, apps = apps) + _micCamEvent.value = event + + if (isCam && !wasCamActive) onCamStarted?.invoke(event) + wasCamActive = isCam + } + + fun startListening() { + if (listening) return + listening = true + privacyItemController.addCallback(callback) + try { + appOpsManager.startWatchingActive( + arrayOf(AppOpsManager.OPSTR_CAMERA), + context.mainExecutor, + cameraOpCallback, + ) + } catch (_: Exception) {} + } + + fun stopListening() { + if (!listening) return + listening = false + privacyItemController.removeCallback(callback) + try { + appOpsManager.stopWatchingActive(cameraOpCallback) + } catch (_: Exception) {} + _micCamEvent.value = null + directCamActive = false + privacyControllerReportedCam = false + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt new file mode 100644 index 000000000000..99a26421f2cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt @@ -0,0 +1,93 @@ +package com.android.systemui.axdynamicbar.data.source + +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds +import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor +import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@SysUISingleton +class ScreenRecordIslandManager +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val screenRecordChipInteractor: ScreenRecordChipInteractor, +) { + private val _screenRecordEvent = MutableStateFlow(null) + val screenRecordEvent: StateFlow = + _screenRecordEvent.asStateFlow() + + @Volatile + private var notificationStartTimeMs: Long = 0L + + private var listening = false + private var listenerJob: Job? = null + + fun startListening() { + if (listening) return + listening = true + listenerJob?.cancel() + listenerJob = + applicationScope.launch(backgroundDispatcher) { + screenRecordChipInteractor.screenRecordState.collect { state -> + _screenRecordEvent.value = + when (state) { + is ScreenRecordChipModel.Recording -> { + val notifMs = notificationStartTimeMs + val existing = _screenRecordEvent.value + val startMs = when { + notifMs > 0L -> notifMs + existing != null && !existing.isCountdown -> existing.startTimeMs + else -> System.currentTimeMillis() + } + if (existing != null && !existing.isCountdown && existing.startTimeMs == startMs) { + existing + } else { + IslandEvent.ScreenRecording(startTimeMs = startMs) + } + } + is ScreenRecordChipModel.Starting -> + IslandEvent.ScreenRecording( + countdownSeconds = + state.millisUntilStarted.toCountdownSeconds(), + ) + is ScreenRecordChipModel.DoingNothing -> null + } + } + } + } + + fun stopListening() { + if (!listening) return + listening = false + listenerJob?.cancel() + listenerJob = null + _screenRecordEvent.value = null + notificationStartTimeMs = 0L + } + + fun stopRecording() { + applicationScope.launch { screenRecordChipInteractor.stopRecording() } + } + + fun updateNotificationStartTime(timeMs: Long) { + notificationStartTimeMs = timeMs + _screenRecordEvent.update { existing -> + if (existing != null && timeMs > 0L && existing.startTimeMs != timeMs) { + existing.copy(startTimeMs = timeMs) + } else existing + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SmartspaceIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SmartspaceIslandManager.kt new file mode 100644 index 000000000000..df253d75bb09 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SmartspaceIslandManager.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.data.source + +import android.app.PendingIntent +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.android.axion.quicklook.SportsData +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.quicklook.QuickLookClient +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class SmartspaceIslandManager +@Inject +constructor( + private val quickLookClient: QuickLookClient, +) { + private val _sportsEvents = MutableStateFlow>(emptyList()) + val sportsEvents: StateFlow> = _sportsEvents.asStateFlow() + + private val _nowPlayingEvent = MutableStateFlow(null) + val nowPlayingEvent: StateFlow = _nowPlayingEvent.asStateFlow() + + private var listening = false + + private val callback = object : QuickLookClient.Callback { + override fun onSportsUpdate(sports: List) { + _sportsEvents.value = sports.mapIndexed { index, data -> + val status = parseGameStatus(data.status, data.statusDetail) + IslandEvent.Sports( + team1Name = data.team1Name, + team2Name = data.team2Name, + score1 = data.score1, + score2 = data.score2, + team1Icon = bytesToDrawable(data.team1IconBytes), + team2Icon = bytesToDrawable(data.team2IconBytes), + status = status, + statusDetail = data.statusDetail, + league = data.league, + key = "ql_sports_$index", + ) + } + } + + override fun onNowPlayingUpdate(text: String, tapAction: PendingIntent?) { + if (text.isBlank()) { + _nowPlayingEvent.value = null + return + } + val byMatch = Regex("""(.+?)\s+by\s+(.+)""", RegexOption.IGNORE_CASE).find(text) + val dashParts = if (byMatch == null) text.split(" - ", " – ", limit = 2) else null + val songTitle: String + val artist: String + when { + byMatch != null -> { + songTitle = byMatch.groupValues[1].trim() + artist = byMatch.groupValues[2].trim() + } + dashParts != null && dashParts.size == 2 -> { + songTitle = dashParts[0].trim() + artist = dashParts[1].trim() + } + else -> { + songTitle = text + artist = "" + } + } + _nowPlayingEvent.value = IslandEvent.NowPlaying( + songTitle = songTitle, + artist = artist, + key = "ql_now_playing", + ) + } + } + + fun startListening() { + if (listening) return + listening = true + quickLookClient.addCallback(callback) + } + + fun stopListening() { + if (!listening) return + listening = false + quickLookClient.removeCallback(callback) + _sportsEvents.value = emptyList() + _nowPlayingEvent.value = null + } + + fun clearSportsEvent(key: String) { + _sportsEvents.value = _sportsEvents.value.filter { it.key != key } + } + + private fun parseGameStatus(status: String, detail: String): IslandEvent.GameStatus { + val combined = "$status $detail".lowercase() + return when { + combined.contains("final") -> IslandEvent.GameStatus.FINAL + combined.contains("half") -> IslandEvent.GameStatus.HALFTIME + combined.contains("live") || combined.contains("q") || + combined.contains("inning") || combined.contains("set") || + combined.contains("period") -> IslandEvent.GameStatus.LIVE + combined.contains("pre") || combined.contains("upcoming") || + combined.contains("tip") || combined.contains("kick") -> IslandEvent.GameStatus.PRE_GAME + else -> IslandEvent.GameStatus.LIVE + } + } + + private fun bytesToDrawable(bytes: ByteArray?): Drawable? { + if (bytes == null || bytes.isEmpty()) return null + return try { + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return null + BitmapDrawable(null, bitmap) + } catch (_: Exception) { + null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SystemIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SystemIslandManager.kt new file mode 100644 index 000000000000..290659fc0258 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/SystemIslandManager.kt @@ -0,0 +1,513 @@ +package com.android.systemui.axdynamicbar.data.source + +import android.content.BroadcastReceiver +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.media.AudioManager +import android.net.Uri +import android.os.Handler +import android.util.Log +import androidx.core.content.FileProvider +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.res.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor +import com.android.systemui.statusbar.policy.BatteryController +import java.io.File +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject + +@SysUISingleton +class SystemIslandManager +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Main private val mainHandler: Handler, + private val batteryInteractor: BatteryInteractor, + private val batteryController: BatteryController, +) { + companion object { + private const val TAG = "SystemIslandManager" + private const val MAX_CLIPBOARD_HISTORY = 10 + private const val PREFS_NAME = "ax_dynamic_bar_prefs" + private const val KEY_CLIPBOARD_STASH = "clipboard_stash" + private const val CLIPBOARD_CACHE_DIR = "clipboard_cache" + private const val FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider" + } + + private val _chargingEvent = MutableStateFlow(null) + val chargingEvent: StateFlow = _chargingEvent.asStateFlow() + + private val _ringerEvent = MutableStateFlow(null) + val ringerEvent: StateFlow = _ringerEvent.asStateFlow() + + private val _clipboardEvent = MutableStateFlow(null) + val clipboardEvent: StateFlow = _clipboardEvent.asStateFlow() + + var onChargingStarted: ((IslandEvent.Charging) -> Unit)? = null + + var onRingerChanged: ((IslandEvent.RingerMode) -> Unit)? = null + + var onClipboardCopied: ((IslandEvent.Clipboard) -> Unit)? = null + + private val clipboardManager: ClipboardManager by lazy { + context.getSystemService(ClipboardManager::class.java) + } + + private var wasCharging = false + @Volatile var chargingDismissed = false + private var lastRingerMode = -1 + private var batteryJob: Job? = null + + private var listening = false + @Volatile private var suppressNextClipEvent = false + + private val clipboardHistory = mutableListOf() + + private val prefs by lazy { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } + + private val clipboardCacheDir: File by lazy { + File(context.cacheDir, CLIPBOARD_CACHE_DIR).apply { mkdirs() } + } + + private var persistJob: Job? = null + + private val ringerReceiver = + object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val mode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1) + if (mode < 0) return + if (lastRingerMode != -1 && lastRingerMode != mode) { + val label = + when (mode) { + AudioManager.RINGER_MODE_SILENT -> context.getString(R.string.ax_dynamic_bar_silent) + AudioManager.RINGER_MODE_VIBRATE -> context.getString(R.string.ax_dynamic_bar_vibrate) + AudioManager.RINGER_MODE_NORMAL -> context.getString(R.string.ax_dynamic_bar_ring) + else -> return + } + val event = IslandEvent.RingerMode(mode = mode, label = label) + _ringerEvent.value = event + onRingerChanged?.invoke(event) + } + lastRingerMode = mode + } + } + + private val clipboardListener = + ClipboardManager.OnPrimaryClipChangedListener { + if (suppressNextClipEvent) { + suppressNextClipEvent = false + return@OnPrimaryClipChangedListener + } + val clip = + try { + clipboardManager.primaryClip + } catch (_: Exception) { + null + } ?: return@OnPrimaryClipChangedListener + val item = clip.getItemAt(0) ?: return@OnPrimaryClipChangedListener + val desc = clip.description + + val rawText = + try { + item.coerceToText(context)?.toString() ?: "" + } catch (_: Exception) { + "" + } + val isUrl = + rawText.startsWith("http://") || + rawText.startsWith("https://") || + rawText.startsWith("www.") + val isImage = desc.hasMimeType("image/*") + val preview = rawText.trim() + val sourceUri = + if (isImage) + try { + item.uri + } catch (_: Exception) { + null + } + else null + + val label = desc.label?.toString() ?: "" + if (preview.isNotEmpty() || isImage || label.isNotEmpty()) { + val itemId = System.currentTimeMillis() + if (isImage && sourceUri != null) { + applicationScope.launch(backgroundDispatcher) { + val cachedUri = cacheClipboardImage(sourceUri, itemId) + val (event, items) = buildClipboardEvent( + itemId, preview, label, isUrl, true, cachedUri) + _clipboardEvent.value = event + mainHandler.post { onClipboardCopied?.invoke(event) } + } + } else { + val (event, _) = buildClipboardEvent( + itemId, preview, label, isUrl, false, null) + _clipboardEvent.value = event + onClipboardCopied?.invoke(event) + } + } + } + + private fun cacheClipboardImage(sourceUri: Uri, itemId: Long): Uri? { + return try { + val source = ImageDecoder.createSource(context.contentResolver, sourceUri) + val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -> + decoder.setTargetSampleSize(2) + } + val file = File(clipboardCacheDir, "clip_$itemId.webp") + file.outputStream().use { out -> + bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 85, out) + } + bitmap.recycle() + FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file) + } catch (e: Exception) { + Log.w(TAG, "Failed to cache clipboard image", e) + null + } + } + + private fun buildClipboardEvent( + itemId: Long, + preview: String, + label: String, + isUrl: Boolean, + isImage: Boolean, + imageUri: Uri?, + ): Pair> { + val clipItem = + IslandEvent.ClipboardItem( + id = itemId, + preview = preview, + label = label, + isUrl = isUrl, + isImage = isImage, + imageUri = imageUri, + timestamp = itemId, + ) + val items: List + synchronized(clipboardHistory) { + clipboardHistory.removeAll { it.preview == preview && !isImage } + clipboardHistory.add(0, clipItem) + while (clipboardHistory.size > MAX_CLIPBOARD_HISTORY) { + val removed = clipboardHistory.removeLast() + cleanupCachedImage(removed.id) + } + items = clipboardHistory.toList() + } + persistClipboardHistory() + + val event = + IslandEvent.Clipboard( + label = label, + preview = preview, + isUrl = isUrl, + isImage = isImage, + imageUri = imageUri, + items = items, + ) + return event to items + } + + private fun cleanupCachedImage(itemId: Long) { + try { + File(clipboardCacheDir, "clip_$itemId.webp").delete() + File(clipboardCacheDir, "clip_$itemId.png").delete() + } catch (_: Exception) {} + } + + private var chargingListening = false + private var ringerListening = false + private var clipboardListening = false + + fun startCharging() { + if (chargingListening) return + chargingListening = true + wasCharging = batteryController.isPluggedIn + batteryJob?.cancel() + batteryJob = + applicationScope.launch(backgroundDispatcher) { + combine( + batteryInteractor.isCharging, + batteryInteractor.level, + batteryInteractor.powerSave, + batteryInteractor.batteryTimeRemainingEstimate, + ) { isCharging: Boolean, level: Int?, isPowerSave: Boolean, timeEst: String? -> + ChargingSnapshot(isCharging, level, isPowerSave, timeEst) + } + .distinctUntilChanged() + .collect { snap -> + val wasChargingBefore = wasCharging + wasCharging = snap.isCharging + if (snap.isCharging && !wasChargingBefore && snap.level != null) { + chargingDismissed = false + val event = + IslandEvent.Charging( + level = snap.level, + isWireless = batteryController.isWirelessCharging, + isPowerSave = snap.isPowerSave, + timeRemaining = snap.timeEst, + ) + _chargingEvent.value = event + onChargingStarted?.invoke(event) + } else if (snap.isCharging && wasChargingBefore && !chargingDismissed) { + _chargingEvent.value = + _chargingEvent.value?.copy( + level = snap.level ?: _chargingEvent.value?.level ?: 0, + isPowerSave = snap.isPowerSave, + timeRemaining = snap.timeEst, + ) + } else if (!snap.isCharging && wasChargingBefore) { + chargingDismissed = false + _chargingEvent.value = null + } + } + } + } + + fun stopCharging() { + if (!chargingListening) return + chargingListening = false + batteryJob?.cancel() + batteryJob = null + _chargingEvent.value = null + } + + fun startRinger() { + if (ringerListening) return + ringerListening = true + lastRingerMode = + (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager).ringerMode + context.registerReceiver( + ringerReceiver, + IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION), + null, + mainHandler, + ) + } + + fun stopRinger() { + if (!ringerListening) return + ringerListening = false + try { context.unregisterReceiver(ringerReceiver) } catch (_: Exception) {} + _ringerEvent.value = null + } + + fun startClipboard() { + if (clipboardListening) return + clipboardListening = true + loadClipboardHistory() + clipboardManager.addPrimaryClipChangedListener(clipboardListener) + } + + fun stopClipboard() { + if (!clipboardListening) return + clipboardListening = false + clipboardManager.removePrimaryClipChangedListener(clipboardListener) + _clipboardEvent.value = null + } + + fun startListening() { + if (listening) return + listening = true + startCharging() + startRinger() + startClipboard() + } + + fun stopListening() { + if (!listening) return + listening = false + stopCharging() + stopRinger() + stopClipboard() + } + + fun clearRinger() { + _ringerEvent.value = null + } + + fun setRingerMode(mode: Int) { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + am.ringerMode = mode + } + + fun getRingerMode(): Int { + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + return am.ringerMode + } + + fun emitRingerEvent(mode: Int) { + val label = + when (mode) { + AudioManager.RINGER_MODE_SILENT -> context.getString(R.string.ax_dynamic_bar_silent) + AudioManager.RINGER_MODE_VIBRATE -> context.getString(R.string.ax_dynamic_bar_vibrate) + else -> context.getString(R.string.ax_dynamic_bar_ring) + } + val event = IslandEvent.RingerMode(mode = mode, label = label) + _ringerEvent.value = event + onRingerChanged?.invoke(event) + } + + fun clearClipboard() { + _clipboardEvent.value = null + synchronized(clipboardHistory) { + clipboardHistory.forEach { cleanupCachedImage(it.id) } + clipboardHistory.clear() + } + persistClipboardHistory() + } + + fun clearCharging() { + chargingDismissed = true + _chargingEvent.value = null + } + + fun removeClipboardItem(id: Long) { + cleanupCachedImage(id) + val event: IslandEvent.Clipboard? + synchronized(clipboardHistory) { + clipboardHistory.removeAll { it.id == id } + event = + if (clipboardHistory.isEmpty()) null + else { + val latest = clipboardHistory.first() + _clipboardEvent.value?.copy( + label = latest.label, + preview = latest.preview, + isUrl = latest.isUrl, + isImage = latest.isImage, + imageUri = latest.imageUri, + items = clipboardHistory.toList(), + ) + } + } + persistClipboardHistory() + _clipboardEvent.value = event + } + + fun copyToClipboard(text: String) { + if (text.isEmpty()) return + suppressNextClipEvent = true + clipboardManager.setPrimaryClip(ClipData.newPlainText("Copied", text)) + } + + fun copyUriToClipboard(uri: Uri, mimeType: String = "image/*") { + suppressNextClipEvent = true + try { + clipboardManager.setPrimaryClip( + ClipData("Copied", arrayOf(mimeType), ClipData.Item(uri))) + } catch (e: SecurityException) { + Log.w(TAG, "Failed to copy URI to clipboard, trying plain text", e) + clipboardManager.setPrimaryClip(ClipData.newPlainText("Copied", uri.toString())) + } + } + + fun openUrl(url: String) { + if (url.isEmpty()) return + try { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + _clipboardEvent.value = null + } catch (e: Exception) { + Log.w(TAG, "Failed to open URL: $url", e) + } + } + + private fun persistClipboardHistory() { + persistJob?.cancel() + persistJob = applicationScope.launch(backgroundDispatcher) { + try { + val arr = JSONArray() + synchronized(clipboardHistory) { + clipboardHistory.forEach { item -> + arr.put( + JSONObject().apply { + put("id", item.id) + put("preview", item.preview) + put("label", item.label) + put("isUrl", item.isUrl) + put("isImage", item.isImage) + put("imageUri", item.imageUri?.toString() ?: "") + put("ts", item.timestamp) + } + ) + } + } + prefs.edit().putString(KEY_CLIPBOARD_STASH, arr.toString()).apply() + } catch (e: Exception) { + Log.w(TAG, "Failed to persist clipboard history", e) + } + } + } + + private fun loadClipboardHistory() { + try { + val json = prefs.getString(KEY_CLIPBOARD_STASH, null) ?: return + val arr = JSONArray(json) + synchronized(clipboardHistory) { + clipboardHistory.clear() + for (i in 0 until arr.length().coerceAtMost(MAX_CLIPBOARD_HISTORY)) { + val obj = arr.getJSONObject(i) + val id = obj.optLong("id", 0L) + val isImage = obj.optBoolean("isImage", false) + val imageUri = + if (isImage) { + val cachedWebp = File(clipboardCacheDir, "clip_$id.webp") + val cachedPng = File(clipboardCacheDir, "clip_$id.png") + when { + cachedWebp.exists() -> + FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, cachedWebp) + cachedPng.exists() -> + FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, cachedPng) + else -> null + } + } else null + clipboardHistory.add( + IslandEvent.ClipboardItem( + id = id, + preview = obj.optString("preview", ""), + label = obj.optString("label", ""), + isUrl = obj.optBoolean("isUrl", false), + isImage = isImage, + imageUri = imageUri, + timestamp = obj.optLong("ts", 0L), + ) + ) + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to load clipboard history", e) + } + } + + private data class ChargingSnapshot( + val isCharging: Boolean, + val level: Int?, + val isPowerSave: Boolean, + val timeEst: String?, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt new file mode 100644 index 000000000000..747f69f9d152 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt @@ -0,0 +1,122 @@ +package com.android.systemui.axdynamicbar.data.source + +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flashlight.domain.interactor.FlashlightInteractor +import com.android.systemui.flashlight.flags.FlashlightStrength +import com.android.systemui.flashlight.shared.model.FlashlightModel +import com.android.systemui.statusbar.policy.FlashlightController +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@SysUISingleton +class TorchIslandManager +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val flashlightController: FlashlightController, + private val flashlightInteractor: FlashlightInteractor, +) { + private val _torchEvent = MutableStateFlow(null) + val torchEvent: StateFlow = _torchEvent.asStateFlow() + + private var listening = false + private var levelJob: Job? = null + + private val listener = + object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) { + if (enabled) { + _torchEvent.value = IslandEvent.Torch() + startLevelObserver() + } else { + stopLevelObserver() + _torchEvent.value = null + } + } + + override fun onFlashlightError() { + stopLevelObserver() + _torchEvent.value = null + } + + override fun onFlashlightAvailabilityChanged(available: Boolean) { + if (!available) { + stopLevelObserver() + _torchEvent.value = null + } + } + } + + private fun startLevelObserver() { + if (!FlashlightStrength.isEnabled) return + levelJob?.cancel() + levelJob = + scope.launch { + flashlightInteractor.state.collect { model -> + when (model) { + is FlashlightModel.Available.Level -> { + if (model.enabled) { + _torchEvent.value = + IslandEvent.Torch(level = model.level, maxLevel = model.max) + } + } + is FlashlightModel.Available.Binary -> { + _torchEvent.value = + if (model.enabled) IslandEvent.Torch() else null + } + is FlashlightModel.Unavailable -> { + _torchEvent.value = null + } + } + } + } + } + + private fun stopLevelObserver() { + levelJob?.cancel() + levelJob = null + } + + fun startListening() { + if (listening) return + listening = true + flashlightController.addCallback(listener) + if (flashlightController.isEnabled) { + _torchEvent.value = IslandEvent.Torch() + startLevelObserver() + } + } + + fun stopListening() { + if (!listening) return + listening = false + flashlightController.removeCallback(listener) + stopLevelObserver() + _torchEvent.value = null + } + + fun clear() { + stopLevelObserver() + _torchEvent.value = null + } + + fun toggleTorch() { + flashlightController.setFlashlight(!flashlightController.isEnabled) + } + + fun setLevel(level: Int) { + if (FlashlightStrength.isEnabled) flashlightInteractor.setLevel(level) + } + + fun setLevelTemporary(level: Int) { + if (FlashlightStrength.isEnabled) flashlightInteractor.setTemporaryLevel(level) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt new file mode 100644 index 000000000000..647d09fc5f2d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.domain + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner +import javax.inject.Inject + +@SysUISingleton +class AxDynamicBarChipsRefiner @Inject constructor( + private val settings: AxDynamicBarSettings, +) : OngoingActivityChipsRefiner { + + override fun transform(input: MultipleOngoingActivityChipsModel): MultipleOngoingActivityChipsModel { + if (!settings.isEnabled.value) return input + + return input.copy( + active = input.active.map { chip -> chip.copy(isHidden = true) }, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt new file mode 100644 index 000000000000..cdcf6eb4ea63 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt @@ -0,0 +1,577 @@ +package com.android.systemui.axdynamicbar.domain + +import android.app.Notification +import android.media.AudioManager +import android.provider.Settings.Global +import com.android.systemui.axdynamicbar.data.IslandEventRepository +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.IslandState +import com.android.systemui.axdynamicbar.model.IslandUiState +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.ZenModeController +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import android.net.Uri + +@SysUISingleton +class AxDynamicBarInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val repository: IslandEventRepository, + val settings: AxDynamicBarSettings, + private val statusBarStateController: StatusBarStateController, + private val keyguardStateController: KeyguardStateController, + val sliderHapticsViewModelFactory: SliderHapticsViewModel.Factory, + private val activityStarter: ActivityStarter, + private val indicationController: KeyguardIndicationController, + private val shadeInteractor: ShadeInteractor, + private val shadeRepository: ShadeRepository, + private val zenModeController: ZenModeController, + private val audioManager: AudioManager, +) : IslandActions { + private val _uiState = MutableStateFlow(IslandUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val autoDismissJobs = ConcurrentHashMap() + @Volatile private var notifAlertJob: Job? = null + + private val dismissedEventIds: MutableSet = ConcurrentHashMap.newKeySet() + + var onCollapseRequested: (() -> Unit)? = null + + override var onFocusableRequested: ((Boolean) -> Unit)? = null + + private var isInitialized = false + + @Volatile private var panelBlocking = false + private val _isPanelExpanded = MutableStateFlow(false) + + val isPanelExpanded: StateFlow = _isPanelExpanded.asStateFlow() + + val qsExpansion: StateFlow = shadeInteractor.qsExpansion + + val legacyShadeExpansion: StateFlow = shadeRepository.legacyShadeExpansion + private val _isOnKeyguard = MutableStateFlow(false) + val isOnKeyguard: StateFlow = _isOnKeyguard.asStateFlow() + private val _isKeyguardFadingAway = MutableStateFlow(false) + val isKeyguardFadingAway: StateFlow = _isKeyguardFadingAway.asStateFlow() + private val _isBouncerShowing = MutableStateFlow(false) + val isBouncerShowing: StateFlow = _isBouncerShowing.asStateFlow() + private val _isDozing = MutableStateFlow(statusBarStateController.isDozing) + val isDozing: StateFlow = _isDozing.asStateFlow() + private val _dozeAmount = MutableStateFlow(0f) + + val dozeAmount: StateFlow = _dozeAmount.asStateFlow() + @Volatile private var isDreaming = false + + private val statusBlocking: Boolean + get() = _isDozing.value || isDreaming + + companion object { + private const val TAG = "AxDynamicBarInteractor" + private const val NOTIF_ALERT_DURATION_MS = 4500L + + private fun IslandEvent.Notification.isActiveCall(): Boolean { + val extras = sbn.notification?.extras ?: return false + return extras.containsKey(Notification.EXTRA_ANSWER_INTENT) || + extras.containsKey(Notification.EXTRA_DECLINE_INTENT) || + extras.containsKey(Notification.EXTRA_HANG_UP_INTENT) + } + } + + fun init() { + if (isInitialized) return + isInitialized = true + + settings.init() + + repository.system.onChargingStarted = { scheduleAutoDismiss(it) } + repository.system.onRingerChanged = { scheduleAutoDismiss(it) } + repository.system.onClipboardCopied = { scheduleAutoDismiss(it) } + + repository.privacy.onCamStarted = { scheduleAutoDismiss(it, 10_000L) } + + repository.notification.activeMediaPackageProvider = { repository.media.activeMediaPackage } + repository.notification.onAlarmEvent = { + scheduleAutoDismiss(it, if (it.isRinging) 30_000L else 5_000L) + } + + repository.media.onMediaSessionLost = { repository.media.clear() } + + repository.biometric.onBiometricUnlock = { scheduleAutoDismiss(it) } + + applicationScope.launch { + repository.notification.audioRecordingEvent.collect { event -> + if (event?.state == RecordingState.SAVED) { + scheduleAutoDismiss(event, 5_000L) + } + } + } + + applicationScope.launch { + repository.notification.notificationFlow.collect { notification -> + repository.notification.coalesceNotification(notification) + showNotificationAlert(notification) + } + } + + applicationScope.launch { + repository.notification.notificationRemovedFlow.collect { key -> + val alert = _uiState.value.notificationAlert ?: return@collect + if (alert.sbn.key == key) dismissNotificationAlert() + } + } + + _isOnKeyguard.value = statusBarStateController.state == StatusBarState.KEYGUARD + _isKeyguardFadingAway.value = keyguardStateController.isKeyguardFadingAway + _isBouncerShowing.value = keyguardStateController.isPrimaryBouncerShowing + + keyguardStateController.addCallback( + object : KeyguardStateController.Callback { + override fun onKeyguardFadingAwayChanged() { + _isKeyguardFadingAway.value = keyguardStateController.isKeyguardFadingAway + } + + override fun onPrimaryBouncerShowingChanged() { + _isBouncerShowing.value = keyguardStateController.isPrimaryBouncerShowing + } + } + ) + + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onExpandedChanged(isExpanded: Boolean) { + onPanelExpandedChanged(isExpanded) + } + + override fun onStateChanged(newState: Int) { + _isOnKeyguard.value = newState == StatusBarState.KEYGUARD + updateChipVisibility() + } + + override fun onDozingChanged(dozing: Boolean) { + _isDozing.value = dozing + updateChipVisibility() + } + + override fun onDozeAmountChanged(linear: Float, eased: Float) { + _dozeAmount.value = linear + } + + override fun onDreamingChanged(dreaming: Boolean) { + isDreaming = dreaming + updateChipVisibility() + } + } + ) + + indicationController.addIndicationListener { type, text -> + val indicationType = mapIndicationType(type) ?: return@addIndicationListener + if (text != null && text.isNotEmpty()) { + val event = IslandEvent.KeyguardIndication( + text = text.toString(), + indicationType = indicationType, + ) + repository.updateIndicationEvent(event) + scheduleAutoDismiss(event) + } else { + repository.clearIndicationEvent(indicationType) + } + } + + applicationScope.launch { + settings.isEnabled.collect { enabled -> + if (enabled) repository.startListening() + else { + repository.stopListening() + autoDismissJobs.values.forEach { it.cancel() } + autoDismissJobs.clear() + dismissedEventIds.clear() + repository.clearAllIndicationEvents() + _uiState.value = IslandUiState() + } + } + } + + applicationScope.launch { + settings.disabledEventTypes.collect { + repository.refreshListeners() + } + } + + applicationScope.launch { + combine( + repository.events, + settings.disabledEventTypes, + _isOnKeyguard, + ) { raw, _, kg -> + raw.filter { settings.isEventEnabled(it) } to kg + }.collect { (rawEvents, onKeyguard) -> + if (!settings.isEnabled.value) return@collect + dismissedEventIds.removeAll { id -> rawEvents.none { it.id == id } } + val events = rawEvents.filter { e -> + e.id !in dismissedEventIds && + + !(onKeyguard && e is IslandEvent.Notification) && + + !(onKeyguard && e is IslandEvent.Charging) && + + !(onKeyguard && e is IslandEvent.AppSwitch) && + + !(!onKeyguard && e is IslandEvent.KeyguardIndication) + } + + val current = _uiState.value + + val hasNewEvents = events.any { e -> current.events.none { it.id == e.id } } + + val userInitiatedRefresh = + events.any { e -> + e is IslandEvent.Torch && + current.events.firstOrNull { it.id == e.id }?.let { it != e } == true + } + + val hasSignificantChange = + !hasNewEvents && + events.any { e -> + val old = + current.events.firstOrNull { it.id == e.id } ?: return@any false + when { + e is IslandEvent.Media && old is IslandEvent.Media -> + e.track != old.track || e.artist != old.artist + else -> false + } + } + + val newState = + when { + events.isEmpty() -> IslandState.HIDDEN + panelBlocking || statusBlocking -> IslandState.HIDDEN + else -> IslandState.CHIP + } + + val prevPinnedId = + current.events.getOrNull(current.pinnedEventIndex)?.id + + val pinnedIndex = + when { + hasNewEvents -> { + val currentIds = current.events.map { it.id }.toSet() + events.indexOfFirst { it.id !in currentIds }.coerceAtLeast(0) + } + hasSignificantChange -> { + val idx = + events.indexOfFirst { e -> + val old = + current.events.firstOrNull { it.id == e.id } + ?: return@indexOfFirst false + when { + e is IslandEvent.Media && old is IslandEvent.Media -> + e.track != old.track || e.artist != old.artist + else -> false + } + } + if (idx >= 0) idx + else resolveByIdOrFallback(prevPinnedId, events, current) + } + userInitiatedRefresh -> 0 + else -> resolveByIdOrFallback(prevPinnedId, events, current) + } + val shouldReset = hasNewEvents || userInitiatedRefresh || hasSignificantChange + + _uiState.value = + IslandUiState( + events = events, + islandState = newState, + pinnedEventIndex = pinnedIndex, + manuallyHidden = if (shouldReset) false else current.manuallyHidden, + forceVisible = false, + notificationAlert = current.notificationAlert, + ) + } + } + } + + fun cycleNext() { + val current = _uiState.value + if (current.events.size <= 1) return + val next = (current.pinnedEventIndex + 1) % current.events.size + _uiState.value = current.copy(pinnedEventIndex = next) + } + + fun cyclePrev() { + val current = _uiState.value + if (current.events.size <= 1) return + val prev = (current.pinnedEventIndex - 1 + current.events.size) % current.events.size + _uiState.value = current.copy(pinnedEventIndex = prev) + } + + fun pinEventAt(index: Int) { + val current = _uiState.value + if (index < 0 || index >= current.events.size) return + _uiState.value = current.copy(pinnedEventIndex = index) + } + + override fun dismissEvent(event: IslandEvent) { + autoDismissJobs[event.id]?.cancel() + autoDismissJobs.remove(event.id) + if (event.behavior.suppressOnDismiss) { + dismissedEventIds.add(event.id) + } + + val current = _uiState.value + val updatedEvents = current.events.filter { it.id != event.id } + val newIndex = + current.pinnedEventIndex.coerceAtMost((updatedEvents.size - 1).coerceAtLeast(0)) + + _uiState.value = + current.copy( + events = updatedEvents, + pinnedEventIndex = newIndex, + islandState = + if (updatedEvents.isEmpty()) IslandState.HIDDEN else current.islandState, + ) + + when (event) { + is IslandEvent.ScreenRecording -> repository.screenRecord.stopListening() + is IslandEvent.MicCamActive -> {} + is IslandEvent.AudioRecording -> repository.notification.clearAudioRecording() + is IslandEvent.Casting -> repository.connectivity.clearCasting() + is IslandEvent.Sports -> { + repository.smartspace.clearSportsEvent(event.key) + repository.notification.clearSportsEvent(event.key) + } + is IslandEvent.NowPlaying -> {} + is IslandEvent.PromotedOngoing -> + repository.notification.clearPromotedOngoing(event.sbn.key) + is IslandEvent.Media -> repository.media.clear() + is IslandEvent.Bluetooth -> repository.connectivity.clearBluetooth() + is IslandEvent.Hotspot -> repository.connectivity.clearHotspot() + is IslandEvent.Charging -> repository.system.clearCharging() + is IslandEvent.Alarm -> repository.notification.clearAlarm() + is IslandEvent.Timer -> repository.notification.clearTimer() + is IslandEvent.Stopwatch -> repository.notification.clearStopwatch() + is IslandEvent.RingerMode -> repository.system.clearRinger() + is IslandEvent.Vpn -> repository.connectivity.clearVpn() + is IslandEvent.Clipboard -> repository.system.clearClipboard() + is IslandEvent.Notification -> repository.notification.dismissNotification(event) + is IslandEvent.AppSwitch -> repository.appTracking.clear() + is IslandEvent.Torch -> { + repository.torch.toggleTorch() + repository.torch.clear() + } + is IslandEvent.BiometricUnlock -> repository.biometric.clear() + is IslandEvent.KeyguardIndication -> repository.clearIndicationEvent(event.indicationType) + } + } + + fun getTopEvent(): IslandEvent? = _uiState.value.topEvent + + override fun collapseIsland() { + onCollapseRequested?.invoke() + } + + override fun onNotificationInteraction(eventId: String) { + autoDismissJobs[eventId]?.cancel() + autoDismissJobs.remove(eventId) + } + + override fun onNotificationInteractionEnd(eventId: String) { + val event = _uiState.value.events.find { it.id == eventId } ?: return + scheduleAutoDismiss(event) + } + + override fun onNotificationAlertInteractionStart() { + notifAlertJob?.cancel() + notifAlertJob = null + } + + override fun onNotificationAlertInteractionEnd() { + val current = _uiState.value + val alert = current.notificationAlert ?: return + if (alert.isActiveCall()) return + notifAlertJob = applicationScope.launch { + delay(NOTIF_ALERT_DURATION_MS) + dismissNotificationAlert() + } + } + + fun dismissNotificationAlert() { + notifAlertJob?.cancel() + notifAlertJob = null + val current = _uiState.value + if (current.notificationAlert != null) { + _uiState.value = current.copy(notificationAlert = null) + } + } + + private fun shouldSuppressForDndOrRinger(notification: IslandEvent.Notification): Boolean { + if (notification.isActiveCall()) return false + val category = notification.sbn.notification?.category + if (category == Notification.CATEGORY_CALL || category == Notification.CATEGORY_ALARM) return false + val zenMode = zenModeController.zen + if (zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS || + zenMode == Global.ZEN_MODE_ALARMS) return true + val ringerMode = audioManager.ringerMode + return ringerMode == AudioManager.RINGER_MODE_SILENT + } + + private fun showNotificationAlert( + notification: IslandEvent.Notification, + ) { + if (panelBlocking || statusBlocking || _isOnKeyguard.value) return + if (shouldSuppressForDndOrRinger(notification)) return + val current = _uiState.value + val existingAlert = current.notificationAlert + if (existingAlert != null && + existingAlert.isActiveCall() && + !notification.isActiveCall() + ) return + + val hasProgress = notification.progress >= 0 || notification.isProgressIndeterminate + val isSameKey = existingAlert != null && existingAlert.sbn.key == notification.sbn.key + + if (isSameKey && hasProgress) { + _uiState.value = current.copy(notificationAlert = notification) + return + } + + notifAlertJob?.cancel() + _uiState.value = current.copy(notificationAlert = notification) + + if (hasProgress) return + + val duration = + if (!notification.isActiveCall()) NOTIF_ALERT_DURATION_MS else null + if (duration != null) { + notifAlertJob = applicationScope.launch { + delay(duration) + dismissNotificationAlert() + } + } + } + + override fun stopScreenRecording() = repository.screenRecord.stopRecording() + + override fun togglePlayPause() = repository.media.togglePlayPause() + + override fun skipNext() = repository.media.skipNext() + + override fun skipPrev() = repository.media.skipPrev() + + override fun sendCustomAction(action: String) = repository.media.sendCustomAction(action) + + override fun openMediaOutputSwitcher() = repository.media.openMediaOutputSwitcher() + + override fun disconnectBluetooth(address: String) = repository.connectivity.disconnectBluetooth(address) + + override fun openUrl(url: String) = repository.system.openUrl(url) + + override fun openMediaApp() = repository.media.openMediaApp() + + override fun seekTo(position: Long) = repository.media.seekTo(position) + + override fun setRingerMode(mode: Int) = repository.system.setRingerMode(mode) + + override fun toggleTorch() = repository.torch.toggleTorch() + + override fun launchNotificationDismissingKeyguard(event: IslandEvent.Notification) { + val intent = event.sbn.notification?.contentIntent ?: return + activityStarter.startPendingIntentDismissingKeyguard(intent) + } + + override fun setTorchLevel(level: Int) = repository.torch.setLevel(level) + + override fun setTorchLevelTemporary(level: Int) = repository.torch.setLevelTemporary(level) + + override fun copyToClipboard(text: String) = repository.system.copyToClipboard(text) + + override fun copyUriToClipboard(uri: Uri) = repository.system.copyUriToClipboard(uri) + + override fun removeClipboardItem(id: Long) = repository.system.removeClipboardItem(id) + + override fun switchToApp(taskId: Int) = repository.appTracking.switchToApp(taskId) + + fun onPanelExpandedChanged(expanded: Boolean) { + panelBlocking = expanded + _isPanelExpanded.value = expanded + if (expanded) dismissNotificationAlert() + updateChipVisibility() + } + + private fun updateChipVisibility() { + val current = _uiState.value + val shouldHide = panelBlocking || statusBlocking + if (shouldHide && current.islandState == IslandState.CHIP) { + _uiState.value = current.copy(islandState = IslandState.HIDDEN) + } else if (!shouldHide && current.events.isNotEmpty() && current.islandState == IslandState.HIDDEN && !current.manuallyHidden) { + _uiState.value = current.copy(islandState = IslandState.CHIP) + } + } + + private fun scheduleAutoDismiss(event: IslandEvent, delayOverride: Long? = null) { + val ms = delayOverride ?: event.behavior.autoDismissMs ?: return + val eventId = event.id + autoDismissJobs[eventId]?.cancel() + autoDismissJobs[eventId] = + applicationScope.launch { + delay(ms) + val current = _uiState.value.events.find { it.id == eventId } ?: event + dismissEvent(current) + } + } + + private fun resolveByIdOrFallback( + pinnedId: String?, + events: List, + current: IslandUiState, + ): Int { + if (pinnedId != null) { + val idx = events.indexOfFirst { it.id == pinnedId } + if (idx >= 0) return idx + } + return current.pinnedEventIndex.coerceAtMost( + (events.size - 1).coerceAtLeast(0) + ) + } + + private fun mapIndicationType(type: Int): IslandEvent.KeyguardIndication.IndicationType? = + when (type) { + KeyguardIndicationController.AX_TYPE_BIOMETRIC -> + IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC + KeyguardIndicationController.AX_TYPE_TRANSIENT -> + IslandEvent.KeyguardIndication.IndicationType.TRANSIENT + KeyguardIndicationController.AX_TYPE_TRUST -> + IslandEvent.KeyguardIndication.IndicationType.TRUST + KeyguardIndicationController.AX_TYPE_DISCLOSURE -> + IslandEvent.KeyguardIndication.IndicationType.DISCLOSURE + KeyguardIndicationController.AX_TYPE_OWNER_INFO -> + IslandEvent.KeyguardIndication.IndicationType.OWNER_INFO + KeyguardIndicationController.AX_TYPE_ALIGNMENT -> + IslandEvent.KeyguardIndication.IndicationType.ALIGNMENT + KeyguardIndicationController.AX_TYPE_PERSISTENT_UNLOCK -> + IslandEvent.KeyguardIndication.IndicationType.PERSISTENT_UNLOCK + else -> null + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt new file mode 100644 index 000000000000..926c9c30ff4d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -0,0 +1,115 @@ +package com.android.systemui.axdynamicbar.domain + +import android.database.ContentObserver +import android.os.Handler +import android.os.UserHandle +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.EVENT_TYPE_IDS +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONArray + +@SysUISingleton +class AxDynamicBarSettings @Inject constructor( + @Main private val mainHandler: Handler, + private val secureSettings: SecureSettings, +) { + companion object { + const val KEY_ENABLED = "ax_dynamic_bar_enabled" + const val KEY_EVENTS = "ax_dynamic_bar_events" + const val KEY_KEYGUARD_ENABLED = "ax_dynamic_bar_keyguard_enabled" + const val KEY_COMPACT_NOTIFICATIONS = "ax_dynamic_bar_compact_notifications" + } + + private val _isEnabled = MutableStateFlow(false) + @get:JvmName("getIsEnabled") val isEnabled: StateFlow = _isEnabled.asStateFlow() + + private val _isKeyguardEnabled = MutableStateFlow(true) + val isKeyguardEnabled: StateFlow = _isKeyguardEnabled.asStateFlow() + + private val _compactNotifications = MutableStateFlow(true) + val compactNotifications: StateFlow = _compactNotifications.asStateFlow() + + private val _disabledEventTypes = MutableStateFlow>(emptySet()) + val disabledEventTypes: StateFlow> = _disabledEventTypes.asStateFlow() + + private val settingsObserver = + object : ContentObserver(mainHandler) { + override fun onChange(selfChange: Boolean) { + refresh() + } + } + + private var initialized = false + + fun init() { + if (initialized) return + initialized = true + refresh() + secureSettings.registerContentObserverForUserSync( + KEY_ENABLED, + false, + settingsObserver, + UserHandle.USER_ALL, + ) + secureSettings.registerContentObserverForUserSync( + KEY_EVENTS, + false, + settingsObserver, + UserHandle.USER_ALL, + ) + secureSettings.registerContentObserverForUserSync( + KEY_KEYGUARD_ENABLED, + false, + settingsObserver, + UserHandle.USER_ALL, + ) + secureSettings.registerContentObserverForUserSync( + KEY_COMPACT_NOTIFICATIONS, + false, + settingsObserver, + UserHandle.USER_ALL, + ) + } + + fun destroy() { + if (!initialized) return + initialized = false + secureSettings.getContentResolver().unregisterContentObserver(settingsObserver) + } + + private fun refresh() { + _isEnabled.value = + secureSettings.getIntForUser(KEY_ENABLED, 0, UserHandle.USER_CURRENT) == 1 + _isKeyguardEnabled.value = + secureSettings.getIntForUser(KEY_KEYGUARD_ENABLED, 1, UserHandle.USER_CURRENT) == 1 + _compactNotifications.value = + secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 + + val json = secureSettings.getStringForUser(KEY_EVENTS, UserHandle.USER_CURRENT) ?: "" + _disabledEventTypes.value = + try { + if (json.isBlank()) emptySet() + else { + val arr = JSONArray(json) + (0 until arr.length()).mapNotNull { arr.optString(it) }.toSet() + } + } catch (_: Exception) { + emptySet() + } + } + + fun isEventEnabled(event: IslandEvent): Boolean { + val typeId = EVENT_TYPE_IDS[event::class.java] ?: return true + return typeId !in _disabledEventTypes.value + } + + fun isNotificationEventsActive(): Boolean = + _isEnabled.value && "notification" !in _disabledEventTypes.value +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt new file mode 100644 index 000000000000..5d2d8822c1aa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt @@ -0,0 +1,338 @@ +package com.android.systemui.axdynamicbar.model + +import android.app.Notification +import android.app.RemoteInput +import android.graphics.drawable.Drawable +import android.net.Uri +import android.service.notification.StatusBarNotification + +enum class RecordingState { + RECORDING, + PAUSED, + SAVED, +} + +data class MicCamApp(val packageName: String, val appName: String, val appIcon: Drawable? = null) + +const val NOTIFICATION_DECAY_MS = 5000L +const val NOTIFICATION_FRESH_PRIORITY = 75 +const val NOTIFICATION_STALE_PRIORITY = 15 +const val MEDIA_PLAYING_PRIORITY = 70 +const val MEDIA_PAUSED_PRIORITY = 20 + +data class EventBehavior( + val autoDismissMs: Long? = null, + val suppressOnDismiss: Boolean = true, + val autoShowsIsland: Boolean = true, +) + +sealed class IslandEvent(open val priority: Int, val id: String) : Comparable { + + open val behavior: EventBehavior + get() = DEFAULT_BEHAVIOR + + open fun withoutDrawables(): IslandEvent = this + + data class ScreenRecording( + val startTimeMs: Long = System.currentTimeMillis(), + val countdownSeconds: Long = 0, + ) : IslandEvent(priority = 90, id = "screen_recording") { + val isCountdown: Boolean get() = countdownSeconds > 0 + } + + data class MicCamActive( + val isMic: Boolean, + val isCam: Boolean, + val appName: String = "", + val apps: List = emptyList(), + ) : IslandEvent(priority = 85, id = "mic_cam") { + override fun withoutDrawables() = copy(apps = apps.map { it.copy(appIcon = null) }) + } + + data class AudioRecording( + val appName: String = "", + val state: RecordingState = RecordingState.RECORDING, + val startTimeMs: Long = System.currentTimeMillis(), + val actions: List = emptyList(), + val appIcon: Drawable? = null, + val pausedDurationMs: Long = 0L, + ) : IslandEvent(priority = 84, id = "audio_recording") { + override fun withoutDrawables() = copy(appIcon = null) + } + + data class Casting(val deviceName: String = "Screen", val description: String? = null) : + IslandEvent(priority = 80, id = "casting") + + data class Media( + val track: String, + val artist: String, + val isPlaying: Boolean, + val albumArt: Drawable? = null, + val progress: Float = 0f, + val duration: Long = 0L, + val position: Long = 0L, + val playbackSpeed: Float = 1f, + val positionUpdateTime: Long = 0L, + val outputDeviceName: String = "", + val customActions: List = emptyList(), + val appIcon: Drawable? = null, + val packageName: String = "", + val mediaColor: Int = 0, + ) : IslandEvent(priority = MEDIA_PLAYING_PRIORITY, id = "media") { + override val priority: Int + get() = if (isPlaying) MEDIA_PLAYING_PRIORITY else MEDIA_PAUSED_PRIORITY + override fun withoutDrawables() = copy(albumArt = null, appIcon = null) + } + + data class Bluetooth( + val deviceName: String, + val batteryLevel: Int = -1, + val isConnected: Boolean = true, + val address: String = "", + val deviceIcon: Drawable? = null, + val deviceTypeLabel: String = "", + ) : IslandEvent(priority = 60, id = "bluetooth") { + override fun withoutDrawables() = copy(deviceIcon = null) + } + + data class Hotspot(val numDevices: Int) : IslandEvent(priority = 55, id = "hotspot") + + data class Charging( + val level: Int, + val isWireless: Boolean, + val isCharging: Boolean = true, + val isPowerSave: Boolean = false, + val timeRemaining: String? = null, + ) : IslandEvent(priority = 50, id = "charging") { + override val behavior = EventBehavior(autoDismissMs = 3000L, suppressOnDismiss = false) + } + + data class RingerMode(val mode: Int, val label: String) : + IslandEvent(priority = 40, id = "ringer_mode") { + override val behavior = EventBehavior(autoDismissMs = 2500L, suppressOnDismiss = false) + } + + data class BiometricUnlock(val sourceType: Int = 0, val sourceName: String = "Fingerprint") : + IslandEvent(priority = 80, id = "biometric_unlock") { + override val behavior = EventBehavior(autoDismissMs = 2000L, suppressOnDismiss = false) + } + + data class Torch(val level: Int = -1, val maxLevel: Int = -1) : + IslandEvent(priority = 97, id = "torch") { + override val behavior = EventBehavior(suppressOnDismiss = false) + val supportsLevel: Boolean + get() = level >= 1 && maxLevel > 1 + } + + data class Vpn(val isBranded: Boolean = false, val isValidated: Boolean = false) : + IslandEvent(priority = 35, id = "vpn") + + data class ClipboardItem( + val id: Long, + val preview: String, + val label: String = "", + val isUrl: Boolean = false, + val isImage: Boolean = false, + val imageUri: Uri? = null, + val timestamp: Long = System.currentTimeMillis(), + ) + + data class Clipboard( + val label: String = "", + val preview: String = "", + val isUrl: Boolean = false, + val isImage: Boolean = false, + val imageUri: Uri? = null, + val items: List = emptyList(), + ) : IslandEvent(priority = 25, id = "clipboard") + + data class PromotedOngoing( + val shortText: String = "", + val title: String = "", + val text: String = "", + val appName: String = "", + val appIcon: Drawable? = null, + val sbn: StatusBarNotification, + val actions: List = emptyList(), + val progress: Float = -1f, + val isIndeterminate: Boolean = false, + ) : IslandEvent(priority = 72, id = "promoted_${sbn.key}") { + override fun withoutDrawables() = copy(appIcon = null) + } + + data class Sports( + val team1Name: String, + val team2Name: String, + val score1: String, + val score2: String, + val team1Icon: Drawable? = null, + val team2Icon: Drawable? = null, + val status: GameStatus = GameStatus.LIVE, + val statusDetail: String = "", + val league: String = "", + val commentary: String = "", + val key: String, + val sbn: StatusBarNotification? = null, + val appIcon: Drawable? = null, + ) : IslandEvent(priority = 73, id = "sports_$key") { + override fun withoutDrawables() = copy(team1Icon = null, team2Icon = null, appIcon = null) + } + + enum class GameStatus { PRE_GAME, LIVE, HALFTIME, FINAL } + + data class NowPlaying( + val songTitle: String, + val artist: String, + val key: String, + val sbn: StatusBarNotification? = null, + val appIcon: Drawable? = null, + val actions: List = emptyList(), + ) : IslandEvent(priority = 42, id = "now_playing") { + override val behavior = EventBehavior(autoDismissMs = null) + override fun withoutDrawables() = copy(appIcon = null) + } + + data class Timer( + val label: String = "", + val endTimeMs: Long = 0L, + val originalDurationMs: Long = 0L, + val appIcon: Drawable? = null, + val isPaused: Boolean = false, + val actions: List = emptyList(), + ) : IslandEvent(priority = 45, id = "timer") { + override fun withoutDrawables() = copy(appIcon = null) + } + + data class Stopwatch( + val label: String = "", + val startTimeMs: Long = System.currentTimeMillis(), + val isRunning: Boolean = true, + val appIcon: Drawable? = null, + val actions: List = emptyList(), + ) : IslandEvent(priority = 44, id = "stopwatch") { + override fun withoutDrawables() = copy(appIcon = null) + } + + data class Alarm( + val label: String = "", + val triggerTimeMs: Long = 0L, + val isRinging: Boolean = false, + val appIcon: Drawable? = null, + ) : IslandEvent(priority = 48, id = "alarm") { + override val behavior = EventBehavior(autoDismissMs = 5000L) + override fun withoutDrawables() = copy(appIcon = null) + } + + data class AppSwitch( + val recentApps: List = emptyList(), + val previousApp: RecentApp? = null, + ) : IslandEvent(priority = 10, id = "app_switch") { + override val behavior = EventBehavior(suppressOnDismiss = false, autoShowsIsland = false) + override fun withoutDrawables() = copy( + recentApps = recentApps.map { it.copy(appIcon = null) }, + previousApp = previousApp?.copy(appIcon = null), + ) + } + + data class KeyguardIndication( + val text: String, + val indicationType: IndicationType, + ) : IslandEvent(priority = 5, id = "kg_indication_${indicationType.name}") { + override val behavior: EventBehavior + get() = when (indicationType) { + IndicationType.BIOMETRIC -> EventBehavior(autoDismissMs = 3500L, suppressOnDismiss = false) + IndicationType.TRANSIENT -> EventBehavior(autoDismissMs = 3500L, suppressOnDismiss = false) + IndicationType.TRUST -> EventBehavior(suppressOnDismiss = false) + IndicationType.DISCLOSURE -> EventBehavior(suppressOnDismiss = false) + IndicationType.OWNER_INFO -> EventBehavior(suppressOnDismiss = false) + IndicationType.ALIGNMENT -> EventBehavior(autoDismissMs = 5000L, suppressOnDismiss = false) + IndicationType.PERSISTENT_UNLOCK -> EventBehavior(suppressOnDismiss = false) + } + + enum class IndicationType { + BIOMETRIC, + TRANSIENT, + TRUST, + DISCLOSURE, + OWNER_INFO, + ALIGNMENT, + PERSISTENT_UNLOCK, + } + } + + data class RecentApp( + val packageName: String, + val appName: String, + val appIcon: Drawable? = null, + val taskId: Int = -1, + val lastActiveTime: Long = System.currentTimeMillis(), + ) + + data class NotificationAction(val label: CharSequence, val action: Notification.Action) + + data class ReplyAction( + val label: CharSequence, + val action: Notification.Action, + val remoteInput: RemoteInput, + ) + + data class MediaCustomAction(val label: String, val action: String) + + data class Notification( + val sbn: StatusBarNotification, + val title: String? = null, + val text: String? = null, + val appIcon: Drawable? = null, + val appName: String = "", + val progress: Int = -1, + val progressMax: Int = 100, + val isProgressIndeterminate: Boolean = false, + val actions: List = emptyList(), + val replyAction: ReplyAction? = null, + val isConversation: Boolean = false, + val isGroupConversation: Boolean = false, + val conversationTitle: String? = null, + val senderIcon: Drawable? = null, + val senderName: String? = null, + val groupKey: String? = null, + val isGroupSummary: Boolean = false, + val notificationImage: Drawable? = null, + val callStartTimeMs: Long = 0L, + val createdAt: Long = System.currentTimeMillis(), + ) : IslandEvent(priority = NOTIFICATION_STALE_PRIORITY, id = "notification_${sbn.key}") { + override val behavior = EventBehavior(autoDismissMs = null) + override val priority: Int + get() = + if (System.currentTimeMillis() - createdAt < NOTIFICATION_DECAY_MS) + NOTIFICATION_FRESH_PRIORITY + else NOTIFICATION_STALE_PRIORITY + } + + companion object { + internal val DEFAULT_BEHAVIOR = EventBehavior() + + val ONGOING_TYPES: Set> = + setOf( + ScreenRecording::class.java, + MicCamActive::class.java, + AudioRecording::class.java, + Casting::class.java, + PromotedOngoing::class.java, + Sports::class.java, + Media::class.java, + Timer::class.java, + Stopwatch::class.java, + ) + } + + val isOngoing: Boolean + get() = this::class.java in ONGOING_TYPES + + override fun compareTo(other: IslandEvent): Int = other.priority - this.priority +} + +enum class IslandState { + HIDDEN, + CHIP, +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt new file mode 100644 index 000000000000..debea8326302 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt @@ -0,0 +1,24 @@ +package com.android.systemui.axdynamicbar.model + +data class IslandUiState( + val events: List = emptyList(), + val islandState: IslandState = IslandState.HIDDEN, + val pinnedEventIndex: Int = 0, + val manuallyHidden: Boolean = false, + val forceVisible: Boolean = false, + val notificationAlert: IslandEvent.Notification? = null, +) { + + val shouldShow: Boolean + get() = islandState == IslandState.CHIP || notificationAlert != null + + val isChip: Boolean + get() = islandState == IslandState.CHIP + + val topEvent: IslandEvent? + get() = events.getOrNull(pinnedEventIndex) ?: events.firstOrNull() + + val activeEvents: List + get() = events.filter { it !is IslandEvent.Notification } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt new file mode 100644 index 000000000000..2a406d59f4b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt @@ -0,0 +1,58 @@ +package com.android.systemui.axdynamicbar.shared + +import android.net.Uri +import com.android.systemui.axdynamicbar.model.IslandEvent + +val EVENT_TYPE_IDS: Map, String> = + mapOf( + IslandEvent.ScreenRecording::class.java to "screen_recording", + IslandEvent.MicCamActive::class.java to "privacy", + IslandEvent.AudioRecording::class.java to "audio_recording", + IslandEvent.Casting::class.java to "casting", + IslandEvent.PromotedOngoing::class.java to "promoted_ongoing", + IslandEvent.Sports::class.java to "sports", + IslandEvent.Media::class.java to "media", + IslandEvent.Bluetooth::class.java to "bluetooth", + IslandEvent.Hotspot::class.java to "hotspot", + IslandEvent.Charging::class.java to "charging", + IslandEvent.Alarm::class.java to "alarm", + IslandEvent.Timer::class.java to "timer", + IslandEvent.Stopwatch::class.java to "stopwatch", + IslandEvent.RingerMode::class.java to "ringer", + IslandEvent.Vpn::class.java to "vpn", + IslandEvent.Clipboard::class.java to "clipboard", + IslandEvent.Notification::class.java to "notification", + IslandEvent.AppSwitch::class.java to "app_switch", + IslandEvent.Torch::class.java to "torch", + IslandEvent.BiometricUnlock::class.java to "biometric_unlock", + ) + +interface IslandActions { + val onFocusableRequested: ((Boolean) -> Unit)? + get() = null + fun collapseIsland() + fun dismissEvent(event: IslandEvent) + fun togglePlayPause() + fun skipNext() + fun skipPrev() + fun seekTo(position: Long) + fun sendCustomAction(action: String) + fun openMediaOutputSwitcher() + fun openMediaApp() + fun stopScreenRecording() + fun disconnectBluetooth(address: String) + fun setRingerMode(mode: Int) + fun toggleTorch() + fun setTorchLevel(level: Int) + fun setTorchLevelTemporary(level: Int) + fun copyToClipboard(text: String) + fun copyUriToClipboard(uri: Uri) + fun openUrl(url: String) + fun removeClipboardItem(id: Long) + fun switchToApp(taskId: Int) + fun onNotificationInteraction(eventId: String) + fun onNotificationInteractionEnd(eventId: String) + fun onNotificationAlertInteractionStart() + fun onNotificationAlertInteractionEnd() + fun launchNotificationDismissingKeyguard(event: IslandEvent.Notification) +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt new file mode 100644 index 000000000000..d636b715efd0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt @@ -0,0 +1,569 @@ +package com.android.systemui.axdynamicbar.shared + +import android.app.ActivityOptions +import com.android.systemui.res.R +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import androidx.core.graphics.ColorUtils +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.ui.graphics.toArgb +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.ThumbDown +import androidx.compose.material.icons.filled.ThumbUp +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import android.os.SystemClock +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import com.android.systemui.axdynamicbar.model.IslandEvent +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import androidx.compose.material3.Icon +import androidx.compose.ui.graphics.vector.ImageVector + +internal val SpaceXxs = 2.dp +internal val SpaceXs = 4.dp +internal val SpaceSm = 6.dp +internal val SpaceMd = 8.dp +internal val SpaceLg = 12.dp +internal val SpaceXl = 14.dp +internal val SpaceXxl = 16.dp +internal val SpaceSection = 20.dp +internal val SpacePanel = 24.dp +internal val SpacePanelLarge = 28.dp + +internal val ShapeXs = RoundedCornerShape(8.dp) +internal val ShapeSm = RoundedCornerShape(12.dp) +internal val ShapeLg = RoundedCornerShape(24.dp) +internal val ShapeXl = RoundedCornerShape(32.dp) +internal val ShapeCard = RoundedCornerShape(28.dp) + +internal val SizeBadge = 14.dp +internal val SizeIconSm = 20.dp +internal val SizeIconMd = 28.dp +internal val SizeButton = 48.dp +internal val SizeButtonLg = 48.dp +internal val SizeButtonXl = 64.dp +internal val SizeAlbumSm = 52.dp +internal val SizeAlbumLg = 200.dp +internal val SizeSeekHeight = 16.dp +internal val SizeProgressHeight = 8.dp +internal val SizeStrokeWidth = 2.dp +internal val SizeStrokeThin = 1.5f +internal val SizeCompactIcon = 40.dp +internal val SizeActionHeight = 48.dp + +internal const val AlphaSecondary = 0.7f +internal const val AlphaTertiary = 0.5f +internal const val AlphaHint = 0.4f +internal const val AlphaDisabled = 0.3f +internal const val AlphaSubtle = 0.15f +internal const val AlphaFaint = 0.1f +internal const val AlphaBorder = 0.08f +internal const val AlphaStatusChip = 0.14f +internal const val AlphaIconBg = 0.16f +internal const val AlphaTrack = 0.25f + +internal val TsBadge: TextStyle + @Composable get() = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp) + + +internal val BatteryChargingColor = Color(0xFF66BB6A) +internal val BatteryPowerSaveColor = Color(0xFFFFA726) +internal val BatteryNeutralColor: Color + @Composable get() = MaterialTheme.colorScheme.surfaceVariant +internal val ChipContentDark = Color(0xFF1B1B1B) + +internal val RedAccent = Color(0xFFEF5350) +internal val PinkAccent = Color(0xFFEC407A) +internal val OrangeAccent = Color(0xFFFFA726) +internal val YellowAccent = Color(0xFFFFCA28) +internal val GreenAccent = Color(0xFF66BB6A) +internal val MintAccent = Color(0xFF26A69A) +internal val TealAccent = Color(0xFF29B6F6) +internal val BlueAccent = Color(0xFF42A5F5) +internal val IndigoAccent = Color(0xFF7E57C2) +internal val PurpleAccent = Color(0xFFAB47BC) +internal val PausedGray = Color(0xFF8E8E93) + +internal val ExpandedMaxWidth = 420.dp + +internal val SubtleGray: Color + @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant +internal val CardBg: Color + @Composable get() = MaterialTheme.colorScheme.surfaceBright +internal val DarkCard: Color + @Composable get() = MaterialTheme.colorScheme.surfaceContainerHigh +internal val OnCardText: Color + @Composable get() = MaterialTheme.colorScheme.onSurface +internal val OnCardSecondary: Color + @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant +internal val ActionBg: Color + @Composable get() = MaterialTheme.colorScheme.primary +internal val OnActionText: Color + @Composable get() = MaterialTheme.colorScheme.onPrimary +internal val DestructiveBg: Color + @Composable get() = MaterialTheme.colorScheme.errorContainer +internal val OnDestructiveText: Color + @Composable get() = MaterialTheme.colorScheme.onErrorContainer + +internal val CardBorderBrush: Brush + @Composable + get() = + Brush.verticalGradient( + colors = + listOf( + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.08f), + ) + ) + +internal val PillPrimary: TextStyle + @Composable get() = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.SemiBold) + +internal val PillAccent: TextStyle + @Composable get() = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold) +internal val PillMono: TextStyle + @Composable get() = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace) +internal val TsMono: TextStyle + @Composable get() = MaterialTheme.typography.labelMedium.copy(fontFamily = FontFamily.Monospace) + +internal val ShapeIconLarge = RoundedCornerShape(20.dp) +internal val ShapeIconMedium = RoundedCornerShape(16.dp) +internal val ShapeAlbum = RoundedCornerShape(16.dp) +internal val ShapeChip = RoundedCornerShape(percent = 50) +internal val ShapeCompact = RoundedCornerShape(12.dp) + +internal fun accentColorFor(event: IslandEvent): Color = eventStyleFor(event).accent + +internal fun chipContentColorOn(background: Color): Color { + val luminance = ColorUtils.calculateLuminance(background.toArgb()) + return if (luminance > 0.4) ChipContentDark else Color.White +} + +internal fun darkenColor(color: Color, keep: Float = 0.35f): Color = + Color( + red = color.red * keep, + green = color.green * keep, + blue = color.blue * keep, + alpha = 1f, + ) + +private const val DARK_THEME_MIN_L = 0.15 +private const val LIGHT_THEME_MAX_L = 0.4 + +private fun ensureContrast(color: Color, isDark: Boolean): Color { + val lum = ColorUtils.calculateLuminance(color.toArgb()) + if (isDark && lum >= DARK_THEME_MIN_L) return color + if (!isDark && lum <= LIGHT_THEME_MAX_L) return color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color.toArgb(), hsl) + if (isDark) { + hsl[2] = (hsl[2] + 0.15f).coerceAtMost(0.65f) + } else { + hsl[2] = (hsl[2] - 0.15f).coerceAtLeast(0.25f) + } + return Color(ColorUtils.HSLToColor(hsl)) +} + +internal data class IslandColorScheme( + val accent: Color, + val onAccent: Color, + val tonal: Color, + val surfaceTint: Color, +) + +@Composable +internal fun rememberIslandColors(event: IslandEvent): IslandColorScheme = + buildColorScheme(accentColorFor(event)) + +@Composable +internal fun rememberMediaColors(event: IslandEvent.Media): IslandColorScheme { + val raw = if (event.mediaColor != 0) Color(event.mediaColor) else PurpleAccent + return buildColorScheme(raw) +} + +@Composable +private fun buildColorScheme(raw: Color): IslandColorScheme { + val isDark = ColorUtils.calculateLuminance( + MaterialTheme.colorScheme.surface.toArgb() + ) < 0.5 + val accent = if (!isDark) ensureContrast(raw) else raw + return IslandColorScheme( + accent = accent, + onAccent = chipContentColorOn(accent), + tonal = accent.copy(alpha = AlphaFaint), + surfaceTint = if (isDark) darkenColor(raw, keep = 0.25f) else accent.copy(alpha = 0.12f), + ) +} + +private fun ensureContrast(color: Color): Color { + val lum = ColorUtils.calculateLuminance(color.toArgb()).toFloat() + return if (lum > 0.5f) darkenColor(color, keep = 0.55f) else color +} + +@Composable +internal fun chipAccentColorFor(event: IslandEvent): Color { + if (event is IslandEvent.Media && event.mediaColor != 0) { + return darkenColor(Color(event.mediaColor)) + } + if (event is IslandEvent.AppSwitch) { + val app = event.previousApp ?: event.recentApps.firstOrNull() + if (app?.appIcon != null) { + val color = rememberPaletteColor(app.appIcon!!) + if (color != null) return color + } + } + if (event is IslandEvent.Notification && event.appIcon != null) { + val isDark = isSystemInDarkTheme() + val color = rememberPaletteColor(event.appIcon!!) + if (color != null) return ensureContrast(color, isDark) + } + return accentColorFor(event) +} + +@Composable +private fun rememberPaletteColor(drawable: Drawable): Color? { + val color by produceState(null, drawable) { + + val bitmap = try { + withContext(Dispatchers.Main) { drawable.toBitmap(24, 24) } + } catch (_: Exception) { null } + value = bitmap?.let { + withContext(Dispatchers.Default) { + try { + val palette = Palette.from(it).generate() + val swatch = + palette.darkVibrantSwatch + ?: palette.vibrantSwatch + ?: palette.darkMutedSwatch + ?: palette.dominantSwatch + swatch?.let { s -> Color(s.rgb) } + } catch (_: Exception) { + null + } + } + } + } + return color +} + +@Composable +internal fun StatusChip(text: String, color: Color = SubtleGray) { + Box( + modifier = + Modifier.background(color.copy(alpha = AlphaSubtle), ShapeChip) + .padding(horizontal = SpaceXl, vertical = SpaceSm) + ) { + Text(text.uppercase(), color = color, style = MaterialTheme.typography.labelSmall) + } +} + +@Composable +internal fun CircleButton( + color: Color = ActionBg, + size: Dp = 48.dp, + onClick: () -> Unit, + content: @Composable () -> Unit, +) { + Surface( + onClick = onClick, + modifier = Modifier.size(size), + shape = CircleShape, + color = color, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size)) { content() } + } +} + +@Composable +internal fun ExpandedCardLayout( + accentColor: Color, + icon: @Composable () -> Unit, + iconSize: Dp = 44.dp, + iconBackground: Boolean = true, + title: @Composable ColumnScope.() -> Unit, + trailing: @Composable (() -> Unit)? = null, + actions: @Composable (ColumnScope.() -> Unit)? = null, +) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(SpaceLg)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeLg) + .background(accentColor.copy(alpha = AlphaFaint)) + .padding(SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (iconBackground) { + Box( + modifier = Modifier + .size(iconSize) + .clip(CircleShape) + .background(accentColor.copy(alpha = AlphaSubtle)), + contentAlignment = Alignment.Center, + ) { icon() } + } else { + Box( + modifier = Modifier.size(iconSize), + contentAlignment = Alignment.Center, + ) { icon() } + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { title() } + trailing?.invoke() + } + actions?.invoke(this) + } +} + +@Composable +internal fun ActionChip( + label: String, + icon: ImageVector? = null, + color: Color = OnActionText, + bg: Color = ActionBg, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Surface( + onClick = onClick, + shape = ShapeChip, + color = bg, + modifier = modifier, + ) { + Row( + modifier = Modifier.height(SizeActionHeight).padding(horizontal = SpacePanel), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd, Alignment.CenterHorizontally), + ) { + if (icon != null) { + Icon(icon, null, tint = color, modifier = Modifier.size(SizeIconSm)) + } + Text(label, color = color, style = MaterialTheme.typography.labelLarge, maxLines = 1) + } + } +} + +@Composable +internal fun ExpressivePillButton( + label: String, + icon: ImageVector? = null, + contentColor: Color, + backgroundColor: Color, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Surface( + onClick = onClick, + shape = RoundedCornerShape(percent = 50), + color = backgroundColor, + modifier = modifier, + ) { + Row( + modifier = Modifier.height(SizeActionHeight).padding(horizontal = SpacePanel), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + if (icon != null) { + Icon( + icon, null, tint = contentColor, modifier = Modifier.size(SizeIconSm), + ) + } + Text(label, color = contentColor, style = MaterialTheme.typography.labelLarge) + } + } +} + +internal data class MediaProgress(val progress: Float, val positionMs: Long) + +@Composable +internal fun rememberMediaProgress(event: IslandEvent.Media): MediaProgress { + val duration = event.duration + var progress by remember { mutableFloatStateOf(event.progress) } + var positionMs by remember { mutableLongStateOf(event.position) } + val currentEvent by rememberUpdatedState(event) + + val computedPosition = if (event.isPlaying && duration > 0L) { + val elapsed = SystemClock.elapsedRealtime() - event.positionUpdateTime + (event.position + (elapsed * event.playbackSpeed).toLong()).coerceIn(0L, duration) + } else { + event.position + } + positionMs = computedPosition + progress = if (duration > 0L) (computedPosition.toFloat() / duration).coerceIn(0f, 1f) + else event.progress + + LaunchedEffect(event.isPlaying, event.duration) { + if (!currentEvent.isPlaying || currentEvent.duration <= 0L) return@LaunchedEffect + while (true) { + delay(1000L) + val ev = currentEvent + if (!ev.isPlaying || ev.duration <= 0L) break + val elapsed = SystemClock.elapsedRealtime() - ev.positionUpdateTime + val current = (ev.position + (elapsed * ev.playbackSpeed).toLong()) + .coerceIn(0L, ev.duration) + positionMs = current + progress = (current.toFloat() / ev.duration).coerceIn(0f, 1f) + } + } + + return MediaProgress(progress, positionMs) +} + +internal fun formatElapsedTime(ms: Long): String { + val secs = (ms / 1000).coerceAtLeast(0) + return "%d:%02d".format(secs / 60, secs % 60) +} + +internal fun formatCountdownLong(ms: Long): String { + val secs = (ms / 1000).coerceAtLeast(0) + val mins = secs / 60 + val hrs = mins / 60 + return if (hrs > 0) "%d:%02d:%02d".format(hrs, mins % 60, secs % 60) + else "%d:%02d".format(mins, secs % 60) +} + +internal fun formatStopwatch(ms: Long): String { + val totalSecs = (ms / 1000).coerceAtLeast(0) + val hrs = totalSecs / 3600 + val mins = (totalSecs % 3600) / 60 + val secs = totalSecs % 60 + val tenths = ((ms % 1000) / 100).coerceIn(0, 9) + return if (hrs > 0) "%d:%02d:%02d.%d".format(hrs, mins, secs, tenths) + else "%d:%02d.%d".format(mins, secs, tenths) +} + +internal fun formatTimeAgo(timestampMs: Long, res: android.content.res.Resources): String { + val diff = System.currentTimeMillis() - timestampMs + if (diff < 0) return "" + val secs = diff / 1000 + val mins = secs / 60 + val hrs = mins / 60 + val days = hrs / 24 + return when { + secs < 60 -> res.getString(R.string.ax_dynamic_bar_just_now) + mins < 60 -> res.getString(R.string.ax_dynamic_bar_mins_ago, mins.toInt()) + hrs < 24 -> res.getString(R.string.ax_dynamic_bar_hours_ago, hrs.toInt()) + days < 7 -> res.getString(R.string.ax_dynamic_bar_days_ago, days.toInt()) + else -> res.getString(R.string.ax_dynamic_bar_weeks_ago, (days / 7).toInt()) + } +} + +@Composable +internal fun Drawable.toScaledBitmap(sizeDp: Dp): ImageBitmap { + val px = with(LocalDensity.current) { sizeDp.roundToPx() } + return remember(this, px) { toBitmap(px, px).asImageBitmap() } +} + +internal fun chipProgressFor(event: IslandEvent): Float? = + when (event) { + is IslandEvent.Media -> + if (event.duration > 0) (event.position.toFloat() / event.duration).coerceIn(0f, 1f) + else null + is IslandEvent.PromotedOngoing -> + if (event.progress >= 0f) event.progress.coerceIn(0f, 1f) else null + is IslandEvent.Notification -> + if (event.progress >= 0) + (event.progress.toFloat() / event.progressMax).coerceIn(0f, 1f) + else null + else -> null + } + +internal fun iconKeyFor(event: IslandEvent): Any = + when (event) { + is IslandEvent.Media -> event.albumArt?.hashCode() ?: "media_default" + is IslandEvent.Notification -> event.appIcon?.hashCode() ?: "notif_default" + is IslandEvent.AppSwitch -> { + val app = event.previousApp ?: event.recentApps.firstOrNull() + app?.appIcon?.hashCode() ?: "app_default" + } + else -> event::class.simpleName ?: "default" + } + +internal fun textKeyFor(event: IslandEvent): Any = + when (event) { + is IslandEvent.Media -> "${event.track}|${event.artist}" + is IslandEvent.Timer, + is IslandEvent.Stopwatch, + is IslandEvent.AudioRecording -> "tick_text" + else -> event.id + } + +internal fun resolveCustomActionIcon(label: String): ImageVector { + val lower = label.lowercase() + return when { + lower.contains("shuffle") -> Icons.Filled.Shuffle + lower.contains("repeat") -> Icons.Filled.Repeat + lower.contains("thumb") && lower.contains("up") -> Icons.Filled.ThumbUp + lower.contains("thumb") && lower.contains("down") -> Icons.Filled.ThumbDown + lower.contains("like") || lower.contains("love") || lower.contains("favorite") -> Icons.Filled.Favorite + else -> Icons.Filled.Shuffle + } +} + +internal fun resolveEndActionIcon(label: String): ImageVector { + val lower = label.lowercase() + return when { + lower.contains("like") || lower.contains("love") || lower.contains("favorite") -> Icons.Filled.FavoriteBorder + lower.contains("thumb") && lower.contains("up") -> Icons.Filled.ThumbUp + lower.contains("thumb") && lower.contains("down") -> Icons.Filled.ThumbDown + lower.contains("repeat") -> Icons.Filled.Repeat + else -> Icons.Filled.FavoriteBorder + } +} + +internal fun PendingIntent.sendWithBal(context: Context, fillIntent: Intent? = null) { + val options = ActivityOptions.makeBasic() + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + send(context, 0, fillIntent, null, null, null, options.toBundle()) +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt new file mode 100644 index 000000000000..06f9f3dcf795 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.shared + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Alarm +import androidx.compose.material.icons.filled.AvTimer +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.Cast +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.FiberManualRecord +import androidx.compose.material.icons.filled.Fingerprint +import androidx.compose.material.icons.filled.FlashlightOn +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.Timer +import androidx.compose.material.icons.filled.Vibration +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VpnKey +import androidx.compose.material.icons.filled.Wifi +import android.media.AudioManager +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.res.R + +internal data class EventStyle( + val accent: Color, + val icon: ImageVector?, + val labelRes: Int, +) + +internal fun eventStyleFor(event: IslandEvent): EventStyle = when (event) { + is IslandEvent.ScreenRecording -> EventStyle( + accent = RedAccent, + icon = Icons.Filled.FiberManualRecord, + labelRes = R.string.ax_dynamic_bar_screen_recording, + ) + is IslandEvent.AudioRecording -> EventStyle( + accent = when (event.state) { + RecordingState.RECORDING -> RedAccent + RecordingState.PAUSED -> PausedGray + RecordingState.SAVED -> GreenAccent + }, + icon = when (event.state) { + RecordingState.RECORDING -> Icons.Filled.Mic + RecordingState.PAUSED -> Icons.Filled.Pause + RecordingState.SAVED -> Icons.Filled.CheckCircle + }, + labelRes = when (event.state) { + RecordingState.RECORDING -> R.string.ax_dynamic_bar_recording + RecordingState.PAUSED -> R.string.ax_dynamic_bar_paused + RecordingState.SAVED -> R.string.ax_dynamic_bar_saved + }, + ) + is IslandEvent.MicCamActive -> EventStyle( + accent = PinkAccent, + icon = when { + event.isCam -> Icons.Filled.Videocam + else -> Icons.Filled.Mic + }, + labelRes = when { + event.isCam && event.isMic -> R.string.ax_dynamic_bar_camera_mic + event.isCam -> R.string.ax_dynamic_bar_camera + else -> R.string.ax_dynamic_bar_microphone + }, + ) + is IslandEvent.Casting -> EventStyle( + accent = TealAccent, + icon = Icons.Filled.Cast, + labelRes = R.string.ax_dynamic_bar_casting_to, + ) + is IslandEvent.Media -> EventStyle( + accent = PurpleAccent, + icon = Icons.Filled.MusicNote, + labelRes = R.string.ax_dynamic_bar_music, + ) + is IslandEvent.PromotedOngoing -> EventStyle( + accent = BlueAccent, + icon = null, + labelRes = R.string.ax_dynamic_bar_on, + ) + is IslandEvent.NowPlaying -> EventStyle( + accent = MintAccent, + icon = Icons.Filled.MusicNote, + labelRes = R.string.ax_dynamic_bar_now_playing, + ) + is IslandEvent.Sports -> EventStyle( + accent = when (event.status) { + IslandEvent.GameStatus.LIVE, IslandEvent.GameStatus.HALFTIME -> GreenAccent + IslandEvent.GameStatus.PRE_GAME -> BlueAccent + IslandEvent.GameStatus.FINAL -> MintAccent + }, + icon = null, + labelRes = R.string.ax_dynamic_bar_on, + ) + is IslandEvent.Bluetooth -> EventStyle( + accent = BlueAccent, + icon = Icons.Filled.Bluetooth, + labelRes = R.string.ax_dynamic_bar_connected, + ) + is IslandEvent.Hotspot -> EventStyle( + accent = TealAccent, + icon = Icons.Filled.Wifi, + labelRes = R.string.ax_dynamic_bar_hotspot, + ) + is IslandEvent.Charging -> EventStyle( + accent = GreenAccent, + icon = Icons.Filled.BatteryChargingFull, + labelRes = if (event.isWireless) R.string.ax_dynamic_bar_wireless_charging + else R.string.ax_dynamic_bar_charging, + ) + is IslandEvent.Alarm -> EventStyle( + accent = OrangeAccent, + icon = Icons.Filled.Alarm, + labelRes = R.string.ax_dynamic_bar_alarm, + ) + is IslandEvent.Timer -> EventStyle( + accent = BlueAccent, + icon = Icons.Filled.Timer, + labelRes = R.string.ax_dynamic_bar_timer, + ) + is IslandEvent.Stopwatch -> EventStyle( + accent = MintAccent, + icon = Icons.Filled.AvTimer, + labelRes = R.string.ax_dynamic_bar_stopwatch, + ) + is IslandEvent.RingerMode -> EventStyle( + accent = when (event.mode) { + AudioManager.RINGER_MODE_SILENT -> RedAccent + AudioManager.RINGER_MODE_VIBRATE -> OrangeAccent + else -> BlueAccent + }, + icon = when (event.mode) { + AudioManager.RINGER_MODE_SILENT -> Icons.Filled.NotificationsOff + AudioManager.RINGER_MODE_VIBRATE -> Icons.Filled.Vibration + else -> Icons.Filled.NotificationsActive + }, + labelRes = when (event.mode) { + AudioManager.RINGER_MODE_SILENT -> R.string.ax_dynamic_bar_silent + AudioManager.RINGER_MODE_VIBRATE -> R.string.ax_dynamic_bar_vibrate + else -> R.string.ax_dynamic_bar_ring + }, + ) + is IslandEvent.Vpn -> EventStyle( + accent = IndigoAccent, + icon = Icons.Filled.VpnKey, + labelRes = if (event.isBranded) R.string.ax_dynamic_bar_vpn_active + else R.string.ax_dynamic_bar_vpn_connected, + ) + is IslandEvent.Clipboard -> EventStyle( + accent = IndigoAccent, + icon = Icons.Filled.ContentCopy, + labelRes = R.string.ax_dynamic_bar_copied, + ) + is IslandEvent.Torch -> EventStyle( + accent = YellowAccent, + icon = Icons.Filled.FlashlightOn, + labelRes = R.string.ax_dynamic_bar_flashlight, + ) + is IslandEvent.Notification -> EventStyle( + accent = BlueAccent, + icon = Icons.Filled.Notifications, + labelRes = R.string.ax_dynamic_bar_on, + ) + is IslandEvent.AppSwitch -> EventStyle( + accent = BlueAccent, + icon = null, + labelRes = R.string.ax_dynamic_bar_on, + ) + is IslandEvent.BiometricUnlock -> EventStyle( + accent = GreenAccent, + icon = Icons.Filled.Fingerprint, + labelRes = R.string.ax_dynamic_bar_unlocked, + ) + is IslandEvent.KeyguardIndication -> EventStyle( + accent = when (event.indicationType) { + IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC -> GreenAccent + IslandEvent.KeyguardIndication.IndicationType.ALIGNMENT -> OrangeAccent + else -> IndigoAccent + }, + icon = null, + labelRes = R.string.ax_dynamic_bar_on, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt new file mode 100644 index 000000000000..be4050894885 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -0,0 +1,206 @@ +package com.android.systemui.axdynamicbar.ui + +import android.os.SystemProperties +import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor +import com.android.systemui.statusbar.policy.BatteryController +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +data class AxDynamicBarChipState( + val event: IslandEvent, + val eventCount: Int, + val pinnedIndex: Int, + val allEvents: List, + val notificationAlert: IslandEvent.Notification? = null, +) + +data class KeyguardBatteryInfo( + val level: Int, + val isCharging: Boolean, + val isPowerSave: Boolean, + val isWireless: Boolean, + val timeRemaining: String?, +) + +@SysUISingleton +class AxDynamicBarChipViewModel +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + val interactor: AxDynamicBarInteractor, + batteryInteractor: BatteryInteractor, + private val batteryController: BatteryController, + authController: AuthController, + udfpsOverlayInteractor: UdfpsOverlayInteractor, +) { + + val isCompactKeyguardChip: StateFlow = + udfpsOverlayInteractor.udfpsOverlayParams + .map { params -> + val propOverride = SystemProperties.getInt(PROP_COMPACT_CHIP, -1) + if (propOverride >= 0) return@map propOverride == 1 + if (!authController.isUdfpsSupported) return@map false + val sensorBottom = params.sensorBounds.bottom + val displayHeight = params.naturalDisplayHeight + if (displayHeight <= 0) return@map false + sensorBottom > displayHeight * LOW_UDFPS_THRESHOLD + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + val chipState: StateFlow = + interactor.uiState + .map { uiState -> + if (!uiState.shouldShow) return@map null + val alert = uiState.notificationAlert + val topEvent = uiState.topEvent ?: alert ?: return@map null + AxDynamicBarChipState( + event = topEvent, + eventCount = uiState.activeEvents.size, + pinnedIndex = uiState.pinnedEventIndex, + allEvents = uiState.events, + notificationAlert = alert, + ) + } + .stateIn(applicationScope, SharingStarted.Lazily, null) + + val isEnabled: StateFlow = interactor.settings.isEnabled + val isKeyguardEnabled: StateFlow = interactor.settings.isKeyguardEnabled + + val keyguardBatteryInfo: StateFlow = + combine( + batteryInteractor.level, + batteryInteractor.isCharging, + batteryInteractor.powerSave, + batteryInteractor.batteryTimeRemainingEstimate, + ) { level, charging, powerSave, timeRemaining -> + KeyguardBatteryInfo( + level = level ?: 0, + isCharging = charging, + isPowerSave = powerSave, + isWireless = batteryController.isPluggedInWireless, + timeRemaining = timeRemaining, + ) + }.stateIn( + applicationScope, + SharingStarted.Lazily, + KeyguardBatteryInfo(0, false, false, false, null), + ) + + val isOnKeyguard: StateFlow = interactor.isOnKeyguard + + val isKeyguardFadingAway: StateFlow = interactor.isKeyguardFadingAway + + val isBouncerShowing: StateFlow = interactor.isBouncerShowing + + private val _keyguardCarrierText = MutableStateFlow("") + val keyguardCarrierText: StateFlow = _keyguardCarrierText.asStateFlow() + + fun updateKeyguardCarrierText(text: String) { + _keyguardCarrierText.value = text + } + + private val _chipCenterXFraction = MutableStateFlow(0.5f) + val chipCenterXFraction: StateFlow = _chipCenterXFraction.asStateFlow() + + fun updateChipCenterX(fraction: Float) { + _chipCenterXFraction.value = fraction + } + + private val _isExpanded = MutableStateFlow(false) + val isExpanded: StateFlow = _isExpanded.asStateFlow() + @Volatile private var collapseOnNullJob: Job? = null + + val isKeyguardExpanded: StateFlow = + combine(isExpanded, isOnKeyguard) { exp, kg -> exp && kg } + .stateIn(applicationScope, SharingStarted.Lazily, false) + + init { + + chipState.onEach { state -> + if (state == null) { + collapseOnNullJob?.cancel() + collapseOnNullJob = applicationScope.launch { + delay(200) + _isExpanded.value = false + } + } else { + collapseOnNullJob?.cancel() + collapseOnNullJob = null + } + }.launchIn(applicationScope) + + interactor.isPanelExpanded.onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) + interactor.qsExpansion.map { it > 0f }.distinctUntilChanged().onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) + + isBouncerShowing.onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) + + isOnKeyguard.onEach { if (!it) _isExpanded.value = false }.launchIn(applicationScope) + + combine(interactor.legacyShadeExpansion, isOnKeyguard) { expansion, onKg -> + onKg && expansion < 0.95f + }.onEach { dismissing -> if (dismissing) _isExpanded.value = false }.launchIn(applicationScope) + + interactor.isDozing.drop(1).onEach { _isExpanded.value = false }.launchIn(applicationScope) + + interactor.dozeAmount.map { it > 0f }.distinctUntilChanged().onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) + } + + fun expandPanel() { + if (chipState.value != null) _isExpanded.value = true + } + + fun collapsePanel() { + _isExpanded.value = false + } + + fun togglePanel() { + if (_isExpanded.value) collapsePanel() else expandPanel() + } + + fun cycleNext() = interactor.cycleNext() + + fun cyclePrev() = interactor.cyclePrev() + + fun dismissEvent(event: IslandEvent) = interactor.dismissEvent(event) + + fun togglePlayPause() = interactor.togglePlayPause() + + fun skipNext() = interactor.skipNext() + + fun skipPrev() = interactor.skipPrev() + + fun toggleTorch() = interactor.toggleTorch() + + fun stopScreenRecording() = interactor.stopScreenRecording() + + fun launchNotificationFromKeyguard(event: IslandEvent.Notification) { + interactor.launchNotificationDismissingKeyguard(event) + } + + companion object { + private const val PROP_COMPACT_CHIP = "persist.sys.axdb.compact_chip" + private const val LOW_UDFPS_THRESHOLD = 0.75f + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt new file mode 100644 index 000000000000..041df07145c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -0,0 +1,396 @@ +package com.android.systemui.axdynamicbar.ui + +import android.content.Context +import android.graphics.PixelFormat +import android.view.Gravity +import android.view.WindowManager +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.android.compose.theme.PlatformTheme +import com.android.systemui.shared.recents.utilities.Utilities +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.ExpandedMaxWidth +import com.android.systemui.axdynamicbar.ui.compose.ExpandedIslandContent +import com.android.systemui.axdynamicbar.ui.compose.NotificationAlertCard +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import android.os.Handler +import android.os.Looper +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import android.view.WindowInsets +import androidx.compose.ui.input.pointer.PointerEvent + +private const val EXIT_ANIM_DURATION = 300L + +@SysUISingleton +class AxDynamicBarExpandedPanel +@Inject +constructor( + @Application private val context: Context, + private val windowManager: WindowManager, + @Application private val applicationScope: CoroutineScope, + @Main private val mainHandler: Handler, + private val viewModel: AxDynamicBarChipViewModel, +) { + private var overlayView: ComposeView? = null + private var panelLifecycleOwner: PanelLifecycleOwner? = null + private var hideOverlayJob: Job? = null + + fun init() { + viewModel.interactor.onCollapseRequested = { viewModel.collapsePanel() } + viewModel.interactor.onFocusableRequested = { focusable -> setOverlayFocusable(focusable) } + + val needsOverlay = + combine( + viewModel.isExpanded, + viewModel.interactor.uiState.map { it.notificationAlert }, + viewModel.isOnKeyguard, + ) { expanded, alert, onKeyguard -> + !onKeyguard && (expanded || alert != null) + } + + needsOverlay + .onEach { needed -> + if (needed) { + hideOverlayJob?.cancel() + hideOverlayJob = null + showOverlay() + } else { + + hideOverlayJob?.cancel() + hideOverlayJob = + applicationScope.launch { + delay(400) + hideOverlay() + } + } + } + .launchIn(applicationScope) + + viewModel.isExpanded + .onEach { expanded -> + updateOverlayFocusability(expanded) + updateOverlaySize(expanded) + } + .launchIn(applicationScope) + } + + private fun ensureMainThread(action: () -> Unit) { + if (Looper.myLooper() == Looper.getMainLooper()) action() + else mainHandler.post(action) + } + + private fun showOverlay() { + ensureMainThread { showOverlayInternal() } + } + + private fun showOverlayInternal() { + if (overlayView != null) return + + val lifecycleOwner = PanelLifecycleOwner() + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START) + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + + val windowMetrics = windowManager.currentWindowMetrics + val statusBarTop = windowMetrics.windowInsets + .getInsets(WindowInsets.Type.statusBars()) + .top + + val view = + ComposeView(context).apply { + setContent { PlatformTheme { OverlayContent(viewModel, statusBarTop) } } + } + + view.setViewTreeLifecycleOwner(lifecycleOwner) + view.setViewTreeSavedStateRegistryOwner(lifecycleOwner) + panelLifecycleOwner = lifecycleOwner + + val isCurrentlyExpanded = viewModel.isExpanded.value + val flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + if (isCurrentlyExpanded) 0 + else WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + + val params = + WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + if (isCurrentlyExpanded) WindowManager.LayoutParams.MATCH_PARENT + else WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, + flags, + PixelFormat.TRANSLUCENT, + ) + params.gravity = Gravity.TOP or Gravity.FILL_HORIZONTAL + params.title = "AxDynamicBarExpanded" + + windowManager.addView(view, params) + overlayView = view + } + + private fun hideOverlay() { + ensureMainThread { hideOverlayInternal() } + } + + private fun hideOverlayInternal() { + shrinkRunnable?.let { mainHandler.removeCallbacks(it) } + shrinkRunnable = null + overlayView?.let { view -> + panelLifecycleOwner?.apply { + handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + handleLifecycleEvent(Lifecycle.Event.ON_STOP) + handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + windowManager.removeViewImmediate(view) + } + overlayView = null + panelLifecycleOwner = null + } + + private fun updateOverlayFocusability(expanded: Boolean) = ensureMainThread { + val view = overlayView ?: return@ensureMainThread + val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread + if (expanded) { + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() + } else { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } + windowManager.updateViewLayout(view, params) + } + + private var shrinkRunnable: Runnable? = null + + private fun updateOverlaySize(expanded: Boolean) = ensureMainThread { + shrinkRunnable?.let { mainHandler.removeCallbacks(it) } + shrinkRunnable = null + val view = overlayView ?: return@ensureMainThread + val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread + if (expanded) { + params.height = WindowManager.LayoutParams.MATCH_PARENT + windowManager.updateViewLayout(view, params) + } else { + val runnable = Runnable { + val v = overlayView ?: return@Runnable + val p = v.layoutParams as? WindowManager.LayoutParams ?: return@Runnable + p.height = WindowManager.LayoutParams.WRAP_CONTENT + windowManager.updateViewLayout(v, p) + } + shrinkRunnable = runnable + mainHandler.postDelayed(runnable, EXIT_ANIM_DURATION) + } + } + + private fun setOverlayFocusable(focusable: Boolean) = ensureMainThread { + val view = overlayView ?: return@ensureMainThread + val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread + if (focusable) { + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() + } else { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } + windowManager.updateViewLayout(view, params) + } +} + +@Composable +private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeightPx: Int) { + val density = LocalDensity.current + val isLargeScreen = Utilities.isLargeScreen(LocalContext.current) + + val topPad = if (isLargeScreen) { + with(density) { statusBarHeightPx.toDp() } + 4.dp + } else 0.dp + val chipState by viewModel.chipState.collectAsStateWithLifecycle() + val isExpanded by viewModel.isExpanded.collectAsStateWithLifecycle() + val uiState by viewModel.interactor.uiState.collectAsStateWithLifecycle() + val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() + val chipX by viewModel.chipCenterXFraction.collectAsStateWithLifecycle() + val notifAlert = uiState.notificationAlert + val compactNotifs by viewModel.interactor.settings.compactNotifications.collectAsStateWithLifecycle() + + val lastAlert = remember { mutableStateOf(null) } + if (notifAlert != null) lastAlert.value = notifAlert + + val expandedVisible = remember { MutableTransitionState(false) } + val showNotif = !isExpanded && notifAlert != null + val notifVisible = remember { MutableTransitionState(false) } + + LaunchedEffect(isExpanded) { + delay(16) + expandedVisible.targetState = isExpanded + } + LaunchedEffect(showNotif) { + delay(16) + notifVisible.targetState = showNotif + } + + val originX = chipX + val origin = TransformOrigin(originX, 0f) + + val chipAlignment = BiasAlignment( + horizontalBias = originX * 2f - 1f, + verticalBias = -1f, + ) + + AnimatedVisibility( + visibleState = expandedVisible, + enter = fadeIn(tween(250)) + scaleIn( + animationSpec = tween(350), + initialScale = 0.4f, + transformOrigin = origin, + ), + exit = fadeOut(tween(200)) + scaleOut( + animationSpec = tween(250), + targetScale = 0.4f, + transformOrigin = origin, + ), + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + val slop = viewConfiguration.touchSlop + awaitEachGesture { + + var ev: PointerEvent + do { + ev = awaitPointerEvent(PointerEventPass.Final) + } while (!ev.changes.any { it.changedToDownIgnoreConsumed() }) + val downPos = ev.changes[0].position + + val downConsumed = ev.changes[0].isConsumed + + while (true) { + val event = awaitPointerEvent(PointerEventPass.Final) + val change = event.changes.firstOrNull() ?: break + if (!change.pressed) { + if (!downConsumed && !change.isConsumed) { + val dx = change.position.x - downPos.x + val dy = change.position.y - downPos.y + if (dx * dx + dy * dy <= slop * slop) { + viewModel.collapsePanel() + } + } + break + } + } + } + } + .padding(top = topPad), + contentAlignment = chipAlignment, + ) { + chipState?.let { state -> + ExpandedIslandContent( + events = state.allEvents, + interactor = viewModel.interactor, + onCollapse = { viewModel.collapsePanel() }, + pinnedEventId = state.event.id, + hapticsViewModelFactory = viewModel.interactor.sliderHapticsViewModelFactory, + ) + } + } + } + + AnimatedVisibility( + visibleState = notifVisible, + enter = fadeIn(tween(300)) + scaleIn( + animationSpec = tween(300), + initialScale = 0.4f, + transformOrigin = origin, + ), + exit = fadeOut(tween(250)) + scaleOut( + animationSpec = tween(250), + targetScale = 0.4f, + transformOrigin = origin, + ), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = topPad), + contentAlignment = chipAlignment, + ) { + val alert = lastAlert.value + if (alert != null) { + NotificationAlertCard( + notification = alert, + interactor = viewModel.interactor, + onDismiss = { viewModel.interactor.dismissNotificationAlert() }, + initiallyCompact = compactNotifs, + modifier = + Modifier.widthIn(max = ExpandedMaxWidth) + .fillMaxWidth() + .padding(horizontal = 12.dp), + ) + } + } + } +} + +private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + private val savedStateRegistryController = SavedStateRegistryController.create(this) + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + override val savedStateRegistry + get() = savedStateRegistryController.savedStateRegistry + + init { + savedStateRegistryController.performRestore(null) + } + + fun handleLifecycleEvent(event: Lifecycle.Event) { + lifecycleRegistry.handleLifecycleEvent(event) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarManager.kt new file mode 100644 index 000000000000..810fe1b3f8cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarManager.kt @@ -0,0 +1,20 @@ +package com.android.systemui.axdynamicbar.ui + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class AxDynamicBarManager +@Inject +constructor( + private val viewModel: AxDynamicBarChipViewModel, + private val expandedPanel: AxDynamicBarExpandedPanel, +) : CoreStartable { + + override fun start() { + viewModel.interactor.init() + expandedPanel.init() + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt new file mode 100644 index 000000000000..b79221e2c0c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -0,0 +1,367 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import android.graphics.drawable.Drawable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.res.stringResource +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.AlphaIconBg +import com.android.systemui.axdynamicbar.shared.AlphaSecondary +import com.android.systemui.axdynamicbar.shared.AlphaTertiary +import com.android.systemui.axdynamicbar.shared.PillPrimary +import com.android.systemui.axdynamicbar.shared.ShapeXl +import com.android.systemui.axdynamicbar.shared.ShapeXs +import com.android.systemui.axdynamicbar.shared.SizeBadge +import com.android.systemui.axdynamicbar.shared.SpaceMd +import com.android.systemui.axdynamicbar.shared.SpaceSm +import com.android.systemui.axdynamicbar.shared.SpaceXs +import com.android.systemui.axdynamicbar.shared.TsBadge +import com.android.systemui.axdynamicbar.shared.chipAccentColorFor +import com.android.systemui.axdynamicbar.shared.chipContentColorOn +import com.android.systemui.axdynamicbar.shared.chipProgressFor +import com.android.systemui.axdynamicbar.shared.iconKeyFor +import com.android.systemui.axdynamicbar.shared.textKeyFor +import com.android.systemui.axdynamicbar.shared.toScaledBitmap +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel +import com.android.systemui.res.R +import kotlin.math.abs + +private val ChipShape = ShapeXl +private val ChipHeight = 24.dp + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AxDynamicBarChip( + viewModel: AxDynamicBarChipViewModel, + modifier: Modifier = Modifier, + ignoreKeyguard: Boolean = false, +) { + val state by viewModel.chipState.collectAsStateWithLifecycle() + val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() + val keyguardCarrier by viewModel.keyguardCarrierText.collectAsStateWithLifecycle() + + val carrierName = if (isOnKeyguard && ignoreKeyguard) keyguardCarrier.takeIf { it.isNotBlank() } else null + val screenWidthPx = with(LocalDensity.current) { + LocalConfiguration.current.screenWidthDp.dp.toPx() + } + + val touchSlop = LocalViewConfiguration.current.touchSlop + + val motionScheme = MaterialTheme.motionScheme + + AnimatedVisibility( + visible = state != null && (ignoreKeyguard || !isOnKeyguard), + enter = fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.8f, animationSpec = motionScheme.defaultSpatialSpec()), + exit = fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.8f, animationSpec = motionScheme.fastSpatialSpec()), + modifier = modifier + .pointerInput(viewModel) { + awaitEachGesture { + val down = awaitFirstDown(pass = PointerEventPass.Initial) + + val startX = down.position.x + val startY = down.position.y + var dragging = false + var totalDx = 0f + var decided = false + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val change = event.changes.firstOrNull() ?: break + if (!change.pressed) { + + if (dragging) { + change.consume() + if (totalDx > 0) viewModel.cyclePrev() + else viewModel.cycleNext() + } else if (!decided) { + + change.consume() + viewModel.togglePanel() + } + + break + } + val dx = change.position.x - startX + val dy = change.position.y - startY + if (!decided && (abs(dx) > touchSlop || abs(dy) > touchSlop)) { + if (abs(dx) >= abs(dy)) { + + decided = true + dragging = true + totalDx = dx + change.consume() + } else { + + decided = true + break + } + } else if (dragging) { + totalDx = dx + change.consume() + } + } + } + } + .onGloballyPositioned { coords -> + val bounds = coords.boundsInWindow() + val centerX = (bounds.left + bounds.right) / 2f + if (screenWidthPx > 0f) { + viewModel.updateChipCenterX(centerX / screenWidthPx) + } + }, + ) { + state?.let { chipState -> + val displayEvent = chipState.notificationAlert ?: chipState.event + val isAlert = chipState.notificationAlert != null + + AnimatedContent( + targetState = ChipDisplay(displayEvent, isAlert), + transitionSpec = { + ((fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.92f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.92f, animationSpec = motionScheme.fastSpatialSpec()))).using( + sizeTransform = null + ) + }, + contentKey = { if (it.isAlert) "alert" else it.event::class.simpleName }, + label = "chip_event", + ) { display -> + val rawAccent = chipAccentColorFor(display.event) + val accent by animateColorAsState(rawAccent, MaterialTheme.motionScheme.fastEffectsSpec(), label = "accent") + val contentColor by animateColorAsState( + chipContentColorOn(rawAccent), MaterialTheme.motionScheme.fastEffectsSpec(), label = "content", + ) + val rawProgress = chipProgressFor(display.event) + val progressTarget = rawProgress ?: 0f + val animatedProgress by animateFloatAsState( + progressTarget, MaterialTheme.motionScheme.defaultSpatialSpec(), label = "progress", + ) + val progress = if (rawProgress != null) animatedProgress else null + + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + Row( + modifier = + Modifier.height(ChipHeight) + .clip(ChipShape) + .background(accent) + .then( + if (progress != null) { + val trackColor = lerp(accent, contentColor, 0.2f) + val fillColor = lerp(accent, contentColor, 0.6f) + Modifier.drawWithContent { + drawContent() + val barH = 2.dp.toPx() + val y = size.height - barH + drawRect( + trackColor, + topLeft = Offset(0f, y), + size = Size(size.width, barH), + ) + drawRect( + fillColor, + topLeft = Offset(0f, y), + size = Size(size.width * progress, barH), + ) + } + } else Modifier + ) + .padding(start = SpaceSm, end = SpaceMd), + verticalAlignment = Alignment.CenterVertically, + ) { + if (carrierName != null) { + Text( + text = carrierName, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 56.dp), + ) + Text( + text = " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaTertiary), + ) + } + if (display.isAlert && display.event is IslandEvent.Notification) { + AnimatedContent( + targetState = display.event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) + }, + contentKey = { + (it as? IslandEvent.Notification)?.sbn?.key + }, + label = "alert_content", + ) { event -> + val notif = event as IslandEvent.Notification + Row(verticalAlignment = Alignment.CenterVertically) { + notif.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(16.dp), + contentDescription = null, + modifier = + Modifier.size(16.dp) + .clip(ShapeXs), + contentScale = ContentScale.Crop, + ) + Spacer(Modifier.width(SpaceXs)) + } + Text( + text = notif.appName ?: "", + style = PillPrimary, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 100.dp).basicMarquee(), + ) + } + } + } else if (display.event is IslandEvent.Sports && (display.event as IslandEvent.Sports).team2Name.isNotEmpty()) { + val sport = display.event as IslandEvent.Sports + StatusBarSportsTeamBadge(sport.team1Name, sport.team1Icon, contentColor) + Spacer(Modifier.width(SpaceXs)) + Text( + if (sport.score1.isNotEmpty()) "${sport.score1} - ${sport.score2}" + else stringResource(R.string.ax_dynamic_bar_sports_vs), + style = PillPrimary, + color = contentColor, + maxLines = 1, + ) + Spacer(Modifier.width(SpaceXs)) + StatusBarSportsTeamBadge(sport.team2Name, sport.team2Icon, contentColor) + } else { + AnimatedContent( + targetState = display.event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) + }, + contentKey = { iconKeyFor(it) }, + label = "chip_icon", + ) { event -> + PillEventIcon(event, tint = contentColor) + } + Spacer(Modifier.width(SpaceXs)) + AnimatedContent( + targetState = display.event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) + }, + contentKey = { textKeyFor(it) }, + label = "chip_text", + modifier = Modifier.weight(1f, fill = false), + ) { event -> + PillEventText( + event, + Modifier.widthIn(max = 100.dp), + overrideColor = contentColor, + ) + } + if (chipState.eventCount > 1) { + Spacer(Modifier.width(SpaceXs)) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .height(SizeBadge) + .widthIn(min = SizeBadge) + .background( + lerp(accent, contentColor, 0.3f), + RoundedCornerShape(SizeBadge / 2), + ) + .padding(horizontal = 3.dp), + ) { + Text( + text = "${chipState.eventCount}", + style = TsBadge, + color = contentColor, + maxLines = 1, + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun StatusBarSportsTeamBadge(name: String, icon: Drawable?, contentColor: Color) { + val badgeSize = 16.dp + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(badgeSize), + contentDescription = name, + modifier = Modifier.size(badgeSize).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = Modifier.size(badgeSize).clip(CircleShape) + .background(contentColor.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Text( + name.take(2).uppercase(), + style = TsBadge, + color = contentColor, + ) + } + } +} + +private data class ChipDisplay(val event: IslandEvent, val isAlert: Boolean) + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt new file mode 100644 index 000000000000..ac4a37158df5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -0,0 +1,861 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +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.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel +import com.android.systemui.axdynamicbar.ui.KeyguardBatteryInfo +import com.android.systemui.res.R +import kotlin.math.abs +import kotlinx.coroutines.delay +import android.content.Context +import android.graphics.drawable.Drawable +import java.util.Calendar + +private val ChipHeight = 36.dp +private val ChipHeightCompact = 28.dp +private val ChipShape = ShapeChip +private val ChipIconSize = ChipHeight - SpaceLg +private val ChipIconSizeCompact = ChipHeightCompact - SpaceMd +private val ActionSize = SpacePanel +private val ActionSizeCompact = SizeIconSm +private val ActionIconSize = SizeBadge +private val ActionIconSizeCompact = SpaceLg +private val BatteryIconSize = ChipHeight - SpaceXxl +private val BatteryIconSizeCompact = ChipHeightCompact - SpaceXxl +private val CountBadgeHeight = ChipHeight / 2 +private val CountBadgeHeightCompact = ChipHeightCompact / 2 + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AxDynamicBarKeyguardChip( + viewModel: AxDynamicBarChipViewModel, + modifier: Modifier = Modifier, +) { + val state by viewModel.chipState.collectAsStateWithLifecycle() + val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() + val isEnabled by viewModel.isEnabled.collectAsStateWithLifecycle() + val isKeyguardEnabled by viewModel.isKeyguardEnabled.collectAsStateWithLifecycle() + val batteryInfo by viewModel.keyguardBatteryInfo.collectAsStateWithLifecycle() + val isKeyguardExpanded by viewModel.isKeyguardExpanded.collectAsStateWithLifecycle() + val isCompact by viewModel.isCompactKeyguardChip.collectAsStateWithLifecycle() + val touchSlop = LocalViewConfiguration.current.touchSlop + + val chipHeight = if (isCompact) ChipHeightCompact else ChipHeight + + val motionScheme = MaterialTheme.motionScheme + + Box(modifier = modifier) { + + AnimatedVisibility( + visible = isKeyguardExpanded && state != null, + enter = fadeIn(motionScheme.defaultEffectsSpec()) + + slideInVertically( + animationSpec = motionScheme.defaultSpatialSpec(), + initialOffsetY = { it / 3 }, + ) + + expandVertically( + animationSpec = motionScheme.defaultSpatialSpec(), + expandFrom = Alignment.Bottom, + ), + exit = fadeOut(motionScheme.fastEffectsSpec()) + + slideOutVertically( + animationSpec = motionScheme.fastSpatialSpec(), + targetOffsetY = { it / 4 }, + ) + + shrinkVertically( + animationSpec = motionScheme.fastSpatialSpec(), + shrinkTowards = Alignment.Bottom, + ), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopStart) + .padding(bottom = chipHeight + SpaceLg), + ) { + state?.let { + KeyguardExpandedContent( + event = it.event, + allEvents = it.allEvents, + interactor = viewModel.interactor, + onCollapse = { viewModel.collapsePanel() }, + hapticsViewModelFactory = viewModel.interactor.sliderHapticsViewModelFactory, + ) + } + } + + AnimatedVisibility( + visible = isOnKeyguard && isEnabled && isKeyguardEnabled, + enter = fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.9f, animationSpec = motionScheme.defaultSpatialSpec()), + exit = fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.9f, animationSpec = motionScheme.fastSpatialSpec()), + modifier = Modifier + .align(Alignment.BottomCenter) + .pointerInput(viewModel) { + awaitEachGesture { + val down = awaitFirstDown(pass = PointerEventPass.Initial) + val startX = down.position.x + var dragging = false + var totalDx = 0f + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val change = event.changes.firstOrNull() ?: break + if (!change.pressed) { + if (dragging) { + change.consume() + if (totalDx > 0) viewModel.cyclePrev() + else viewModel.cycleNext() + } + break + } + val dx = change.position.x - startX + if (!dragging && abs(dx) > touchSlop) { + dragging = true + } + if (dragging) { + totalDx = dx + change.consume() + } + } + } + }, + ) { + val chipState = state + if (chipState != null) { + val displayEvent = chipState.notificationAlert ?: chipState.event + + AnimatedContent( + targetState = displayEvent, + transitionSpec = { + ((fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn( + initialScale = 0.95f, + animationSpec = motionScheme.defaultSpatialSpec(), + )) togetherWith (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut( + targetScale = 0.95f, + animationSpec = motionScheme.fastSpatialSpec(), + ))).using(sizeTransform = null) + }, + contentKey = { it::class.simpleName }, + label = "keyguard_chip_event", + ) { event -> + val rawAccent = chipAccentColorFor(event) + val accent by animateColorAsState( + rawAccent, + MaterialTheme.motionScheme.fastEffectsSpec(), + label = "kg_accent", + ) + val contentColor by animateColorAsState( + chipContentColorOn(rawAccent), + MaterialTheme.motionScheme.fastEffectsSpec(), + label = "kg_content", + ) + val rawProgress = chipProgressFor(event) + val progressTarget = rawProgress ?: 0f + val animatedProgress by animateFloatAsState( + progressTarget, + MaterialTheme.motionScheme.defaultSpatialSpec(), + label = "kg_progress", + ) + val progress = if (rawProgress != null) animatedProgress else null + + KeyguardChipBody( + event = event, + accent = accent, + contentColor = contentColor, + progress = progress, + eventCount = chipState.eventCount, + viewModel = viewModel, + compact = isCompact, + ) + } + } else { + + KeyguardBatteryChip(batteryInfo, isCompact) + } + } + } +} + +@Composable +private fun KeyguardChipBody( + event: IslandEvent, + accent: Color, + contentColor: Color, + progress: Float?, + eventCount: Int, + viewModel: AxDynamicBarChipViewModel, + compact: Boolean, +) { + val context = LocalContext.current + val height = if (compact) ChipHeightCompact else ChipHeight + val iconSize = if (compact) ChipIconSizeCompact else ChipIconSize + val actSize = if (compact) ActionSizeCompact else ActionSize + val actIconSize = if (compact) ActionIconSizeCompact else ActionIconSize + val badgeHeight = if (compact) CountBadgeHeightCompact else CountBadgeHeight + val textStyle = if (compact) MaterialTheme.typography.labelSmall else PillPrimary + val maxWidth = if (compact) 220.dp else 260.dp + val startPad = if (compact) SpaceXs else SpaceSm + val endPad = if (compact) SpaceSm else SpaceMd + + Box(contentAlignment = Alignment.Center) { + Row( + modifier = Modifier + .height(height) + .widthIn(max = maxWidth) + .clip(ChipShape) + .background(accent) + .then( + if (progress != null) { + val trackColor = lerp(accent, contentColor, 0.2f) + val fillColor = lerp(accent, contentColor, 0.6f) + Modifier.drawWithContent { + drawContent() + val barH = SizeStrokeWidth.toPx() + val y = size.height - barH + drawRect(trackColor, Offset(0f, y), Size(size.width, barH)) + drawRect(fillColor, Offset(0f, y), Size(size.width * progress, barH)) + } + } else Modifier + ) + .clickable { + when (event) { + is IslandEvent.Notification -> + viewModel.launchNotificationFromKeyguard(event) + + is IslandEvent.KeyguardIndication, + is IslandEvent.AppSwitch -> { } + else -> viewModel.togglePanel() + } + } + .padding(start = startPad, end = endPad), + verticalAlignment = Alignment.CenterVertically, + ) { + if (event is IslandEvent.Media) { + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(iconSize), + contentDescription = null, + modifier = Modifier + .size(iconSize) + .clip(ShapeXs), + contentScale = ContentScale.Crop, + ) + } ?: PillEventIcon(event, tint = contentColor) + Spacer(Modifier.width(SpaceXs)) + Box(modifier = Modifier.weight(1f, fill = false)) { + if (event.artist.isNotBlank()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, + style = textStyle, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = if (compact) 70.dp else 90.dp).basicMarquee(), + ) + Text( + " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaHint), + ) + Text( + event.artist, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = if (compact) 44.dp else 60.dp), + ) + } + } else { + MarqueeText(event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, contentColor, Modifier) + } + } + Spacer(Modifier.width(SpaceXs)) + Surface( + onClick = { viewModel.togglePlayPause() }, + modifier = Modifier.size(actSize), + shape = CircleShape, + color = lerp(accent, contentColor, AlphaSubtle), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(actSize)) { + Icon( + if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + contentDescription = stringResource( + if (event.isPlaying) R.string.ax_dynamic_bar_pause + else R.string.ax_dynamic_bar_play, + ), + tint = contentColor, + modifier = Modifier.size(actIconSize), + ) + } + } + } else if (event is IslandEvent.Sports && event.team2Name.isNotEmpty()) { + SportsChipTeamBadge(event.team1Name, event.team1Icon, contentColor) + Spacer(Modifier.width(SpaceXs)) + Text( + if (event.score1.isNotEmpty()) "${event.score1} - ${event.score2}" + else stringResource(R.string.ax_dynamic_bar_sports_vs), + style = textStyle, + color = contentColor, + maxLines = 1, + ) + Spacer(Modifier.width(SpaceXs)) + SportsChipTeamBadge(event.team2Name, event.team2Icon, contentColor) + } else { + PillEventIcon(event, tint = contentColor) + Spacer(Modifier.width(SpaceXs)) + KeyguardPrimaryText(event, contentColor, Modifier.weight(1f, fill = false)) + val secondary = secondaryTextFor(event) + if (secondary != null) { + Text( + " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaTertiary), + ) + Text( + secondary, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = if (compact) 60.dp else 80.dp), + ) + } + } + + if (event !is IslandEvent.Media) { + val actions = actionsFor(event) + if (actions.isNotEmpty()) { + Spacer(Modifier.width(SpaceXs)) + actions.forEach { action -> + Spacer(Modifier.width(SpaceXxs)) + ActionButton( + icon = action.icon, + color = contentColor, + bgColor = lerp(accent, contentColor, AlphaSubtle), + onClick = { action.perform(viewModel, event, context) }, + size = actSize, + iconSize = actIconSize, + ) + } + } + } + + if (eventCount > 1) { + Spacer(Modifier.width(SpaceXs)) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .height(badgeHeight) + .widthIn(min = badgeHeight) + .background(lerp(accent, contentColor, AlphaDisabled), ShapeChip) + .padding(horizontal = SpaceXxs), + ) { + Text( + "$eventCount", + style = TsBadge, + color = contentColor, + maxLines = 1, + ) + } + } + } + } +} + +@Composable +private fun KeyguardBatteryChip(info: KeyguardBatteryInfo, compact: Boolean) { + val accent = when { + info.isCharging -> BatteryChargingColor + info.isPowerSave -> BatteryPowerSaveColor + else -> BatteryNeutralColor + } + val contentColor = chipContentColorOn(accent) + val height = if (compact) ChipHeightCompact else ChipHeight + val battIconSize = if (compact) BatteryIconSizeCompact else BatteryIconSize + + Box(contentAlignment = Alignment.Center) { + Row( + modifier = Modifier + .height(height) + .clip(ChipShape) + .background(accent) + .padding(horizontal = if (compact) SpaceSm else SpaceMd), + verticalAlignment = Alignment.CenterVertically, + ) { + + if (info.isCharging) { + AnimatedChargingBoltIcon(contentColor, battIconSize) + } else { + AnimatedBatteryFillIcon(info.level, contentColor, battIconSize) + } + Spacer(Modifier.width(SpaceXs)) + Text( + "${info.level}%", + style = PillPrimary, + color = contentColor, + maxLines = 1, + ) + + val secondaryLabel = when { + info.isCharging && info.isWireless -> stringResource(R.string.ax_dynamic_bar_wireless) + info.isCharging -> stringResource(R.string.ax_dynamic_bar_charging) + info.isPowerSave -> stringResource(R.string.ax_dynamic_bar_saver) + else -> null + } + if (secondaryLabel != null) { + Text( + " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaTertiary), + ) + Text( + secondaryLabel, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + ) + } + + val timeRemaining = info.timeRemaining + if (timeRemaining != null) { + Text( + " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaTertiary), + ) + Text( + timeRemaining, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + ) + } + } + } +} + +@Composable +private fun AnimatedChargingBoltIcon(color: Color, iconSize: Dp = BatteryIconSize) { + val transition = rememberInfiniteTransition(label = "kg_bolt") + val glow by transition.animateFloat( + initialValue = 0.5f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + tween(800, easing = FastOutSlowInEasing), + RepeatMode.Reverse, + ), + label = "kg_bolt_glow", + ) + val boltPath = remember { Path() } + Canvas(modifier = Modifier.size(iconSize)) { + val w = size.width + val h = size.height + boltPath.rewind() + boltPath.moveTo(w * 0.55f, h * 0.05f) + boltPath.lineTo(w * 0.25f, h * 0.50f) + boltPath.lineTo(w * 0.45f, h * 0.50f) + boltPath.lineTo(w * 0.40f, h * 0.95f) + boltPath.lineTo(w * 0.75f, h * 0.42f) + boltPath.lineTo(w * 0.55f, h * 0.42f) + boltPath.close() + drawPath(boltPath, color.copy(alpha = glow)) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = BatteryIconSize) { + val fillFraction by animateFloatAsState( + targetValue = (level / 100f).coerceIn(0f, 1f), + animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + label = "kg_battery_fill", + ) + Canvas(modifier = Modifier.size(iconSize)) { + val w = size.width + val h = size.height + val bodyW = w * 0.7f + val bodyH = h * 0.85f + val bodyX = (w - bodyW) / 2f + val bodyY = h - bodyH + val tipW = bodyW * 0.4f + val tipH = h * 0.1f + + drawRect( + color.copy(alpha = AlphaTertiary), + topLeft = Offset((w - tipW) / 2f, 0f), + size = Size(tipW, tipH), + ) + + drawRect( + color.copy(alpha = AlphaTertiary), + topLeft = Offset(bodyX, bodyY), + size = Size(bodyW, bodyH), + ) + + val fillH = bodyH * fillFraction + drawRect( + color, + topLeft = Offset(bodyX, bodyY + bodyH - fillH), + size = Size(bodyW, fillH), + ) + } +} + +@Composable +private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modifier) { + when (event) { + is IslandEvent.ScreenRecording -> + if (event.isCountdown) MarqueeText(event.countdownSeconds.toString(), color, modifier) + else ElapsedTimeText(event.startTimeMs, color, modifier) + is IslandEvent.AudioRecording -> when (event.state) { + RecordingState.RECORDING -> ElapsedTimeText( + event.startTimeMs, color, modifier, event.pausedDurationMs, + ) + RecordingState.PAUSED -> MarqueeText(stringResource(R.string.ax_dynamic_bar_paused), color, modifier) + RecordingState.SAVED -> MarqueeText(stringResource(R.string.ax_dynamic_bar_saved), color, modifier) + } + is IslandEvent.Media -> MarqueeText(event.track, color, modifier) + is IslandEvent.Timer -> { + if (event.endTimeMs > 0L) CountdownText(event, color, modifier) + else MarqueeText(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_timer) }, color, modifier) + } + is IslandEvent.Stopwatch -> StopwatchTimeText(event, color, modifier) + is IslandEvent.Notification -> MarqueeText(event.title ?: event.appName, color, modifier) + is IslandEvent.Charging -> MarqueeText("${event.level}%", color, modifier) + is IslandEvent.Bluetooth -> MarqueeText(event.deviceName.take(12), color, modifier) + is IslandEvent.Hotspot -> { + val hotspotLabel = stringResource(R.string.ax_dynamic_bar_hotspot) + MarqueeText( + if (event.numDevices > 0) "$hotspotLabel · ${event.numDevices}" else hotspotLabel, + color, modifier, + ) + } + is IslandEvent.Alarm -> MarqueeText(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_alarm) }, color, modifier) + is IslandEvent.Casting -> MarqueeText(event.deviceName.take(12), color, modifier) + is IslandEvent.Torch -> MarqueeText( + if (event.supportsLevel) "${(event.level.toFloat() / event.maxLevel * 100).toInt()}%" + else stringResource(R.string.ax_dynamic_bar_flashlight), + color, modifier, + ) + is IslandEvent.RingerMode -> MarqueeText(event.label, color, modifier) + is IslandEvent.Vpn -> MarqueeText(stringResource(R.string.ax_dynamic_bar_vpn_active), color, modifier) + is IslandEvent.Clipboard -> MarqueeText( + event.preview.ifEmpty { stringResource(R.string.ax_dynamic_bar_copied) }, color, modifier, + ) + is IslandEvent.BiometricUnlock -> MarqueeText(stringResource(R.string.ax_dynamic_bar_unlocked), color, modifier) + is IslandEvent.AppSwitch -> MarqueeText(stringResource(R.string.ax_dynamic_bar_recents), color, modifier) + is IslandEvent.MicCamActive -> MarqueeText( + event.appName.ifEmpty { + buildString { + if (event.isCam) append(stringResource(R.string.ax_dynamic_bar_cam_short)) + if (event.isMic && event.isCam) append(" · ") + if (event.isMic) append(stringResource(R.string.ax_dynamic_bar_mic_short)) + } + }, + color, modifier, + ) + is IslandEvent.PromotedOngoing -> MarqueeText( + event.shortText.ifEmpty { event.title.ifEmpty { event.appName } }, color, modifier, + ) + is IslandEvent.Sports -> MarqueeText( + "${event.score1}-${event.score2}", color, modifier, + ) + is IslandEvent.NowPlaying -> MarqueeText( + "${event.songTitle} · ${event.artist}".trimEnd(' ', '·', ' '), color, modifier, + ) + is IslandEvent.KeyguardIndication -> MarqueeText(event.text, color, modifier) + } +} + +@Composable +private fun secondaryTextFor(event: IslandEvent): String? = when (event) { + is IslandEvent.Media -> event.artist.takeIf { it.isNotBlank() } + is IslandEvent.ScreenRecording -> + if (event.isCountdown) event.countdownSeconds.toString() + else stringResource(R.string.ax_dynamic_bar_rec_short) + is IslandEvent.AudioRecording -> event.appName.takeIf { it.isNotBlank() } + is IslandEvent.Timer -> event.label.takeIf { it.isNotBlank() } + is IslandEvent.Stopwatch -> event.label.takeIf { it.isNotBlank() } + is IslandEvent.Charging -> event.timeRemaining + is IslandEvent.Bluetooth -> if (event.batteryLevel >= 0) "${event.batteryLevel}%" else null + is IslandEvent.Hotspot -> if (event.numDevices > 0) stringResource(R.string.ax_dynamic_bar_hotspot_devices, event.numDevices) else null + is IslandEvent.Alarm -> { + if (event.triggerTimeMs > 0) { + val cal = Calendar.getInstance().apply { timeInMillis = event.triggerTimeMs } + "%d:%02d".format(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)) + } else null + } + is IslandEvent.Casting -> stringResource(R.string.ax_dynamic_bar_cast_short) + is IslandEvent.Vpn -> stringResource(R.string.ax_dynamic_bar_active) + is IslandEvent.BiometricUnlock -> event.sourceName + is IslandEvent.AppSwitch -> null + is IslandEvent.Notification -> event.text?.take(30) + is IslandEvent.PromotedOngoing -> event.text.takeIf { it.isNotBlank() }?.take(20) + is IslandEvent.Sports -> "${event.team1Name} ${stringResource(R.string.ax_dynamic_bar_sports_vs)} ${event.team2Name}" + is IslandEvent.KeyguardIndication -> when (event.indicationType) { + IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC -> stringResource(R.string.ax_dynamic_bar_biometric) + IslandEvent.KeyguardIndication.IndicationType.TRUST -> stringResource(R.string.ax_dynamic_bar_trust) + IslandEvent.KeyguardIndication.IndicationType.ALIGNMENT -> stringResource(R.string.ax_dynamic_bar_alignment) + else -> null + } + else -> null +} + +@Composable +private fun SportsChipTeamBadge(name: String, icon: Drawable?, contentColor: Color) { + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(ChipIconSize), + contentDescription = name, + modifier = Modifier.size(ChipIconSize).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = Modifier.size(ChipIconSize).clip(CircleShape) + .background(contentColor.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Text( + name.take(2).uppercase(), + style = TsBadge, + color = contentColor, + ) + } + } +} + +private enum class ActionIcon { PLAY, PAUSE, STOP, SKIP_PREV, SKIP_NEXT } + +private data class ChipAction( + val icon: ActionIcon, + val perform: (AxDynamicBarChipViewModel, IslandEvent, Context) -> Unit, +) + +private fun actionsFor(event: IslandEvent): List = when (event) { + is IslandEvent.Media -> listOf( + ChipAction(ActionIcon.SKIP_PREV) { vm, _, _ -> vm.skipPrev() }, + ChipAction(if (event.isPlaying) ActionIcon.PAUSE else ActionIcon.PLAY) { vm, _, _ -> + vm.togglePlayPause() + }, + ChipAction(ActionIcon.SKIP_NEXT) { vm, _, _ -> vm.skipNext() }, + ) + is IslandEvent.ScreenRecording -> + if (event.isCountdown) emptyList() + else listOf(ChipAction(ActionIcon.STOP) { vm, _, _ -> vm.stopScreenRecording() }) + is IslandEvent.AudioRecording -> { + val pauseResume = event.actions.firstOrNull() + listOfNotNull( + pauseResume?.let { action -> + ChipAction( + if (event.state == RecordingState.RECORDING) ActionIcon.PAUSE else ActionIcon.PLAY, + ) { _, _, ctx -> action.action.actionIntent?.sendWithBal(ctx) } + }, + ChipAction(ActionIcon.STOP) { vm, e, _ -> vm.dismissEvent(e) }, + ) + } + is IslandEvent.Timer -> { + val toggleAction = event.actions.firstOrNull() + listOfNotNull( + toggleAction?.let { action -> + ChipAction( + if (event.isPaused) ActionIcon.PLAY else ActionIcon.PAUSE, + ) { _, _, ctx -> action.action.actionIntent?.sendWithBal(ctx) } + }, + ) + } + is IslandEvent.Stopwatch -> { + val toggleAction = event.actions.firstOrNull() + listOfNotNull( + toggleAction?.let { action -> + ChipAction( + if (event.isRunning) ActionIcon.PAUSE else ActionIcon.PLAY, + ) { _, _, ctx -> action.action.actionIntent?.sendWithBal(ctx) } + }, + ) + } + is IslandEvent.Torch -> listOf( + ChipAction(ActionIcon.STOP) { vm, _, _ -> vm.toggleTorch() }, + ) + is IslandEvent.Casting -> listOf( + ChipAction(ActionIcon.STOP) { vm, e, _ -> vm.dismissEvent(e) }, + ) + else -> emptyList() +} + +@Composable +private fun ActionButton( + icon: ActionIcon, + color: Color, + bgColor: Color, + onClick: () -> Unit, + size: Dp = ActionSize, + iconSize: Dp = ActionIconSize, +) { + val imageVector = when (icon) { + ActionIcon.PLAY -> Icons.Filled.PlayArrow + ActionIcon.PAUSE -> Icons.Filled.Pause + ActionIcon.STOP -> Icons.Filled.Stop + ActionIcon.SKIP_PREV -> Icons.Filled.SkipPrevious + ActionIcon.SKIP_NEXT -> Icons.Filled.SkipNext + } + Surface( + onClick = onClick, + modifier = Modifier.size(size), + shape = CircleShape, + color = bgColor, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size)) { + Icon( + imageVector, + contentDescription = icon.name, + tint = color, + modifier = Modifier.size(iconSize), + ) + } + } +} + +@Composable +private fun MarqueeText(text: String, color: Color, modifier: Modifier) { + Text( + text, + color = color, + style = PillPrimary, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = modifier.widthIn(max = 120.dp).basicMarquee(), + ) +} + +@Composable +private fun ElapsedTimeText( + startTimeMs: Long, + color: Color, + modifier: Modifier, + pausedDurationMs: Long = 0L, +) { + var elapsedMs by remember(startTimeMs, pausedDurationMs) { + mutableLongStateOf( + (System.currentTimeMillis() - startTimeMs - pausedDurationMs).coerceAtLeast(0L) + ) + } + LaunchedEffect(startTimeMs, pausedDurationMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - startTimeMs - pausedDurationMs) + .coerceAtLeast(0L) + } + } + Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) +} + +@Composable +private fun CountdownText(event: IslandEvent.Timer, color: Color, modifier: Modifier) { + if (event.isPaused) { + Text(stringResource(R.string.ax_dynamic_bar_paused), color = color, style = PillMono, modifier = modifier) + } else { + var remainingMs by remember(event.endTimeMs) { + mutableLongStateOf((event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L)) + } + LaunchedEffect(event.endTimeMs) { + while (remainingMs > 0L) { + delay(500) + remainingMs = (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + } + } + Text(formatCountdownLong(remainingMs), color = color, style = PillMono, modifier = modifier) + } +} + +@Composable +private fun StopwatchTimeText(event: IslandEvent.Stopwatch, color: Color, modifier: Modifier) { + if (!event.isRunning) { + Text(stringResource(R.string.ax_dynamic_bar_paused), color = color, style = PillMono, modifier = modifier) + } else { + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(200) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + Text(formatStopwatch(elapsedMs), color = color, style = PillMono, modifier = modifier) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt new file mode 100644 index 000000000000..85c4a4c21a70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt @@ -0,0 +1,301 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.AlphaHint +import com.android.systemui.axdynamicbar.shared.AlphaTrack +import com.android.systemui.axdynamicbar.shared.PillPrimary +import com.android.systemui.axdynamicbar.shared.ShapeXl +import com.android.systemui.axdynamicbar.shared.SpaceSm +import com.android.systemui.axdynamicbar.shared.SpaceXs +import com.android.systemui.axdynamicbar.shared.chipAccentColorFor +import com.android.systemui.axdynamicbar.shared.chipContentColorOn +import com.android.systemui.axdynamicbar.shared.chipProgressFor +import com.android.systemui.axdynamicbar.shared.iconKeyFor +import com.android.systemui.axdynamicbar.shared.textKeyFor +import com.android.systemui.axdynamicbar.shared.toScaledBitmap +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipState +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel +import kotlin.math.abs + +private val NowBarShape = ShapeXl +private val NowBarHeight = 32.dp +private val NowBarMaxWidth = 200.dp + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AxDynamicBarNowBar( + state: AxDynamicBarChipState?, + viewModel: AxDynamicBarChipViewModel, +) { + val touchSlop = LocalViewConfiguration.current.touchSlop + val motionScheme = MaterialTheme.motionScheme + + AnimatedVisibility( + visible = state != null, + enter = slideInVertically(motionScheme.defaultSpatialSpec()) { -it } + fadeIn(motionScheme.defaultEffectsSpec()), + exit = slideOutVertically(motionScheme.fastSpatialSpec()) { -it } + fadeOut(motionScheme.fastEffectsSpec()), + ) { + state?.let { chipState -> + val displayEvent = chipState.notificationAlert ?: chipState.event + val isAlert = chipState.notificationAlert != null + + AnimatedContent( + targetState = NowBarDisplay(displayEvent, isAlert), + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.92f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.92f, animationSpec = motionScheme.fastSpatialSpec())) + }, + contentKey = { if (it.isAlert) "alert" else it.event::class.simpleName }, + label = "nowbar_event", + ) { display -> + var targetWidthPx by remember { mutableIntStateOf(0) } + val animatedWidthPx by animateIntAsState( + targetWidthPx, MaterialTheme.motionScheme.defaultSpatialSpec(), label = "nowbar_w", + ) + + val rawAccent = chipAccentColorFor(display.event) + val accent by animateColorAsState(rawAccent, MaterialTheme.motionScheme.fastEffectsSpec(), label = "accent") + val contentColor by animateColorAsState( + chipContentColorOn(rawAccent), MaterialTheme.motionScheme.fastEffectsSpec(), label = "content", + ) + val rawProgress = chipProgressFor(display.event) + val progressTarget = rawProgress ?: 0f + val animatedProgress by animateFloatAsState( + progressTarget, MaterialTheme.motionScheme.defaultSpatialSpec(), label = "progress", + ) + val progress = if (rawProgress != null) animatedProgress else null + + Box( + modifier = Modifier + .padding(top = SpaceXs) + .pointerInput(viewModel, touchSlop) { + awaitEachGesture { + awaitFirstDown(requireUnconsumed = false) + var totalDragX = 0f + var isDragging = false + + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull() ?: break + + if (!change.pressed) { + change.consume() + if (isDragging) { + if (totalDragX > 0f) viewModel.cyclePrev() + else viewModel.cycleNext() + } else { + viewModel.togglePanel() + } + break + } + + val delta = change.positionChange() + totalDragX += delta.x + + if (!isDragging && abs(totalDragX) > touchSlop) { + isDragging = true + } + + if (isDragging) { + change.consume() + } + } + } + }, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier + .height(NowBarHeight) + .widthIn(max = NowBarMaxWidth) + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + if (targetWidthPx != placeable.width) targetWidthPx = placeable.width + val w = if (animatedWidthPx > 0) animatedWidthPx else placeable.width + layout(w, placeable.height) { placeable.placeRelative(0, 0) } + } + .shadow(6.dp, NowBarShape) + .clip(NowBarShape) + .background(accent) + .then( + if (progress != null) { + Modifier.drawWithContent { + drawContent() + val barH = 3.dp.toPx() + val y = size.height - barH + drawRect( + contentColor.copy(alpha = AlphaTrack), + topLeft = Offset(0f, y), + size = Size(size.width, barH), + ) + drawRect( + contentColor.copy(alpha = 0.85f), + topLeft = Offset(0f, y), + size = Size(size.width * progress, barH), + ) + } + } else Modifier + ) + .padding(start = 10.dp, end = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (display.isAlert && display.event is IslandEvent.Notification) { + AlertPillContent(display.event, contentColor) + } else { + EventPillContent(display.event, contentColor, chipState) + } + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun AlertPillContent(event: IslandEvent, contentColor: Color) { + val motionScheme = MaterialTheme.motionScheme + val notif = event as IslandEvent.Notification + AnimatedContent( + targetState = notif, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) + }, + contentKey = { it.sbn.key }, + label = "alert_content", + ) { n -> + Row(verticalAlignment = Alignment.CenterVertically) { + n.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(18.dp), + contentDescription = null, + modifier = Modifier.size(18.dp).clip(RoundedCornerShape(5.dp)), + contentScale = ContentScale.Crop, + ) + Spacer(Modifier.width(SpaceSm)) + } + Text( + text = n.appName ?: "", + style = PillPrimary, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.basicMarquee(), + ) + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun EventPillContent( + event: IslandEvent, + contentColor: Color, + chipState: AxDynamicBarChipState, +) { + val motionScheme = MaterialTheme.motionScheme + + AnimatedContent( + targetState = event, + transitionSpec = { fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec()) }, + contentKey = { iconKeyFor(it) }, + label = "nowbar_icon", + ) { e -> + PillEventIcon(e, tint = contentColor) + } + Spacer(Modifier.width(SpaceSm)) + + AnimatedContent( + targetState = event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) + }, + contentKey = { textKeyFor(it) }, + label = "nowbar_text", + ) { e -> + PillEventText(e, Modifier, overrideColor = contentColor) + } + if (chipState.eventCount > 1) { + Spacer(Modifier.width(SpaceSm)) + NowBarDots( + count = chipState.eventCount, + activeIndex = chipState.pinnedIndex, + color = contentColor, + ) + } +} + +@Composable +private fun NowBarDots(count: Int, activeIndex: Int, color: Color) { + val dotCount = count.coerceAtMost(5) + Row( + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + for (i in 0 until dotCount) { + val active = i == activeIndex.coerceIn(0, dotCount - 1) % dotCount + Box( + Modifier + .size(if (active) 5.dp else 4.dp) + .clip(CircleShape) + .background(color.copy(alpha = if (active) 1f else AlphaHint)) + ) + } + } +} + +private data class NowBarDisplay(val event: IslandEvent, val isAlert: Boolean) + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAlertContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAlertContent.kt new file mode 100644 index 000000000000..323df1000156 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAlertContent.kt @@ -0,0 +1,108 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Alarm +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R +import android.text.format.DateFormat +import java.util.Date + +@Composable +internal fun AlarmExpanded(event: IslandEvent.Alarm, interactor: IslandActions) { + ExpandedCardLayout( + accentColor = OrangeAccent, + iconSize = SizeAlbumSm, + icon = { + if (event.isRinging) PulsingDot(color = OrangeAccent, size = 30.dp) + else Icon(Icons.Filled.Alarm, null, tint = OrangeAccent, modifier = Modifier.size(26.dp)) + }, + title = { + if (event.isRinging) StatusChip(stringResource(R.string.ax_dynamic_bar_ringing), OrangeAccent) + Text(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_alarm) }, color = OnCardText, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (event.triggerTimeMs > 0L) { + Text( + DateFormat.getTimeFormat(LocalContext.current) + .format(Date(event.triggerTimeMs)), + color = SubtleGray, + style = MaterialTheme.typography.labelMedium, + ) + } + }, + actions = { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Close, + color = OrangeAccent, + bg = OrangeAccent.copy(alpha = AlphaIconBg), + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.dismissEvent(event) }, + ) + }, + ) +} + +@Composable +internal fun RowScope.CompactAlarmRow( + event: IslandEvent.Alarm, + interactor: IslandActions, +) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(OrangeAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + if (event.isRinging) PulsingDot(color = OrangeAccent, size = SizeIconSm) + else Icon(Icons.Filled.Alarm, null, tint = OrangeAccent, modifier = Modifier.size(18.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_alarm) }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.isRinging) Text(stringResource(R.string.ax_dynamic_bar_ringing), color = OrangeAccent, style = MaterialTheme.typography.labelSmall) + } + if (event.isRinging) { + Spacer(Modifier.width(SpaceMd)) + Surface( + onClick = { interactor.dismissEvent(event) }, + shape = ShapeChip, + color = OrangeAccent.copy(alpha = AlphaIconBg), + ) { + Text( + stringResource(R.string.ax_dynamic_bar_dismiss), + color = OrangeAccent, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceSm), + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAppHistoryContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAppHistoryContent.kt new file mode 100644 index 000000000000..37f0eaf89244 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedAppHistoryContent.kt @@ -0,0 +1,140 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +@Composable +internal fun AppHistoryExpanded(event: IslandEvent.AppSwitch, interactor: IslandActions) { + if (event.recentApps.isEmpty()) return + + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(SpaceXxl)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeLg) + .background(BlueAccent.copy(alpha = AlphaFaint)) + .padding(SpaceXxl), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(R.string.ax_dynamic_bar_recent_apps), color = OnCardText, style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.ax_dynamic_bar_count_running, event.recentApps.size), color = SubtleGray, style = MaterialTheme.typography.labelMedium) + } + + val rows = event.recentApps.chunked(4) + rows.forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + row.forEach { app -> + AppGridItem( + app = app, + onClick = { + interactor.switchToApp(app.taskId) + interactor.collapseIsland() + }, + modifier = Modifier.weight(1f), + ) + } + + repeat(4 - row.size) { Spacer(Modifier.weight(1f)) } + } + } + } +} + +@Composable +private fun AppGridItem( + app: IslandEvent.RecentApp, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .clip(ShapeLg) + .clickable(onClick = onClick) + .background(BlueAccent.copy(alpha = AlphaFaint), ShapeLg), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + Spacer(Modifier.size(SpaceMd)) + app.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(48.dp), + contentDescription = app.appName, + modifier = Modifier.size(48.dp).clip(ShapeIconLarge), + contentScale = ContentScale.Crop, + ) + } + ?: Box( + modifier = Modifier.size(48.dp).clip(ShapeIconLarge).background(CardBg), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Apps, null, tint = SubtleGray, modifier = Modifier.size(24.dp)) + } + + Text( + app.appName, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.size(SpaceSm)) + } +} + +@Composable +internal fun RowScope.CompactAppSwitchRow(event: IslandEvent.AppSwitch) { + val lastApp = event.recentApps.firstOrNull() ?: return + lastApp.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeCompactIcon), + null, + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact), + contentScale = ContentScale.Crop, + ) + } + ?: Box( + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(CardBg), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Apps, null, tint = SubtleGray, modifier = Modifier.size(20.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text(stringResource(R.string.ax_dynamic_bar_recent_apps), color = OnCardText, style = MaterialTheme.typography.bodySmall) + Text(stringResource(R.string.ax_dynamic_bar_count_running, event.recentApps.size), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedClipboardContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedClipboardContent.kt new file mode 100644 index 000000000000..6ad068ce8be8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedClipboardContent.kt @@ -0,0 +1,294 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.DeleteSweep +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Photo +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +@Composable +internal fun ClipboardExpanded(event: IslandEvent.Clipboard, interactor: IslandActions) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(SpaceLg)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeLg) + .background(IndigoAccent.copy(alpha = AlphaFaint)) + .padding(SpaceXxl), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Filled.ContentCopy, null, tint = IndigoAccent, modifier = Modifier.size(22.dp)) + Spacer(Modifier.width(SpaceLg)) + Text( + stringResource(if (event.isImage) R.string.ax_dynamic_bar_image_copied else R.string.ax_dynamic_bar_copied), + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + ) + } + + if (event.items.isNotEmpty()) { + Column(verticalArrangement = Arrangement.spacedBy(SpaceMd)) { + event.items.forEach { item -> + ClipboardStashItem(item, interactor) + } + } + } else { + ClipboardSingleItem(event, interactor) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + if (event.items.isNotEmpty()) { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_clear_all), + icon = Icons.Filled.DeleteSweep, + color = IndigoAccent, + bg = IndigoAccent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { + event.items.forEach { interactor.removeClipboardItem(it.id) } + interactor.dismissEvent(event) + }, + ) + } + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Close, + color = IndigoAccent, + bg = IndigoAccent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { interactor.dismissEvent(event) }, + ) + } + } +} + +@Composable +private fun ClipboardStashItem( + item: IslandEvent.ClipboardItem, + interactor: IslandActions, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeSm) + .background(DarkCard) + .clickable { + if (item.isImage && item.imageUri != null) interactor.copyUriToClipboard(item.imageUri) + else interactor.copyToClipboard(item.preview) + } + .padding(SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (item.isImage) { + Box( + modifier = Modifier + .size(SizeCompactIcon) + .clip(ShapeCompact) + .background(IndigoAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Photo, null, tint = IndigoAccent, modifier = Modifier.size(18.dp)) + } + } else if (item.isUrl) { + Box( + modifier = Modifier + .size(SizeCompactIcon) + .clip(ShapeCompact) + .background(IndigoAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Link, null, tint = IndigoAccent, modifier = Modifier.size(18.dp)) + } + } + + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + if (item.isImage) { + Text( + stringResource(R.string.ax_dynamic_bar_image), + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + ) + if (item.preview.isNotEmpty()) { + Text( + item.preview, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } else { + Text( + item.preview, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Icon( + Icons.Filled.ContentCopy, + null, + tint = IndigoAccent, + modifier = Modifier + .size(SizeIconSm) + .clickable { + if (item.isImage && item.imageUri != null) interactor.copyUriToClipboard(item.imageUri) + else interactor.copyToClipboard(item.preview) + }, + ) + } +} + +@Composable +private fun ClipboardSingleItem(event: IslandEvent.Clipboard, interactor: IslandActions) { + if (event.preview.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeSm) + .background(DarkCard) + .clickable { + interactor.copyToClipboard(event.preview) + interactor.collapseIsland() + } + .padding(SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (event.isUrl) { + Box( + modifier = Modifier + .size(SizeCompactIcon) + .clip(ShapeCompact) + .background(IndigoAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Link, null, tint = IndigoAccent, modifier = Modifier.size(18.dp)) + } + } + Text( + event.preview, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Icon( + Icons.Filled.ContentCopy, + null, + tint = IndigoAccent, + modifier = Modifier + .size(SizeIconSm) + .wrapContentWidth() + .clickable { + interactor.copyToClipboard(event.preview) + interactor.collapseIsland() + }, + ) + } + } + + if (event.isUrl) { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_open), + icon = Icons.Filled.Link, + color = IndigoAccent, + bg = IndigoAccent.copy(alpha = AlphaIconBg), + modifier = Modifier.fillMaxWidth(), + onClick = { + interactor.openUrl(event.preview) + interactor.collapseIsland() + }, + ) + } +} + +@Composable +internal fun RowScope.CompactClipboardRow( + event: IslandEvent.Clipboard, + interactor: IslandActions, +) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(BlueAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.ContentCopy, null, tint = BlueAccent, modifier = Modifier.size(18.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Text( + event.preview.ifEmpty { stringResource(if (event.isImage) R.string.ax_dynamic_bar_image_copied else R.string.ax_dynamic_bar_copied) }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Spacer(Modifier.width(SpaceMd)) + Surface( + onClick = { interactor.dismissEvent(event) }, + shape = ShapeChip, + color = IndigoAccent.copy(alpha = AlphaIconBg), + ) { + Text( + stringResource(R.string.ax_dynamic_bar_dismiss), + color = IndigoAccent, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceSm), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt new file mode 100644 index 000000000000..dc26ba77697f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt @@ -0,0 +1,245 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import kotlinx.coroutines.delay +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +private val EXPANDED_BOTTOM_PAD = 110.dp + +@Composable +fun ExpandedIslandContent( + events: List, + interactor: IslandActions, + onCollapse: () -> Unit, + expandedFilter: String? = null, + pinnedEventId: String? = null, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) { + if (events.isEmpty()) return + + val filteredEvents = + remember(events, expandedFilter, pinnedEventId) { + if (expandedFilter != null) { + events.filter { + EVENT_TYPE_IDS[it::class.java] == expandedFilter + } + } else { + + val base = events.filter { it !is IslandEvent.Notification } + val pinned = base.find { it.id == pinnedEventId } + if (pinned != null) { + listOf(pinned) + base.filter { it.id != pinned.id } + } else { + base + } + } + } + + val notifIds = + remember(filteredEvents) { + filteredEvents.filterIsInstance().map { it.id } + } + LaunchedEffect(notifIds) { notifIds.forEach { interactor.onNotificationInteraction(it) } } + DisposableEffect(notifIds) { + onDispose { notifIds.forEach { interactor.onNotificationInteractionEnd(it) } } + } + + LaunchedEffect(filteredEvents) { + if (filteredEvents.isEmpty()) { + + delay(200) + onCollapse() + } + } + + if (filteredEvents.isEmpty()) return + + val isNotificationFilter = expandedFilter == "notification" + val notifGroups = + remember(filteredEvents, isNotificationFilter) { + if (!isNotificationFilter) return@remember emptyList() + filteredEvents + .filterIsInstance() + .groupBy { it.sbn.packageName } + .values + .toList() + } + + val expandedGroups = remember(expandedFilter) { mutableStateMapOf() } + + LazyColumn( + modifier = + Modifier.widthIn(max = ExpandedMaxWidth) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(SpaceMd), + contentPadding = + PaddingValues(start = SpaceLg, end = SpaceLg, top = SpaceXxs, bottom = EXPANDED_BOTTOM_PAD), + ) { + if (isNotificationFilter && notifGroups.isNotEmpty()) { + notifGroups.forEach { group -> + if (group.size == 1) { + val event = group.first() + item(key = event.id) { + MagneticSwipeToDismiss( + onDismiss = { interactor.dismissEvent(event) }, + modifier = Modifier.animateItem(), + ) { + PrimaryCard { NotificationExpanded(event, interactor) } + } + } + } else { + val pkg = group.first().sbn.packageName + val isExpanded = expandedGroups[pkg] == true + item(key = "group_$pkg") { + MagneticSwipeToDismiss( + onDismiss = { group.forEach { interactor.dismissEvent(it) } }, + modifier = Modifier.animateItem(), + ) { + PrimaryCard { + NotificationGroupCard( + notifications = group, + isExpanded = isExpanded, + onToggleExpand = { expandedGroups[pkg] = !isExpanded }, + interactor = interactor, + ) + } + } + } + } + } + } else { + items(filteredEvents, key = { it.id }) { event -> + MagneticSwipeToDismiss( + onDismiss = { interactor.dismissEvent(event) }, + modifier = Modifier.animateItem(), + ) { + if (event is IslandEvent.Media) { + MediaCard(event, interactor) + } else { + PrimaryCard { + AnimatedContent( + targetState = event, + transitionSpec = { + ((fadeIn(tween(180)) + + scaleIn( + initialScale = 0.95f, + animationSpec = tween(250), + )) togetherWith + (fadeOut(tween(120)) + + scaleOut( + targetScale = 0.95f, + animationSpec = tween(200), + ))).using(sizeTransform = null) + }, + contentKey = { it::class.simpleName + it.id }, + label = "expanded_card", + ) { animatedEvent -> + ExpandedEventContent(animatedEvent, interactor, hapticsViewModelFactory) + } + } + } + } + } + } + } +} + +@Composable +internal fun ExpandedEventContent( + event: IslandEvent, + interactor: IslandActions, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) { + when (event) { + is IslandEvent.ScreenRecording -> ScreenRecordExpanded(event, interactor) + is IslandEvent.MicCamActive -> MicCamExpanded(event) + is IslandEvent.AudioRecording -> AudioRecordingExpanded(event, interactor) + is IslandEvent.Casting -> CastingExpanded(event) + is IslandEvent.PromotedOngoing -> PromotedOngoingExpanded(event, interactor) + is IslandEvent.Sports -> SportsExpanded(event, interactor) + is IslandEvent.NowPlaying -> NowPlayingExpanded(event, interactor) + is IslandEvent.Media -> MediaExpanded(event, interactor) + is IslandEvent.Bluetooth -> BluetoothExpanded(event, interactor) + is IslandEvent.Hotspot -> HotspotExpanded(event) + is IslandEvent.Charging -> ChargingExpanded(event) + is IslandEvent.Alarm -> AlarmExpanded(event, interactor) + is IslandEvent.Timer -> TimerExpanded(event, interactor) + is IslandEvent.Stopwatch -> StopwatchExpanded(event, interactor) + is IslandEvent.RingerMode -> RingerModeExpanded(event, interactor) + is IslandEvent.Vpn -> VpnExpanded(event) + is IslandEvent.Clipboard -> ClipboardExpanded(event, interactor) + is IslandEvent.Notification -> NotificationExpanded(event, interactor) + is IslandEvent.AppSwitch -> AppHistoryExpanded(event, interactor) + is IslandEvent.Torch -> TorchExpanded(event, interactor, hapticsViewModelFactory) + is IslandEvent.BiometricUnlock -> BiometricUnlockExpanded(event) + is IslandEvent.KeyguardIndication -> {} + } +} + +@Composable +internal fun BiometricUnlockExpanded(event: IslandEvent.BiometricUnlock) { + ExpandedCardLayout( + accentColor = GreenAccent, + icon = { Icon(Icons.Filled.Check, null, tint = GreenAccent, modifier = Modifier.size(26.dp)) }, + title = { + Text(stringResource(R.string.ax_dynamic_bar_device_unlocked), color = OnCardText, style = MaterialTheme.typography.titleMedium) + Text(event.sourceName, color = SubtleGray, style = MaterialTheme.typography.labelMedium) + }, + ) +} + +@Composable +internal fun PrimaryCard(content: @Composable () -> Unit) { + Box( + modifier = + Modifier.fillMaxWidth() + .clip(ShapeCard) + .background(CardBg) + .border(1.dp, CardBorderBrush, ShapeCard) + .padding(SpaceXxl) + ) { + content() + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt new file mode 100644 index 000000000000..8cd763529ccc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -0,0 +1,504 @@ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +private val AlbumArtSize = 80.dp +private val PlayPauseSize = 56.dp +private val ControlButtonSize = 44.dp +private val ControlIconSize = 22.dp + +@Composable +internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { + val colors = rememberMediaColors(event) + val accent = colors.accent + + Surface( + modifier = Modifier.fillMaxWidth().border(1.dp, CardBorderBrush, ShapeCard), + shape = ShapeCard, + color = CardBg, + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + interactor.openMediaApp() + interactor.collapseIsland() + } + .padding(SpaceXxl), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXxl), + ) { + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(AlbumArtSize), + contentDescription = null, + modifier = Modifier.size(AlbumArtSize).clip(ShapeLg), + contentScale = ContentScale.Crop, + ) + } ?: Box( + modifier = Modifier + .size(AlbumArtSize) + .clip(ShapeLg) + .background(accent.copy(alpha = AlphaFaint)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(36.dp)) + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Text( + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (event.artist.isNotEmpty()) { + Text( + event.artist, + color = accent, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(OnCardText), + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(accent.copy(alpha = AlphaFaint)) + .padding(horizontal = SpaceXxl, vertical = SpaceLg), + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (event.duration > 0L) { + MediaSeekBar(event, interactor, accent) + } + MediaControls(event, interactor, accent) + } + } + } +} + +@Composable +internal fun MediaExpanded( + event: IslandEvent.Media, + interactor: IslandActions, + modifier: Modifier = Modifier, +) { + val colors = rememberMediaColors(event) + val accent = colors.accent + + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(SpaceXxl)) { + Row( + modifier = Modifier.fillMaxWidth().clickable { + interactor.openMediaApp() + interactor.collapseIsland() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXxl), + ) { + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(SizeAlbumSm), + contentDescription = null, + modifier = Modifier.size(SizeAlbumSm).clip(ShapeLg), + contentScale = ContentScale.Crop, + ) + } ?: Surface( + modifier = Modifier.size(SizeAlbumSm), + shape = ShapeLg, + color = accent.copy(alpha = AlphaSubtle), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + Icons.Filled.MusicNote, null, + tint = accent, + modifier = Modifier.size(SpacePanel), + ) + } + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Text( + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.artist.isNotEmpty()) { + Text( + event.artist, + color = accent, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(OnCardText), + ) + } + } + + MediaControls(event, interactor, accent) + if (event.duration > 0L) { + MediaSeekBar(event, interactor, accent) + } + } +} + +@Composable +private fun MediaControls( + event: IslandEvent.Media, + interactor: IslandActions, + accent: Color, +) { + val onAccent = chipContentColorOn(accent) + val tonalBg = accent.copy(alpha = AlphaSubtle) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + MediaCustomActionButton(event, interactor, accent, tonalBg) + + Surface( + onClick = { interactor.skipPrev() }, + shape = CircleShape, + color = tonalBg, + modifier = Modifier.size(ControlButtonSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + Icons.Filled.SkipPrevious, null, + tint = accent, + modifier = Modifier.size(ControlIconSize), + ) + } + } + + Surface( + onClick = { interactor.togglePlayPause() }, + shape = CircleShape, + color = accent, + modifier = Modifier.size(PlayPauseSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + if (event.isPlaying) + stringResource(R.string.ax_dynamic_bar_pause) + else + stringResource(R.string.ax_dynamic_bar_play), + tint = onAccent, + modifier = Modifier.size(26.dp), + ) + } + } + + Surface( + onClick = { interactor.skipNext() }, + shape = CircleShape, + color = tonalBg, + modifier = Modifier.size(ControlButtonSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + Icons.Filled.SkipNext, null, + tint = accent, + modifier = Modifier.size(ControlIconSize), + ) + } + } + + MediaEndActionButton(event, interactor, accent, tonalBg) + } +} + +@Composable +private fun MediaSeekBar( + event: IslandEvent.Media, + interactor: IslandActions, + accent: Color, +) { + val mediaProgress = rememberMediaProgress(event) + val clamped = mediaProgress.progress + var isSeeking by remember { mutableStateOf(false) } + var seekProgress by remember { mutableFloatStateOf(clamped) } + if (!isSeeking) seekProgress = clamped + val displayMs = + if (isSeeking) (seekProgress * event.duration).toLong() + else mediaProgress.positionMs + + Column(verticalArrangement = Arrangement.spacedBy(SpaceXs)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text(formatElapsedTime(event.duration), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(SizeSeekHeight) + .pointerInput("tap") { + detectTapGestures { offset -> + val fraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + seekProgress = fraction + interactor.seekTo((fraction * event.duration).toLong()) + } + } + .pointerInput("drag") { + detectHorizontalDragGestures( + onDragStart = { offset -> + isSeeking = true + seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + isSeeking = false + interactor.seekTo((seekProgress * event.duration).toLong()) + }, + onDragCancel = { isSeeking = false }, + onHorizontalDrag = { change, _ -> + seekProgress = + (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) + change.consume() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + LinearWavyProgressIndicator( + progress = { seekProgress }, + modifier = Modifier.fillMaxWidth(), + color = accent, + trackColor = accent.copy(alpha = AlphaSubtle), + ) + } + } +} + +@Composable +private fun MediaCustomActionButton( + event: IslandEvent.Media, + interactor: IslandActions, + accent: Color, + tonalBg: Color, +) { + if (event.customActions.isNotEmpty()) { + val ca = event.customActions.first() + Surface( + onClick = { interactor.sendCustomAction(ca.action) }, + shape = CircleShape, + color = tonalBg, + modifier = Modifier.size(ControlButtonSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + resolveCustomActionIcon(ca.label), ca.label, + tint = accent, + modifier = Modifier.size(ControlIconSize), + ) + } + } + } else { + Surface( + onClick = { }, + shape = CircleShape, + color = tonalBg.copy(alpha = AlphaSubtle), + modifier = Modifier.size(ControlButtonSize), + enabled = false, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + Icons.Filled.Shuffle, null, + tint = accent.copy(alpha = AlphaDisabled), + modifier = Modifier.size(ControlIconSize), + ) + } + } + } +} + +@Composable +private fun MediaEndActionButton( + event: IslandEvent.Media, + interactor: IslandActions, + accent: Color, + tonalBg: Color, +) { + if (event.customActions.size > 1) { + val ca = event.customActions[1] + Surface( + onClick = { interactor.sendCustomAction(ca.action) }, + shape = CircleShape, + color = tonalBg, + modifier = Modifier.size(ControlButtonSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + resolveEndActionIcon(ca.label), ca.label, + tint = accent, + modifier = Modifier.size(ControlIconSize), + ) + } + } + } else { + Surface( + onClick = { + interactor.openMediaOutputSwitcher() + interactor.collapseIsland() + }, + shape = CircleShape, + color = tonalBg, + modifier = Modifier.size(ControlButtonSize), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + Icons.Filled.VolumeUp, null, + tint = accent, + modifier = Modifier.size(ControlIconSize), + ) + } + } + } +} + +@Composable +internal fun RowScope.CompactMediaRow( + event: IslandEvent.Media, + interactor: IslandActions, +) { + event.albumArt?.let { + Image( + bitmap = it.toScaledBitmap(SizeCompactIcon), + null, + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact), + contentScale = ContentScale.Crop, + ) + } ?: run { + val accent = rememberMediaColors(event).accent + Box( + modifier = Modifier.size(SizeCompactIcon) + .clip(ShapeCompact) + .background(accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(20.dp)) + } + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.artist.isNotEmpty()) + Text( + event.artist, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(Modifier.width(SpaceMd)) + Surface( + onClick = { interactor.togglePlayPause() }, + shape = CircleShape, + color = ActionBg, + modifier = Modifier.size(36.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + null, + tint = OnActionText, + modifier = Modifier.size(18.dp), + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt new file mode 100644 index 000000000000..76e5fd88fe0c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt @@ -0,0 +1,718 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui.compose + +import android.app.Notification +import android.app.RemoteInput +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.service.notification.StatusBarNotification +import android.util.Log +import android.util.Size +import android.widget.FrameLayout +import android.widget.RemoteViews +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Icon +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +@Composable +internal fun NotificationExpanded( + event: IslandEvent.Notification, + interactor: IslandActions, +) { + val context = LocalContext.current + val accent = BlueAccent + val hasProgress = event.progress >= 0 || event.isProgressIndeterminate + val hasCustomContent = + event.sbn.isOngoing && + (event.sbn.notification.bigContentView != null || + event.sbn.notification.contentView != null) + + Column( + modifier = Modifier.fillMaxWidth().clickable { + try { event.sbn.notification?.contentIntent?.sendWithBal(context) } + catch (_: Exception) {} + interactor.collapseIsland() + }, + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + Row( + modifier = Modifier.fillMaxWidth() + .clip(ShapeLg) + .background(accent.copy(alpha = AlphaFaint)) + .padding(SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + NotifExpandedAvatar(event, SizeCompactIcon) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXxs), + ) { + if (event.isConversation && event.senderName != null) { + val subtitle = if (event.isGroupConversation && event.conversationTitle != null) + "${event.appName} · ${event.conversationTitle}" + else + "${event.appName} · ${event.senderName}" + Text( + subtitle, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + if (event.isGroupConversation) event.senderName + else event.title ?: event.senderName, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + if (event.appName.isNotEmpty()) { + Text( + event.appName, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + event.title ?: event.appName.ifEmpty { event.sbn.packageName.substringAfterLast('.') }, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + event.appIcon?.let { + Image( + bitmap = it.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeSm), + ) + } + } + + if (hasProgress || hasCustomContent) { + SbnRemoteViewContent( + event.sbn, + fallback = { + if (hasProgress) { + NotificationProgressFallback(event) + } else { + event.text?.let { + Text(it, color = SubtleGray, style = MaterialTheme.typography.bodySmall, maxLines = 4, overflow = TextOverflow.Ellipsis) + } + } + }, + ) + } else { + event.text?.let { + Text(it, color = SubtleGray, style = MaterialTheme.typography.bodySmall, maxLines = 4, overflow = TextOverflow.Ellipsis) + } + } + + event.notificationImage?.let { img -> + Image( + bitmap = img.toScaledBitmap(200.dp), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 200.dp) + .clip(ShapeSm), + contentScale = ContentScale.Crop, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Close, + color = accent, + bg = accent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { + interactor.onNotificationInteraction(event.id) + interactor.dismissEvent(event) + }, + ) + + event.actions + .filter { a -> + val l = a.label?.toString()?.lowercase() ?: "" + l != "collapse" && l != "expand" && l != "minimize" + } + .take(2) + .forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + color = OnActionText, + bg = ActionBg, + modifier = Modifier.weight(1f), + onClick = { + interactor.onNotificationInteraction(event.id) + try { notifAction.action.actionIntent?.sendWithBal(context) } + catch (_: Exception) {} + interactor.dismissEvent(event) + interactor.collapseIsland() + }, + ) + } + } + + event.replyAction?.let { reply -> + NotificationReplyField( + reply = reply, + sbn = event.sbn, + interactor = interactor, + eventId = event.id, + ) + } + } +} + +@Composable +private fun NotifExpandedAvatar(event: IslandEvent.Notification, size: Dp) { + val icon = event.senderIcon ?: event.appIcon + val isRound = event.isConversation && event.senderIcon != null + val hasBadge = event.isConversation && event.senderIcon != null && event.appIcon != null + + when { + hasBadge && icon != null -> BadgedContactIcon(icon, event.appIcon!!, size, SpaceXxl, true) + icon != null -> Image( + bitmap = icon.toScaledBitmap(size), + contentDescription = null, + modifier = Modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), + ) + else -> Box( + modifier = Modifier.size(size).clip(ShapeIconMedium).background(BlueAccent.copy(alpha = AlphaSubtle)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Notifications, null, tint = BlueAccent, modifier = Modifier.size(SizeIconSm)) + } + } +} + +private fun resolveRemoteViews(ctx: Context, sbn: StatusBarNotification): RemoteViews? { + + sbn.notification.bigContentView?.let { + return it + } + sbn.notification.contentView?.let { + return it + } + + return try { + val builder = Notification.Builder.recoverBuilder(ctx, sbn.notification) + builder.createBigContentView() ?: builder.createContentView() + } catch (_: Exception) { + null + } +} + +@Composable +internal fun SbnRemoteViewContent(sbn: StatusBarNotification, fallback: @Composable () -> Unit) { + var remoteViewFailed by remember(sbn.key) { mutableStateOf(false) } + + if (!remoteViewFailed) { + AndroidView( + factory = { factoryCtx -> + FrameLayout(factoryCtx).apply { + try { + val rv = resolveRemoteViews(factoryCtx, sbn) + val inflated = rv?.apply(factoryCtx, this) + if (inflated != null) { + addView( + inflated, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + } else { + remoteViewFailed = true + } + } catch (_: Exception) { + remoteViewFailed = true + } + } + }, + update = { container -> + try { + container.removeAllViews() + val rv = resolveRemoteViews(container.context, sbn) + val inflated = rv?.apply(container.context, container) + if (inflated != null) { + container.addView( + inflated, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + } + } catch (_: Exception) {} + }, + modifier = Modifier.fillMaxWidth().clip(ShapeIconMedium), + ) + } else { + fallback() + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun NotificationProgressFallback(event: IslandEvent.Notification) { + val fraction = + when { + event.isProgressIndeterminate -> null + event.progressMax > 0 -> event.progress.toFloat() / event.progressMax + else -> null + } + Column(verticalArrangement = Arrangement.spacedBy(SpaceXs)) { + event.text?.let { + Text( + it, + color = SubtleGray, + style = MaterialTheme.typography.labelMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Text(stringResource(R.string.ax_dynamic_bar_progress), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + if (fraction != null) { + Text("${(fraction * 100).toInt()}%", color = BlueAccent, style = MaterialTheme.typography.bodySmall) + } + } + if (fraction != null) { + LinearWavyProgressIndicator( + progress = { fraction.coerceIn(0f, 1f) }, + modifier = Modifier.fillMaxWidth(), + color = BlueAccent, + trackColor = BlueAccent.copy(alpha = AlphaSubtle), + ) + } else { + LinearWavyProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = BlueAccent, + trackColor = BlueAccent.copy(alpha = AlphaSubtle), + ) + } + } +} + +@Composable +private fun NotificationReplyField( + reply: IslandEvent.ReplyAction, + sbn: StatusBarNotification, + interactor: IslandActions, + eventId: String, +) { + var replyText by remember { mutableStateOf("") } + + Row( + modifier = + Modifier.fillMaxWidth() + .height(40.dp) + .clip(ShapeLg) + .background(DarkCard), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = replyText, + onValueChange = { replyText = it }, + modifier = + Modifier.weight(1f).padding(horizontal = SpaceLg).onFocusChanged { focusState -> + interactor.onFocusableRequested?.invoke(focusState.isFocused) + if (focusState.isFocused) { + interactor.onNotificationInteraction(eventId) + } + }, + textStyle = MaterialTheme.typography.bodySmall.copy(color = OnCardText), + singleLine = true, + cursorBrush = SolidColor(BlueAccent), + decorationBox = { inner -> + if (replyText.isEmpty()) { + Text(reply.label.toString(), color = SubtleGray, style = MaterialTheme.typography.bodySmall) + } + inner() + }, + ) + if (replyText.isNotEmpty()) { + val context = LocalContext.current + Icon( + Icons.AutoMirrored.Filled.Send, + null, + tint = BlueAccent, + modifier = + Modifier.size(32.dp) + .clip(CircleShape) + .clickable { + sendReply(context, reply, replyText, sbn) + replyText = "" + interactor.onFocusableRequested?.invoke(false) + } + .padding(SpaceSm), + ) + } + } +} + +private fun sendReply( + context: Context, + reply: IslandEvent.ReplyAction, + text: String, + sbn: StatusBarNotification, +) { + val intent = + Intent().apply { + val results = Bundle().apply { putCharSequence(reply.remoteInput.resultKey, text) } + RemoteInput.addResultsToIntent(arrayOf(reply.remoteInput), this, results) + } + try { + reply.action.actionIntent?.sendWithBal(context, intent) + } catch (e: Exception) { + Log.w("NotificationReply", "Failed to send reply", e) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +internal fun NotificationGroupCard( + notifications: List, + isExpanded: Boolean, + onToggleExpand: () -> Unit, + interactor: IslandActions, +) { + val first = notifications.first() + val accent = BlueAccent + + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeLg) + .background(accent.copy(alpha = AlphaFaint)) + .clickable { onToggleExpand() } + .padding(SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + NotifExpandedAvatar(first, SizeCompactIcon) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXxs), + ) { + Text( + first.appName.ifEmpty { first.sbn.packageName.substringAfterLast('.') }, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (!isExpanded) { + val latest = notifications.first() + Text( + buildString { + latest.senderName?.let { append("$it: ") } + append(latest.text ?: latest.title ?: "") + }, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Box( + modifier = Modifier + .clip(ShapeChip) + .background(accent.copy(alpha = AlphaSubtle)) + .padding(horizontal = SpaceMd, vertical = SpaceXs), + contentAlignment = Alignment.Center, + ) { + Text( + "${notifications.size}", + color = accent, + style = MaterialTheme.typography.labelSmall, + ) + } + + Icon( + if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + contentDescription = stringResource( + if (isExpanded) R.string.ax_dynamic_bar_show_less + else R.string.ax_dynamic_bar_show_more, + ), + tint = SubtleGray, + modifier = Modifier.size(SizeIconSm), + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(MaterialTheme.motionScheme.defaultSpatialSpec()) + fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec()), + exit = shrinkVertically(MaterialTheme.motionScheme.defaultSpatialSpec()) + fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = SpaceMd), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + notifications.forEach { event -> + GroupedNotificationRow( + event = event, + interactor = interactor, + onDismiss = { interactor.dismissEvent(event) }, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun GroupedNotificationRow( + event: IslandEvent.Notification, + interactor: IslandActions, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val accent = BlueAccent + var childExpanded by remember { mutableStateOf(false) } + + MagneticSwipeToDismiss(onDismiss = onDismiss) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeSm) + .background(DarkCard) + .padding(SpaceLg), + verticalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { childExpanded = !childExpanded }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + NotifExpandedAvatar(event, SpacePanel) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXxs), + ) { + val displayName = event.senderName + ?: event.title + ?: event.appName.ifEmpty { + event.sbn.packageName.substringAfterLast('.') + } + val nameWithGroup = if (event.isGroupConversation && event.conversationTitle != null && event.senderName != null) + "$displayName · ${event.conversationTitle}" else displayName + Text( + nameWithGroup, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val body = event.text ?: if (event.senderName != null) event.title else null + body?.let { + Text( + it, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = if (childExpanded) 4 else 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Icon( + if (childExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + contentDescription = null, + tint = SubtleGray, + modifier = Modifier.size(SizeIconSm), + ) + } + + AnimatedVisibility( + visible = childExpanded, + enter = expandVertically(MaterialTheme.motionScheme.defaultSpatialSpec()) + fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec()), + exit = shrinkVertically(MaterialTheme.motionScheme.defaultSpatialSpec()) + fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_open), + color = accent, + bg = accent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { + try { + event.sbn.notification?.contentIntent?.sendWithBal(context) + } catch (_: Exception) {} + interactor.collapseIsland() + }, + ) + + event.actions + .filter { a -> + val l = a.label?.toString()?.lowercase() ?: "" + l != "collapse" && l != "expand" && l != "minimize" + } + .take(2) + .forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + color = OnActionText, + bg = ActionBg, + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + interactor.collapseIsland() + }, + ) + } + } + } + + if (childExpanded) { + event.replyAction?.let { reply -> + NotificationReplyField( + reply = reply, + sbn = event.sbn, + interactor = interactor, + eventId = event.id, + ) + } + } + } + } +} + +@Composable +internal fun RowScope.CompactNotificationRow(event: IslandEvent.Notification) { + event.appIcon?.let { + Image( + bitmap = it.toScaledBitmap(SizeCompactIcon), + null, + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact), + ) + } + ?: Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(BlueAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Filled.Notifications, + null, + tint = BlueAccent, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.title ?: event.appName.ifEmpty { event.sbn.packageName.substringAfterLast('.') }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.text != null) + Text( + event.text, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNowPlayingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNowPlayingContent.kt new file mode 100644 index 000000000000..28f4ad5abc47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNowPlayingContent.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.ActionChip +import com.android.systemui.axdynamicbar.shared.ExpandedCardLayout +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.shared.MintAccent +import com.android.systemui.axdynamicbar.shared.OnActionText +import com.android.systemui.axdynamicbar.shared.OnCardSecondary +import com.android.systemui.axdynamicbar.shared.OnCardText +import com.android.systemui.axdynamicbar.shared.SpaceLg +import com.android.systemui.axdynamicbar.shared.sendWithBal +import com.android.systemui.res.R + +@Composable +internal fun NowPlayingExpanded(event: IslandEvent.NowPlaying, interactor: IslandActions) { + val context = LocalContext.current + ExpandedCardLayout( + accentColor = MintAccent, + icon = { + Icon(Icons.Filled.MusicNote, null, tint = MintAccent, modifier = Modifier.size(30.dp)) + }, + title = { + Text( + event.songTitle, + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (event.artist.isNotEmpty()) { + Text( + event.artist, + color = OnCardSecondary, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + trailing = { + Text( + stringResource(R.string.ax_dynamic_bar_now_playing), + color = MintAccent, + style = MaterialTheme.typography.labelSmall, + ) + }, + actions = if (event.actions.isNotEmpty()) { + { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + event.actions.forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + color = OnActionText, + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + interactor.collapseIsland() + }, + ) + } + } + } + } else null, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt new file mode 100644 index 000000000000..d889b3bc3ba9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt @@ -0,0 +1,284 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import android.app.Notification +import android.content.Context +import android.service.notification.StatusBarNotification +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.RemoteViews +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cast +import androidx.compose.material3.Icon +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import androidx.compose.ui.platform.LocalContext +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +private fun resolveRemoteViews(ctx: Context, notification: Notification): RemoteViews? { + notification.bigContentView?.let { + return it + } + notification.contentView?.let { + return it + } + try { + val builder = Notification.Builder.recoverBuilder(ctx, notification) + builder.createBigContentView()?.let { + return it + } + builder.createContentView()?.let { + return it + } + } catch (_: Exception) {} + return null +} + +@Composable +private fun SbnContentView(sbn: StatusBarNotification, fallback: @Composable () -> Unit) { + var failed by remember(sbn.key) { mutableStateOf(false) } + if (!failed) { + key(sbn.key) { + AndroidView( + factory = { ctx -> + FrameLayout(ctx).apply { + setBackgroundColor(android.graphics.Color.TRANSPARENT) + try { + val rv = resolveRemoteViews(ctx, sbn.notification) + val inflated = rv?.apply(ctx, this) + if (inflated != null) { + prepareForIsland(inflated) + addView( + inflated, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + } else { + failed = true + } + } catch (_: Exception) { + failed = true + } + } + }, + modifier = Modifier.fillMaxWidth().clip(ShapeIconMedium), + ) + } + } else { + fallback() + } +} + +private val COLLAPSE_CHIP_IDS = arrayOf("expand_button_touch_container", "expand_button") + +private fun prepareForIsland(root: View) { + val res = root.resources + + for (name in COLLAPSE_CHIP_IDS) { + val id = res.getIdentifier(name, "id", "android") + if (id != 0) root.findViewById(id)?.visibility = View.GONE + } + hideCollapseButtons(root) +} + +private val COLLAPSE_LABELS = setOf("collapse", "expand", "minimize") + +private fun hideCollapseButtons(view: View) { + if (view is Button) { + val text = view.text?.toString()?.lowercase() ?: "" + if (COLLAPSE_LABELS.any { text.contains(it) }) { + view.visibility = View.GONE + } + } + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + hideCollapseButtons(view.getChildAt(i)) + } + } +} + +@Composable +internal fun CastingExpanded(event: IslandEvent.Casting) { + val style = eventStyleFor(event) + ExpandedCardLayout( + accentColor = style.accent, + icon = { style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(28.dp)) } }, + title = { + Text(stringResource(style.labelRes), color = SubtleGray, style = MaterialTheme.typography.labelMedium) + Text(event.deviceName, color = OnCardText, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (!event.description.isNullOrEmpty()) { + Text(event.description, color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + }, + trailing = { PulsingDot(color = style.accent, size = SpaceMd) }, + ) +} + +@Composable +internal fun RowScope.CompactCastingRow(event: IslandEvent.Casting) { + val style = eventStyleFor(event) + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(20.dp)) } + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text(stringResource(style.labelRes), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text( + event.deviceName, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + PulsingDot(color = style.accent, size = 7.dp) +} + +@Composable +internal fun PromotedOngoingExpanded( + event: IslandEvent.PromotedOngoing, + interactor: IslandActions, +) { + val sbn = event.sbn + SbnContentView(sbn, fallback = { PromotedOngoingFallback(event, interactor) }) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun PromotedOngoingFallback( + event: IslandEvent.PromotedOngoing, + interactor: IslandActions, +) { + val context = LocalContext.current + ExpandedCardLayout( + accentColor = BlueAccent, + iconSize = SizeButtonLg, + iconBackground = false, + icon = { + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeButtonLg), + contentDescription = null, + modifier = Modifier.size(SizeButtonLg).clip(ShapeIconLarge), + contentScale = ContentScale.Crop, + ) + } ?: PulsingDot(color = BlueAccent, size = 10.dp) + }, + title = { + Text(event.title.ifEmpty { event.appName }, color = OnCardText, style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis) + if (event.text.isNotEmpty()) { + Text(event.text, color = SubtleGray, style = MaterialTheme.typography.bodySmall, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + if (event.shortText.isNotEmpty()) { + StatusChip(event.shortText, BlueAccent) + } + }, + actions = { + if (event.progress >= 0f || event.isIndeterminate) { + LinearWavyProgressIndicator( + progress = { if (event.isIndeterminate) 0f else event.progress }, + modifier = Modifier.fillMaxWidth(), + color = BlueAccent, + trackColor = BlueAccent.copy(alpha = 0.20f), + ) + } + val usableActions = + event.actions.filter { action -> + val label = action.label.toString().lowercase() + label != "collapse" && label != "expand" && label != "minimize" + } + if (usableActions.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + usableActions.take(2).forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + interactor.collapseIsland() + }, + ) + } + } + } + }, + ) +} + +@Composable +internal fun RowScope.CompactPromotedOngoingRow(event: IslandEvent.PromotedOngoing) { + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeCompactIcon), + contentDescription = null, + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact), + contentScale = ContentScale.Crop, + ) + } + ?: Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(BlueAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + PulsingDot(color = BlueAccent, size = SpaceMd) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.title.ifEmpty { event.appName }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.shortText.isNotEmpty()) { + Text(event.shortText, color = BlueAccent, style = MaterialTheme.typography.labelSmall) + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt new file mode 100644 index 000000000000..281b0a2ea71f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt @@ -0,0 +1,372 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.res.R +import androidx.compose.ui.platform.LocalContext +import com.android.systemui.axdynamicbar.shared.* +import kotlinx.coroutines.delay + +@Composable +internal fun ScreenRecordExpanded( + event: IslandEvent.ScreenRecording, + interactor: IslandActions, +) { + if (event.isCountdown) { + ScreenRecordCountdownExpanded(event) + return + } + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + ExpandedCardLayout( + accentColor = RedAccent, + icon = { PulsingDot(color = RedAccent, size = 14.dp, durationMs = 550, minAlpha = AlphaTrack) }, + title = { + Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(formatElapsedTime(elapsedMs), color = RedAccent, style = MaterialTheme.typography.headlineMedium) + }, + trailing = { StatusChip(stringResource(R.string.ax_dynamic_bar_recording), RedAccent) }, + actions = { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_stop_recording), + icon = Icons.Filled.Stop, + color = OnDestructiveText, + bg = DestructiveBg, + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.stopScreenRecording() }, + ) + }, + ) +} + +@Composable +private fun ScreenRecordCountdownExpanded(event: IslandEvent.ScreenRecording) { + ExpandedCardLayout( + accentColor = RedAccent, + icon = { Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(22.dp)) }, + title = { + Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(event.countdownSeconds.toString(), color = RedAccent, style = MaterialTheme.typography.headlineMedium) + }, + ) +} + +@Composable +internal fun AudioRecordingExpanded( + event: IslandEvent.AudioRecording, + interactor: IslandActions, +) { + val context = LocalContext.current + val style = eventStyleFor(event) + val isSaved = event.state == RecordingState.SAVED + + var elapsedMs by remember(event.startTimeMs, event.pausedDurationMs) { + mutableLongStateOf( + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs).coerceAtLeast(0L) + ) + } + LaunchedEffect(event.startTimeMs, event.state, event.pausedDurationMs) { + if (event.state == RecordingState.RECORDING) { + while (true) { + delay(1000) + elapsedMs = + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + } + } + } + + ExpandedCardLayout( + accentColor = style.accent, + icon = { + when (event.state) { + RecordingState.RECORDING -> PulsingDot(color = style.accent, size = 14.dp, durationMs = 550, minAlpha = AlphaTrack) + else -> style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(22.dp)) } + } + }, + title = { + Text(event.appName.ifEmpty { stringResource(style.labelRes) }, color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (!isSaved) { + Text(formatElapsedTime(elapsedMs), color = style.accent, style = MaterialTheme.typography.headlineMedium) + } else { + Text(stringResource(style.labelRes), color = style.accent, style = MaterialTheme.typography.titleSmall) + } + }, + trailing = { + when (event.state) { + RecordingState.RECORDING -> StatusChip(stringResource(R.string.ax_dynamic_bar_recording), style.accent) + RecordingState.PAUSED -> StatusChip(stringResource(R.string.ax_dynamic_bar_paused), SubtleGray) + RecordingState.SAVED -> {} + } + }, + actions = if (event.actions.isNotEmpty()) { + { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + event.actions.forEach { notifAction -> + val lower = notifAction.label.toString().lowercase() + val btnColor = + when { + lower.contains("stop") || lower.contains("delete") -> DestructiveBg + lower.contains("resume") || lower.contains("play") -> GreenAccent + else -> ActionBg + } + val textColor = + when { + lower.contains("stop") || lower.contains("delete") -> OnDestructiveText + lower.contains("resume") || lower.contains("play") -> chipContentColorOn(GreenAccent) + else -> OnActionText + } + val btnIcon = + when { + lower.contains("stop") || lower.contains("delete") -> Icons.Filled.Stop + lower.contains("resume") || lower.contains("play") -> Icons.Filled.PlayArrow + lower.contains("pause") -> Icons.Filled.Pause + else -> null + } + ActionChip( + label = notifAction.label.toString(), + icon = btnIcon, + color = textColor, + bg = btnColor, + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + interactor.collapseIsland() + }, + ) + } + } + } + } else null, + ) +} + +@Composable +internal fun MicCamExpanded(event: IslandEvent.MicCamActive) { + val accent = if (event.isCam) RedAccent else OrangeAccent + ExpandedCardLayout( + accentColor = accent, + icon = { + Row(horizontalArrangement = Arrangement.spacedBy(SpaceSm)) { + if (event.isCam) Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(28.dp)) + if (event.isMic) Icon(Icons.Filled.Mic, null, tint = OrangeAccent, modifier = Modifier.size(28.dp)) + } + }, + title = { + Text( + when { + event.isCam && event.isMic -> stringResource(R.string.ax_dynamic_bar_camera_mic) + event.isCam -> stringResource(R.string.ax_dynamic_bar_camera) + else -> stringResource(R.string.ax_dynamic_bar_microphone) + }, + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + ) + if (event.apps.size > 1) { + Row( + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + verticalAlignment = Alignment.CenterVertically, + ) { + event.apps.take(4).forEach { app -> + app.appIcon?.let { + Image( + bitmap = it.toScaledBitmap(28.dp), + contentDescription = app.appName, + modifier = Modifier.size(28.dp).clip(ShapeIconMedium), + ) + } + } + } + StatusChip(stringResource(R.string.ax_dynamic_bar_apps_count, event.apps.size), accent) + } else if (event.appName.isNotEmpty()) { + StatusChip(event.appName, accent) + } + }, + trailing = { PulsingDot(color = accent, size = SpaceMd) }, + ) +} + +@Composable +internal fun RowScope.CompactRecordingRow(event: IslandEvent.ScreenRecording) { + if (event.isCountdown) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(RedAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(16.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = OnCardText, style = MaterialTheme.typography.bodySmall) + Text(event.countdownSeconds.toString(), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } + return + } + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(RedAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + PulsingDot(color = RedAccent, size = 12.dp, durationMs = 550, minAlpha = AlphaTrack) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = OnCardText, style = MaterialTheme.typography.bodySmall) + Text(formatElapsedTime(elapsedMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } +} + +@Composable +internal fun RowScope.CompactAudioRecordingRow(event: IslandEvent.AudioRecording) { + val style = eventStyleFor(event) + var elapsedMs by remember(event.startTimeMs, event.pausedDurationMs) { + mutableLongStateOf( + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs).coerceAtLeast(0L) + ) + } + LaunchedEffect(event.startTimeMs, event.state, event.pausedDurationMs) { + if (event.state == RecordingState.RECORDING) { + while (true) { + delay(1000) + elapsedMs = + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + } + } + } + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + when (event.state) { + RecordingState.RECORDING -> PulsingDot(color = style.accent, size = 10.dp, durationMs = 550, minAlpha = AlphaTrack) + RecordingState.PAUSED -> + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(16.dp)) } + RecordingState.SAVED -> + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(16.dp)) } + } + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + when (event.state) { + RecordingState.RECORDING -> stringResource(R.string.ax_dynamic_bar_recording) + RecordingState.PAUSED -> stringResource(R.string.ax_dynamic_bar_paused) + RecordingState.SAVED -> stringResource(R.string.ax_dynamic_bar_saved) + }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + ) + when (event.state) { + RecordingState.RECORDING, RecordingState.PAUSED -> + Text(formatElapsedTime(elapsedMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + RecordingState.SAVED -> if (event.appName.isNotEmpty()) { + Text( + event.appName, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +internal fun RowScope.CompactMicCamRow(event: IslandEvent.MicCamActive) { + val color = if (event.isCam) RedAccent else OrangeAccent + Box( + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(color.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon( + if (event.isCam) Icons.Filled.Videocam else Icons.Filled.Mic, + null, + tint = color, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + when { + event.isCam && event.isMic -> stringResource(R.string.ax_dynamic_bar_camera_mic_short) + event.isCam -> stringResource(R.string.ax_dynamic_bar_camera) + else -> stringResource(R.string.ax_dynamic_bar_microphone) + }, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + ) + if (event.apps.size > 1) { + Text(stringResource(R.string.ax_dynamic_bar_apps_count, event.apps.size), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } else if (event.appName.isNotEmpty()) { + Text( + event.appName, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSportsContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSportsContent.kt new file mode 100644 index 000000000000..4b1743c131bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSportsContent.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui.compose + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +@Composable +internal fun SportsExpanded(event: IslandEvent.Sports, interactor: IslandActions) { + val accent = accentColorFor(event) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (event.league.isNotEmpty()) { + Text( + event.league, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + StatusBadge(event.status, accent) + + if (event.team2Name.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + TeamColumn(event.team1Name, event.team1Icon?.toScaledBitmap(48.dp)) + if (event.score1.isNotEmpty()) { + ScoreDisplay(event.score1, event.score2, accent) + } else { + Text( + stringResource(R.string.ax_dynamic_bar_sports_vs), + color = SubtleGray, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Light, + ) + } + TeamColumn(event.team2Name, event.team2Icon?.toScaledBitmap(48.dp)) + } + } else { + Text( + event.team1Name, + color = OnCardText, + style = MaterialTheme.typography.bodyMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + } + + if (event.statusDetail.isNotEmpty()) { + Text( + event.statusDetail, + color = OnCardSecondary, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (event.commentary.isNotEmpty()) { + Text( + event.commentary, + color = OnCardSecondary, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +internal fun RowScope.CompactSportsRow(event: IslandEvent.Sports) { + val accent = accentColorFor(event) + + CompactTeamBadge(event.team1Name, event.team1Icon, accent) + + Spacer(Modifier.width(SpaceSm)) + + if (event.team2Name.isNotEmpty()) { + if (event.score1.isNotEmpty()) { + Text( + "${event.score1} - ${event.score2}", + color = accent, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + ) + } else { + Text( + stringResource(R.string.ax_dynamic_bar_sports_vs), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + ) + } + + Spacer(Modifier.width(SpaceSm)) + + CompactTeamBadge(event.team2Name, event.team2Icon, accent) + } else { + Text( + event.team1Name, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + + Spacer(Modifier.width(SpaceSm)) + + StatusBadge(event.status, accent) +} + +@Composable +private fun CompactTeamBadge(name: String, icon: Drawable?, accent: Color) { + icon?.let { + Image( + bitmap = it.toScaledBitmap(SizeCompactIcon), + contentDescription = name, + modifier = Modifier.size(SizeCompactIcon).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } ?: Box( + modifier = Modifier.size(SizeCompactIcon).clip(CircleShape).background(accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Text( + name.take(3).uppercase(), + color = accent, + style = TsBadge, + ) + } +} + +@Composable +private fun TeamColumn( + name: String, + icon: ImageBitmap?, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpaceMd), + modifier = Modifier.width(80.dp), + ) { + if (icon != null) { + Image( + bitmap = icon, + contentDescription = name, + modifier = Modifier.size(48.dp).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = Modifier.size(48.dp).clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Text( + name.take(3).uppercase(), + color = OnCardText, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + ) + } + } + Text( + name, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun ScoreDisplay(score1: String, score2: String, accent: Color) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + Text( + score1, + color = OnCardText, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + Text( + "-", + color = SubtleGray, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Light, + ) + Text( + score2, + color = OnCardText, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + } +} + +@Composable +private fun StatusBadge(status: IslandEvent.GameStatus, accent: Color) { + val label = when (status) { + IslandEvent.GameStatus.LIVE -> stringResource(R.string.ax_dynamic_bar_sports_live) + IslandEvent.GameStatus.FINAL -> stringResource(R.string.ax_dynamic_bar_sports_final) + IslandEvent.GameStatus.HALFTIME -> stringResource(R.string.ax_dynamic_bar_sports_halftime) + IslandEvent.GameStatus.PRE_GAME -> stringResource(R.string.ax_dynamic_bar_sports_upcoming) + } + Surface( + shape = ShapeChip, + color = accent.copy(alpha = AlphaIconBg), + ) { + Row( + modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceXs), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + if (status == IslandEvent.GameStatus.LIVE) { + PulsingDot(color = accent, size = 6.dp) + } + Text(label, color = accent, style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.SemiBold) + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt new file mode 100644 index 000000000000..6c60e0c9c7d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt @@ -0,0 +1,376 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import android.media.AudioManager +import androidx.compose.animation.animateColorAsState +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.BluetoothDisabled +import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.Vibration +import androidx.compose.material.icons.filled.VpnKey +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R + +@Composable +internal fun ChargingExpanded(event: IslandEvent.Charging) { + ExpandedCardLayout( + accentColor = GreenAccent, + icon = { + Icon(Icons.Filled.BatteryChargingFull, null, tint = GreenAccent, modifier = Modifier.size(30.dp)) + }, + title = { + Text( + if (event.isWireless) stringResource(R.string.ax_dynamic_bar_wireless_charging) + else stringResource(R.string.ax_dynamic_bar_charging), + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + StatusChip("${event.level}%", GreenAccent) + if (event.isPowerSave) { + StatusChip(stringResource(R.string.ax_dynamic_bar_battery_saver), OrangeAccent) + } + } + }, + trailing = if (!event.timeRemaining.isNullOrEmpty()) { + { + Text( + "${event.timeRemaining} ${stringResource(R.string.ax_dynamic_bar_until_full)}", + color = SubtleGray, + style = MaterialTheme.typography.labelMedium, + ) + } + } else null, + ) +} + +@Composable +internal fun BluetoothExpanded(event: IslandEvent.Bluetooth, interactor: IslandActions) { + ExpandedCardLayout( + accentColor = BlueAccent, + icon = { + event.deviceIcon?.let { + Image( + bitmap = it.toScaledBitmap(30.dp), + contentDescription = event.deviceTypeLabel.ifEmpty { + stringResource(R.string.ax_dynamic_bar_bluetooth_device) + }, + modifier = Modifier.size(30.dp), + ) + } ?: Icon(Icons.Filled.Bluetooth, null, tint = BlueAccent, modifier = Modifier.size(28.dp)) + }, + title = { + Text(event.deviceName, color = OnCardText, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + StatusChip( + event.deviceTypeLabel.ifEmpty { stringResource(R.string.ax_dynamic_bar_connected) }, + BlueAccent, + ) + if (event.batteryLevel >= 0) { + val batteryColor = if (event.batteryLevel > 20) GreenAccent else RedAccent + StatusChip("${event.batteryLevel}%", batteryColor) + } + } + }, + trailing = { PulsingDot(color = BlueAccent, size = SpaceMd) }, + actions = { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_disconnect), + icon = Icons.Filled.BluetoothDisabled, + color = OnDestructiveText, + bg = DestructiveBg, + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.disconnectBluetooth(event.address) }, + ) + }, + ) +} + +@Composable +internal fun HotspotExpanded(event: IslandEvent.Hotspot) { + ExpandedCardLayout( + accentColor = OrangeAccent, + icon = { Icon(Icons.Filled.Wifi, null, tint = OrangeAccent, modifier = Modifier.size(SizeIconMd)) }, + title = { + Text(stringResource(R.string.ax_dynamic_bar_hotspot_active), color = OnCardText, style = MaterialTheme.typography.titleMedium) + StatusChip( + when (event.numDevices) { + 0 -> stringResource(R.string.ax_dynamic_bar_no_devices) + 1 -> stringResource(R.string.ax_dynamic_bar_one_device_connected) + else -> stringResource(R.string.ax_dynamic_bar_devices_connected, event.numDevices) + }, + OrangeAccent, + ) + }, + trailing = { PulsingDot(color = OrangeAccent, size = SpaceMd) }, + ) +} + +@Composable +internal fun RingerModeExpanded(event: IslandEvent.RingerMode, interactor: IslandActions) { + val style = eventStyleFor(event) + ExpandedCardLayout( + accentColor = style.accent, + icon = { style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(22.dp)) } }, + title = { + Text(stringResource(R.string.ax_dynamic_bar_sound_mode), color = OnCardText, style = MaterialTheme.typography.titleMedium) + StatusChip(stringResource(style.labelRes), style.accent) + }, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + RingerCard( + isSelected = event.mode == AudioManager.RINGER_MODE_NORMAL, + icon = Icons.Filled.NotificationsActive, + label = stringResource(R.string.ax_dynamic_bar_ring), + accent = BlueAccent, + onClick = { interactor.setRingerMode(AudioManager.RINGER_MODE_NORMAL) }, + modifier = Modifier.weight(1f), + ) + RingerCard( + isSelected = event.mode == AudioManager.RINGER_MODE_VIBRATE, + icon = Icons.Filled.Vibration, + label = stringResource(R.string.ax_dynamic_bar_vibrate), + accent = OrangeAccent, + onClick = { interactor.setRingerMode(AudioManager.RINGER_MODE_VIBRATE) }, + modifier = Modifier.weight(1f), + ) + RingerCard( + isSelected = event.mode == AudioManager.RINGER_MODE_SILENT, + icon = Icons.Filled.NotificationsOff, + label = stringResource(R.string.ax_dynamic_bar_silent), + accent = RedAccent, + onClick = { interactor.setRingerMode(AudioManager.RINGER_MODE_SILENT) }, + modifier = Modifier.weight(1f), + ) + } + }, + ) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun RingerCard( + isSelected: Boolean, + icon: ImageVector, + label: String, + accent: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val bg by + animateColorAsState( + targetValue = if (isSelected) accent.copy(alpha = AlphaIconBg) else OnCardText.copy(alpha = AlphaFaint), + animationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), + label = "ringer_bg", + ) + val tint by + animateColorAsState( + targetValue = if (isSelected) accent else SubtleGray, + animationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), + label = "ringer_tint", + ) + + Surface( + onClick = onClick, + modifier = modifier, + shape = ShapeLg, + color = bg, + ) { + Column( + modifier = Modifier.padding(vertical = SpaceSection), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + Icon(icon, null, tint = tint, modifier = Modifier.size(28.dp)) + Text(label, color = tint, style = MaterialTheme.typography.labelMedium) + } + } +} + +@Composable +internal fun VpnExpanded(event: IslandEvent.Vpn) { + val style = eventStyleFor(event) + ExpandedCardLayout( + accentColor = style.accent, + icon = { style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(28.dp)) } }, + title = { + Text( + stringResource(style.labelRes), + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + ) + StatusChip( + if (event.isValidated) stringResource(R.string.ax_dynamic_bar_secured) + else stringResource(R.string.ax_dynamic_bar_connecting), + if (event.isValidated) GreenAccent else OrangeAccent, + ) + }, + trailing = { + PulsingDot(color = if (event.isValidated) GreenAccent else OrangeAccent, size = SpaceMd) + }, + ) +} + +@Composable +internal fun RowScope.CompactBluetoothRow(event: IslandEvent.Bluetooth) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(BlueAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + event.deviceIcon?.let { + Image(bitmap = it.toScaledBitmap(SizeIconSm), null, modifier = Modifier.size(SizeIconSm)) + } ?: Icon(Icons.Filled.Bluetooth, null, tint = BlueAccent, modifier = Modifier.size(18.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.deviceName, + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + event.deviceTypeLabel.ifEmpty { stringResource(R.string.ax_dynamic_bar_connected) }, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) + } + if (event.batteryLevel >= 0) { + Spacer(Modifier.width(SpaceMd)) + Text( + "${event.batteryLevel}%", + color = if (event.batteryLevel > 20) GreenAccent else RedAccent, + style = MaterialTheme.typography.bodySmall, + ) + } +} + +@Composable +internal fun RowScope.CompactHotspotRow(event: IslandEvent.Hotspot) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(OrangeAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Wifi, null, tint = OrangeAccent, modifier = Modifier.size(18.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text(stringResource(R.string.ax_dynamic_bar_hotspot), color = OnCardText, style = MaterialTheme.typography.bodySmall) + Text( + when (event.numDevices) { + 0 -> stringResource(R.string.ax_dynamic_bar_no_devices) + 1 -> stringResource(R.string.ax_dynamic_bar_one_device) + else -> stringResource(R.string.ax_dynamic_bar_hotspot_devices, event.numDevices) + }, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) + } + PulsingDot(color = OrangeAccent, size = 7.dp) +} + +@Composable +internal fun RowScope.CompactChargingRow(event: IslandEvent.Charging) { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(GreenAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Filled.BatteryChargingFull, + null, + tint = GreenAccent, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + if (event.isWireless) stringResource(R.string.ax_dynamic_bar_wireless_charging) + else stringResource(R.string.ax_dynamic_bar_charging), + color = OnCardText, + style = MaterialTheme.typography.bodySmall, + ) + val saverLabel = if (event.isPowerSave) stringResource(R.string.ax_dynamic_bar_saver) else null + Text( + buildString { + append("${event.level}%") + saverLabel?.let { append(" · $it") } + }, + color = if (event.isPowerSave) OrangeAccent else GreenAccent, + style = MaterialTheme.typography.labelSmall, + ) + } +} + +@Composable +internal fun RowScope.CompactRingerRow(event: IslandEvent.RingerMode) { + val style = eventStyleFor(event) + Box( + modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaStatusChip)), + contentAlignment = Alignment.Center, + ) { + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(18.dp)) } + } + Spacer(Modifier.width(SpaceLg)) + Text(event.label, color = OnCardText, style = MaterialTheme.typography.bodySmall, modifier = Modifier.weight(1f)) +} + +@Composable +internal fun RowScope.CompactVpnRow() { + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(IndigoAccent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.VpnKey, null, tint = IndigoAccent, modifier = Modifier.size(18.dp)) + } + Spacer(Modifier.width(SpaceLg)) + Text(stringResource(R.string.ax_dynamic_bar_vpn_active), color = OnCardText, style = MaterialTheme.typography.bodySmall, modifier = Modifier.weight(1f)) + PulsingDot(color = GreenAccent, size = 7.dp) +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTimerContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTimerContent.kt new file mode 100644 index 000000000000..b14bbc77e46f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTimerContent.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R +import kotlinx.coroutines.delay + +@Composable +internal fun TimerExpanded(event: IslandEvent.Timer, interactor: IslandActions) { + val context = LocalContext.current + val style = eventStyleFor(event) + val totalMs = event.originalDurationMs.takeIf { it > 0L } ?: 1L + var remainingMs by + remember(event.endTimeMs) { + mutableLongStateOf( + if (event.endTimeMs > 0L) + (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + else 0L + ) + } + if (!event.isPaused) { + LaunchedEffect(event.endTimeMs) { + if (event.endTimeMs > 0L) { + while (remainingMs > 0L) { + delay(500) + remainingMs = (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + } + } + } + } + val progress = if (event.endTimeMs > 0L) remainingMs.toFloat() / totalMs else 0f + + ExpandedCardLayout( + accentColor = style.accent, + icon = { + Box(modifier = Modifier.size(44.dp), contentAlignment = Alignment.Center) { + Canvas(Modifier.size(44.dp)) { + drawArc( + color = style.accent.copy(alpha = AlphaSubtle), + startAngle = -90f, + sweepAngle = 360f, + useCenter = false, + style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round), + topLeft = Offset.Zero, + size = Size(size.width, size.height), + ) + drawArc( + color = style.accent, + startAngle = -90f, + sweepAngle = 360f * progress.coerceIn(0f, 1f), + useCenter = false, + style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round), + topLeft = Offset.Zero, + size = Size(size.width, size.height), + ) + } + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(22.dp)) } + } + }, + title = { + Text(event.label.ifEmpty { stringResource(style.labelRes) }, color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (event.endTimeMs > 0L) { + Text( + if (event.isPaused) stringResource(R.string.ax_dynamic_bar_paused) else formatCountdownLong(remainingMs), + color = if (event.isPaused) SubtleGray else style.accent, + style = MaterialTheme.typography.headlineMedium, + ) + } else { + Text( + stringResource(if (event.isPaused) R.string.ax_dynamic_bar_paused else R.string.ax_dynamic_bar_running), + color = if (event.isPaused) SubtleGray else style.accent, + style = MaterialTheme.typography.titleSmall, + ) + } + }, + trailing = { if (event.isPaused) StatusChip(stringResource(R.string.ax_dynamic_bar_paused), SubtleGray) }, + actions = { + if (event.actions.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + event.actions.forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + }, + ) + } + } + } else { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Close, + color = style.accent, + bg = style.accent.copy(alpha = AlphaIconBg), + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.dismissEvent(event) }, + ) + } + }, + ) +} + +@Composable +internal fun StopwatchExpanded(event: IslandEvent.Stopwatch, interactor: IslandActions) { + val context = LocalContext.current + val style = eventStyleFor(event) + var elapsedMs by + remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + if (event.isRunning) { + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(100) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + } + + ExpandedCardLayout( + accentColor = style.accent, + icon = { + if (event.isRunning) PulsingDot(color = style.accent, size = 22.dp) + else style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(22.dp)) } + }, + title = { + Text(event.label.ifEmpty { stringResource(style.labelRes) }, color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + if (event.isRunning) formatStopwatch(elapsedMs) else stringResource(R.string.ax_dynamic_bar_paused), + color = if (event.isRunning) style.accent else SubtleGray, + style = MaterialTheme.typography.headlineMedium, + ) + }, + trailing = { if (!event.isRunning) StatusChip(stringResource(R.string.ax_dynamic_bar_paused), SubtleGray) }, + actions = { + if (event.actions.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + event.actions.forEach { notifAction -> + ActionChip( + label = notifAction.label.toString(), + modifier = Modifier.weight(1f), + onClick = { + try { + notifAction.action.actionIntent?.sendWithBal(context) + } catch (_: Exception) {} + }, + ) + } + } + } else { + ActionChip( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Close, + color = style.accent, + bg = style.accent.copy(alpha = AlphaIconBg), + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.dismissEvent(event) }, + ) + } + }, + ) +} + +@Composable +internal fun RowScope.CompactTimerRow(event: IslandEvent.Timer) { + var remainingMs by + remember(event.endTimeMs) { + mutableLongStateOf( + if (event.endTimeMs > 0L) + (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + else 0L + ) + } + if (!event.isPaused) { + LaunchedEffect(event.endTimeMs) { + if (event.endTimeMs > 0L) { + while (remainingMs > 0L) { + delay(500) + remainingMs = (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + } + } + } + } + val style = eventStyleFor(event) + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(18.dp)) } + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + if (event.label.isNotEmpty()) + Text( + event.label, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (event.endTimeMs > 0L) { + Text( + if (event.isPaused) stringResource(R.string.ax_dynamic_bar_paused) else formatCountdownLong(remainingMs), + color = if (event.isPaused) SubtleGray else style.accent, + style = TsMono.copy(fontSize = 13.sp), + ) + } else { + Text(stringResource(if (event.isPaused) R.string.ax_dynamic_bar_paused else R.string.ax_dynamic_bar_running), color = style.accent, style = MaterialTheme.typography.labelSmall) + } + } +} + +@Composable +internal fun RowScope.CompactStopwatchRow(event: IslandEvent.Stopwatch) { + var elapsedMs by + remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + if (event.isRunning) { + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(200) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + } + val style = eventStyleFor(event) + Box( + modifier = + Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaIconBg)), + contentAlignment = Alignment.Center, + ) { + if (event.isRunning) PulsingDot(color = style.accent, size = 18.dp) + else style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(18.dp)) } + } + Spacer(Modifier.width(SpaceLg)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { + Text( + event.label.ifEmpty { stringResource(style.labelRes) }, + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + if (event.isRunning) formatStopwatch(elapsedMs) else stringResource(R.string.ax_dynamic_bar_paused), + color = if (event.isRunning) style.accent else SubtleGray, + style = TsMono.copy(fontSize = 13.sp), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTorchContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTorchContent.kt new file mode 100644 index 000000000000..347d5cd77b39 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedTorchContent.kt @@ -0,0 +1,138 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FlashlightOff +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.flashlight.ui.composable.VerticalFlashlightSlider +import com.android.systemui.res.R + +@Composable +internal fun RowScope.TorchPill(event: IslandEvent.Torch) { + val style = eventStyleFor(event) + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(15.dp)) } + Spacer(Modifier.width(SpaceSm)) + Text(stringResource(style.labelRes), color = OnCardText, style = PillPrimary) + if (event.supportsLevel) { + Spacer(Modifier.width(SpaceSm)) + val pct = (event.level.toFloat() / event.maxLevel * 100).toInt() + Text("$pct%", color = style.accent, style = PillAccent) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TorchExpanded( + event: IslandEvent.Torch, + interactor: IslandActions, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) { + var isDragging by remember { mutableStateOf(false) } + var localLevel by remember { mutableIntStateOf(event.level) } + LaunchedEffect(event.level) { if (!isDragging) localLevel = event.level } + val displayLevel = if (isDragging) localLevel else event.level + + val style = eventStyleFor(event) + ExpandedCardLayout( + accentColor = style.accent, + icon = { style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(28.dp)) } }, + title = { + Text(stringResource(style.labelRes), color = OnCardText, style = MaterialTheme.typography.titleMedium) + if (event.supportsLevel) { + val pct = (displayLevel.toFloat() / event.maxLevel * 100).toInt() + StatusChip("$pct%", style.accent) + } else { + StatusChip(stringResource(R.string.ax_dynamic_bar_on), style.accent) + } + }, + trailing = { + CircleButton( + color = RedAccent.copy(alpha = AlphaSubtle), + size = SizeButton, + onClick = { interactor.toggleTorch() }, + ) { + Icon(Icons.Filled.FlashlightOff, null, tint = RedAccent, modifier = Modifier.size(22.dp)) + } + }, + actions = if (event.supportsLevel) { + { + Box( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + contentAlignment = Alignment.Center, + ) { + VerticalFlashlightSlider( + valueRange = 1..event.maxLevel, + onValueChange = { + isDragging = true + localLevel = it + interactor.setTorchLevelTemporary(it) + }, + onValueChangeFinished = { + isDragging = false + interactor.setTorchLevel(it) + }, + isEnabled = true, + levelValue = displayLevel, + hapticsViewModelFactory = hapticsViewModelFactory, + colors = + SliderDefaults.colors( + thumbColor = style.accent, + activeTrackColor = style.accent, + ), + ) + } + } + } else null, + ) +} + +@Composable +internal fun RowScope.CompactTorchRow(event: IslandEvent.Torch) { + val style = eventStyleFor(event) + style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(SpaceXxl)) } + Spacer(Modifier.width(SpaceSm)) + Text( + stringResource(style.labelRes), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + modifier = Modifier.weight(1f), + ) + if (event.supportsLevel) { + val pct = (event.level.toFloat() / event.maxLevel * 100).toInt() + Text("$pct%", color = style.accent, style = PillAccent) + } else { + Text(stringResource(R.string.ax_dynamic_bar_on), color = style.accent, style = PillAccent) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt new file mode 100644 index 000000000000..9bd2679f3c36 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -0,0 +1,846 @@ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material.icons.filled.AvTimer +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import kotlin.math.cos +import kotlin.math.sin +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.res.R +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import kotlinx.coroutines.delay + +@Composable +internal fun KeyguardExpandedContent( + event: IslandEvent, + allEvents: List, + interactor: IslandActions, + onCollapse: () -> Unit, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) { + if (event is IslandEvent.KeyguardIndication || event is IslandEvent.AppSwitch) { + LaunchedEffect(Unit) { onCollapse() } + return + } + + Box( + modifier = Modifier + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onCollapse() }, + contentAlignment = Alignment.Center, + ) { + when (event) { + is IslandEvent.Media -> KeyguardMediaPanel(event, interactor) + is IslandEvent.Timer -> KeyguardTimerPanel(event, interactor) + is IslandEvent.Stopwatch -> KeyguardStopwatchPanel(event, interactor) + is IslandEvent.ScreenRecording -> KeyguardRecordingPanel(event, interactor) + is IslandEvent.AudioRecording -> KeyguardAudioRecordingPanel(event, interactor) + else -> KeyguardGenericPanel(event, interactor, hapticsViewModelFactory) + } + } +} + +@Composable +private fun ProgressRing( + progress: Float, + color: Color, + modifier: Modifier = Modifier, + trackColor: Color = color.copy(alpha = AlphaFaint), + strokeWidth: Dp = 10.dp, + handleRadius: Dp = 8.dp, +) { + Canvas(modifier = modifier) { + val stroke = strokeWidth.toPx() + val handle = handleRadius.toPx() + val pad = handle.coerceAtLeast(stroke / 2) + val arcSize = Size(size.width - pad * 2, size.height - pad * 2) + val topLeft = Offset(pad, pad) + drawArc( + color = trackColor, + startAngle = -90f, + sweepAngle = 360f, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = stroke, cap = StrokeCap.Round), + ) + if (progress > 0f) { + val sweep = 360f * progress + drawArc( + color = color, + startAngle = -90f, + sweepAngle = sweep, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = stroke, cap = StrokeCap.Round), + ) + val angleRad = Math.toRadians((-90.0 + sweep)).toFloat() + val cx = size.width / 2 + (arcSize.width / 2) * cos(angleRad) + val cy = size.height / 2 + (arcSize.height / 2) * sin(angleRad) + drawCircle(color = color, radius = handle, center = Offset(cx, cy)) + } + } +} + +@Composable +private fun KeyguardPanelSurface(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .widthIn(max = ExpandedMaxWidth) + .fillMaxWidth() + .padding(horizontal = SpaceSection) + .clip(ShapeXl) + .background(CardBg) + .border(1.dp, CardBorderBrush, ShapeXl) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {}, + ), + ) { + content() + } +} + +@Composable +private fun TonalBanner( + colors: IslandColorScheme, + content: @Composable () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(ShapeLg) + .background(colors.tonal) + .padding(horizontal = SpaceXxl, vertical = SpaceLg), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + content() + } +} + +@Composable +private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActions) { + val colors = rememberMediaColors(event) + + val eqBars = if (event.isPlaying) { + val eqTransition = rememberInfiniteTransition(label = "media_eq") + (0 until 4).map { i -> + eqTransition.animateFloat( + initialValue = 0.25f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + tween(500 + i * 100, easing = LinearEasing), + RepeatMode.Reverse, + initialStartOffset = StartOffset(i * 130), + ), + label = "meq_$i", + ) + } + } else { + null + } + + KeyguardPanelSurface { Column(modifier = Modifier.fillMaxWidth()) { + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(280.dp), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(280.dp) + .padding(SpaceLg) + .clip(ShapeLg), + contentScale = ContentScale.Crop, + ) + } ?: Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(SpaceLg) + .clip(ShapeLg) + .background(colors.tonal), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.MusicNote, null, tint = colors.accent.copy(AlphaDisabled), modifier = Modifier.size(72.dp)) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Text( + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, + color = OnCardText, + style = MaterialTheme.typography.titleLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (event.artist.isNotEmpty()) { + Text( + event.artist, + color = colors.accent, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + Spacer(Modifier.width(SpaceLg)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + Canvas(modifier = Modifier.width(SpaceSection).height(SpaceXxl)) { + val barW = 3.dp.toPx() + val gap = 2.dp.toPx() + val totalW = 4 * barW + 3 * gap + val startX = (size.width - totalW) / 2f + for (i in 0 until 4) { + val h = (eqBars?.get(i)?.value ?: 0.3f) * size.height + drawRoundRect( + color = colors.accent, + topLeft = Offset(startX + i * (barW + gap), size.height - h), + size = Size(barW, h), + cornerRadius = CornerRadius(barW / 2f), + ) + } + } + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(OnCardText), + ) + } + } + } + + if (event.outputDeviceName.isNotBlank()) { + Surface( + onClick = { + interactor.openMediaOutputSwitcher() + interactor.collapseIsland() + }, + shape = ShapeChip, + color = colors.tonal, + ) { + Row( + modifier = Modifier.padding(horizontal = SpaceXl, vertical = SpaceMd), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + ) { + Icon(Icons.Filled.VolumeUp, null, tint = SubtleGray, modifier = Modifier.size(SpaceXl)) + Text(event.outputDeviceName, color = SubtleGray, style = MaterialTheme.typography.labelSmall, maxLines = 1) + } + } + } + + if (event.duration > 0L) { + val mediaProgress = rememberMediaProgress(event) + var isSeeking by remember { mutableStateOf(false) } + var seekProgress by remember { mutableFloatStateOf(mediaProgress.progress) } + if (!isSeeking) seekProgress = mediaProgress.progress + val displayMs = + if (isSeeking) (seekProgress * event.duration).toLong() + else mediaProgress.positionMs + + Column(verticalArrangement = Arrangement.spacedBy(SpaceSm)) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(SpaceSection) + .pointerInput("tap") { + detectTapGestures { offset -> + val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + seekProgress = f + interactor.seekTo((f * event.duration).toLong()) + } + } + .pointerInput("drag") { + detectHorizontalDragGestures( + onDragStart = { offset -> + isSeeking = true + seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + isSeeking = false + interactor.seekTo((seekProgress * event.duration).toLong()) + }, + onDragCancel = { isSeeking = false }, + onHorizontalDrag = { change, _ -> + seekProgress = (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) + change.consume() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + LinearWavyProgressIndicator( + progress = { seekProgress }, + modifier = Modifier.fillMaxWidth(), + color = colors.accent, + trackColor = colors.accent.copy(alpha = AlphaSubtle), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text(formatElapsedTime(event.duration), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } + } + } + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(top = SpaceLg), + shape = ShapeLg, + color = colors.tonal, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = SpaceLg), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { + event.customActions.firstOrNull()?.let { + interactor.sendCustomAction(it.action) + } + }, + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.firstOrNull() + val caIcon = ca?.let { resolveCustomActionIcon(it.label) } ?: Icons.Filled.Shuffle + Icon(caIcon, ca?.label ?: stringResource(R.string.ax_dynamic_bar_shuffle), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } + + IconButton( + onClick = { interactor.skipPrev() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon(Icons.Filled.SkipPrevious, stringResource(R.string.ax_dynamic_bar_previous), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) + } + + Surface( + onClick = { interactor.togglePlayPause() }, + modifier = Modifier.size(SizeButtonXl), + shape = CircleShape, + color = colors.accent, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + if (event.isPlaying) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_play), + tint = colors.onAccent, + modifier = Modifier.size(32.dp), + ) + } + } + + IconButton( + onClick = { interactor.skipNext() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon(Icons.Filled.SkipNext, stringResource(R.string.ax_dynamic_bar_next), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) + } + + IconButton( + onClick = { + val ca = event.customActions.getOrNull(1) + if (ca != null) interactor.sendCustomAction(ca.action) + else { + interactor.openMediaOutputSwitcher() + interactor.collapseIsland() + } + }, + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.getOrNull(1) + val endIcon = ca?.let { resolveEndActionIcon(it.label) } ?: Icons.Filled.VolumeUp + Icon(endIcon, ca?.label ?: stringResource(R.string.ax_dynamic_bar_output), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } + } + } + } +} +} + +@Composable +private fun KeyguardTimerPanel(event: IslandEvent.Timer, interactor: IslandActions) { + val context = LocalContext.current + val colors = rememberIslandColors(event) + var remainingMs by remember(event.endTimeMs) { + mutableLongStateOf((event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L)) + } + if (!event.isPaused) { + LaunchedEffect(event.endTimeMs) { + while (remainingMs > 0L) { + delay(500) + remainingMs = (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + } + } + } + + val elapsedFraction = if (event.originalDurationMs > 0L) + (remainingMs.toFloat() / event.originalDurationMs).coerceIn(0f, 1f) + else 0f + + val pulseTransition = rememberInfiniteTransition(label = "timer_pulse") + val pulseScale by pulseTransition.animateFloat( + initialValue = 1f, targetValue = if (remainingMs < 10_000L) 1.06f else 1f, + animationSpec = infiniteRepeatable(tween(500), RepeatMode.Reverse), + label = "timer_pulse_scale", + ) + + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TonalBanner(colors) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(colors.accent.copy(alpha = AlphaSubtle)), + contentAlignment = Alignment.Center, + ) { + eventStyleFor(event).icon?.let { Icon(it, null, tint = colors.accent, modifier = Modifier.size(18.dp)) } + } + Text( + event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_timer) }.uppercase(), + color = colors.accent, + style = MaterialTheme.typography.labelMedium, + ) + } + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(SizeAlbumLg) + .graphicsLayer { scaleX = pulseScale; scaleY = pulseScale }, + ) { + ProgressRing( + progress = elapsedFraction, + color = colors.accent, + modifier = Modifier.fillMaxSize(), + ) + Text( + if (event.isPaused) stringResource(R.string.ax_dynamic_bar_paused) else formatCountdownLong(remainingMs), + color = OnCardText, + style = MaterialTheme.typography.displayMedium, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + val toggleAction = event.actions.firstOrNull() + if (toggleAction != null) { + ExpressivePillButton( + label = if (event.isPaused) stringResource(R.string.ax_dynamic_bar_resume) else stringResource(R.string.ax_dynamic_bar_pause), + icon = if (event.isPaused) Icons.Filled.PlayArrow else Icons.Filled.Pause, + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.weight(1f), + onClick = { toggleAction.action.actionIntent?.sendWithBal(context) }, + ) + } + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + icon = Icons.Filled.Stop, + contentColor = colors.accent, + backgroundColor = colors.tonal, + modifier = Modifier.weight(1f), + onClick = { interactor.dismissEvent(event) }, + ) + } + } +} +} + +@Composable +private fun KeyguardStopwatchPanel(event: IslandEvent.Stopwatch, interactor: IslandActions) { + val context = LocalContext.current + val colors = rememberIslandColors(event) + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + if (event.isRunning) { + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(200) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + } + + val secFraction = if (event.isRunning) (elapsedMs % 60000) / 60000f else 0f + + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TonalBanner(colors) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(colors.accent.copy(alpha = AlphaSubtle)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.AvTimer, null, tint = colors.accent, modifier = Modifier.size(18.dp)) + } + Text( + event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_stopwatch) }.uppercase(), + color = colors.accent, + style = MaterialTheme.typography.labelMedium, + ) + } + + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(SizeAlbumLg)) { + ProgressRing( + progress = secFraction, + color = colors.accent, + modifier = Modifier.fillMaxSize(), + ) + Text( + if (event.isRunning) formatStopwatch(elapsedMs) else stringResource(R.string.ax_dynamic_bar_paused), + color = OnCardText, + style = MaterialTheme.typography.displayMedium, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + val toggleAction = event.actions.firstOrNull() + if (toggleAction != null) { + ExpressivePillButton( + label = if (event.isRunning) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_resume), + icon = if (event.isRunning) Icons.Filled.Pause else Icons.Filled.PlayArrow, + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.weight(1f), + onClick = { toggleAction.action.actionIntent?.sendWithBal(context) }, + ) + } + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_reset), + icon = Icons.Filled.Stop, + contentColor = colors.accent, + backgroundColor = colors.tonal, + modifier = Modifier.weight(1f), + onClick = { interactor.dismissEvent(event) }, + ) + } + } +} +} + +@Composable +private fun KeyguardRecordingPanel(event: IslandEvent.ScreenRecording, interactor: IslandActions) { + val colors = rememberIslandColors(event) + + if (event.isCountdown) { + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TonalBanner(colors) { + Icon(Icons.Filled.Videocam, null, tint = colors.accent, modifier = Modifier.size(SizeIconSm)) + Text( + stringResource(R.string.ax_dynamic_bar_screen_recording).uppercase(), + color = colors.accent, + style = MaterialTheme.typography.labelMedium, + ) + } + + Text( + event.countdownSeconds.toString(), + color = OnCardText, + style = MaterialTheme.typography.displayLarge, + ) + + Text( + stringResource(R.string.ax_dynamic_bar_screen_recording), + color = SubtleGray, + style = MaterialTheme.typography.bodyMedium, + ) + } } + return + } + + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TonalBanner(colors) { + PulsingDot(color = colors.accent, size = SpaceMd) + Icon(Icons.Filled.Videocam, null, tint = colors.accent, modifier = Modifier.size(SizeIconSm)) + Text( + stringResource(R.string.ax_dynamic_bar_screen_recording).uppercase(), + color = colors.accent, + style = MaterialTheme.typography.labelMedium, + ) + } + + Text( + formatElapsedTime(elapsedMs), + color = OnCardText, + style = MaterialTheme.typography.displayLarge, + ) + + LinearWavyProgressIndicator( + modifier = Modifier.fillMaxWidth().clip(ShapeChip), + color = colors.accent, + trackColor = colors.accent.copy(alpha = AlphaFaint), + ) + + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_stop), + icon = Icons.Filled.Stop, + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.stopScreenRecording() }, + ) + } +} +} + +@Composable +private fun KeyguardAudioRecordingPanel(event: IslandEvent.AudioRecording, interactor: IslandActions) { + val context = LocalContext.current + val colors = rememberIslandColors(event) + var elapsedMs by remember { mutableLongStateOf(0L) } + LaunchedEffect(event.startTimeMs, event.state, event.pausedDurationMs) { + if (event.state == RecordingState.RECORDING) { + while (true) { + elapsedMs = (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + delay(1000) + } + } else { + elapsedMs = (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + } + } + + val isRecording = event.state == RecordingState.RECORDING + + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TonalBanner(colors) { + if (isRecording) PulsingDot(color = colors.accent, size = SpaceMd) + Icon(Icons.Filled.Mic, null, tint = colors.accent, modifier = Modifier.size(SizeIconSm)) + Text( + when (event.state) { + RecordingState.RECORDING -> stringResource(R.string.ax_dynamic_bar_recording) + RecordingState.PAUSED -> stringResource(R.string.ax_dynamic_bar_paused) + RecordingState.SAVED -> stringResource(R.string.ax_dynamic_bar_saved) + }.uppercase(), + color = colors.accent, + style = MaterialTheme.typography.labelMedium, + ) + } + + Text( + formatElapsedTime(elapsedMs), + color = OnCardText, + style = MaterialTheme.typography.displayLarge, + ) + + if (isRecording) { + LinearWavyProgressIndicator( + modifier = Modifier.fillMaxWidth().clip(ShapeChip), + color = colors.accent, + trackColor = colors.accent.copy(alpha = AlphaFaint), + ) + } else { + LinearWavyProgressIndicator( + progress = { 0f }, + modifier = Modifier.fillMaxWidth().clip(ShapeChip), + color = colors.accent.copy(alpha = AlphaDisabled), + trackColor = colors.accent.copy(alpha = AlphaFaint), + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + val pauseResume = event.actions.firstOrNull() + if (pauseResume != null) { + ExpressivePillButton( + label = if (isRecording) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_resume), + icon = if (isRecording) Icons.Filled.Pause else Icons.Filled.PlayArrow, + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.weight(1f), + onClick = { pauseResume.action.actionIntent?.sendWithBal(context) }, + ) + } + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_stop), + icon = Icons.Filled.Stop, + contentColor = colors.accent, + backgroundColor = colors.tonal, + modifier = Modifier.weight(1f), + onClick = { interactor.dismissEvent(event) }, + ) + } + } +} +} + +@Composable +private fun KeyguardGenericPanel( + event: IslandEvent, + interactor: IslandActions, + hapticsViewModelFactory: SliderHapticsViewModel.Factory, +) { + val colors = rememberIslandColors(event) + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + ) { + ExpandedEventContent(event, interactor, hapticsViewModelFactory) + + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.dismissEvent(event) }, + ) + } +} +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt new file mode 100644 index 000000000000..02046d1b4fdf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt @@ -0,0 +1,185 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.sign +import kotlinx.coroutines.launch +import androidx.compose.ui.input.pointer.PointerInputChange + +private val DETACH_THRESHOLD = 72.dp +private const val MAGNETIC_PULL = 0.5f +private val DISMISS_VELOCITY = 500.dp +private const val SNAP_STIFFNESS = 550f +private const val SNAP_DAMPING = 0.6f +private const val FLING_STIFFNESS = 300f +private const val VELOCITY_SMOOTHING = 0.4f + +@Composable +internal fun MagneticSwipeToDismiss( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + allowUpDismiss: Boolean = false, + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + val haptic = LocalHapticFeedback.current + val scope = rememberCoroutineScope() + val viewConfig = LocalViewConfiguration.current + + val detachPx = with(density) { DETACH_THRESHOLD.toPx() } + val dismissVelPx = with(density) { DISMISS_VELOCITY.toPx() } + + val offsetX = remember { Animatable(0f) } + val offsetY = remember { Animatable(0f) } + var detached by remember { mutableStateOf(false) } + var dismissed by remember { mutableStateOf(false) } + + if (dismissed) return + + val absOff = maxOf(abs(offsetX.value), abs(offsetY.value)) + val progress = (absOff / (detachPx * 4f)).coerceIn(0f, 1f) + + Box( + modifier = + modifier + .graphicsLayer { + translationX = offsetX.value + translationY = offsetY.value + alpha = 1f - progress * 0.5f + scaleX = 1f - progress * 0.04f + scaleY = 1f - progress * 0.04f + rotationZ = (offsetX.value / detachPx).coerceIn(-1f, 1f) * 2f + } + .pointerInput(allowUpDismiss) { + val touchSlop = viewConfig.touchSlop + + awaitEachGesture { + + val down: PointerInputChange + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val d = event.changes.firstOrNull { + it.changedToDownIgnoreConsumed() + } + if (d != null) { down = d; break } + } + + var axis = 0 + var velocity = 0f + var prevTime = 0L + detached = false + + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val change = event.changes.firstOrNull() ?: break + + if (!change.pressed) { + + if (axis != 0) change.consume() + if (axis == 0) break + + val offset = if (axis == 2) offsetY else offsetX + val vel = velocity + val off = offset.value + scope.launch { + val shouldDismiss = + abs(vel) >= dismissVelPx || + (detached && abs(off) > detachPx * 0.5f) + if (shouldDismiss) { + val target = when (axis) { + 2 -> -size.height * 1.5f + else -> { + val dir = if (abs(vel) > 100f) sign(vel) else sign(off) + dir * size.width * 1.5f + } + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + offset.animateTo( + target, + spring(dampingRatio = 1f, stiffness = FLING_STIFFNESS), + ) + dismissed = true + onDismiss() + } else { + if (detached) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + offset.animateTo(0f, spring(SNAP_DAMPING, SNAP_STIFFNESS)) + detached = false + } + } + break + } + + val dx = change.position.x - down.position.x + val dy = change.position.y - down.position.y + + if (axis == 0) { + if (abs(dx) > touchSlop || abs(dy) > touchSlop) { + axis = if (abs(dx) >= abs(dy)) { + 1 + } else if (allowUpDismiss && dy < 0) { + 2 + } else { + break + } + } else { + continue + } + } + + change.consume() + + val delta = change.position - change.previousPosition + val dragAmount = if (axis == 2) delta.y else delta.x + val offset = if (axis == 2) offsetY else offsetX + + val now = change.uptimeMillis + if (prevTime > 0L) { + val dt = (now - prevTime).coerceAtLeast(1) / 1000f + val instantVel = dragAmount / dt + velocity = + velocity * (1f - VELOCITY_SMOOTHING) + + instantVel * VELOCITY_SMOOTHING + } + prevTime = now + + if (!detached) { + val target = offset.value + dragAmount * MAGNETIC_PULL + if (abs(target) >= detachPx) { + detached = true + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + scope.launch { offset.snapTo(offset.value + dragAmount) } + } else { + scope.launch { offset.snapTo(target) } + } + } else { + scope.launch { offset.snapTo(offset.value + dragAmount) } + } + } + } + } + ) { + content() + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt new file mode 100644 index 000000000000..c6402a27731e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt @@ -0,0 +1,692 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import android.app.Notification +import android.app.PendingIntent +import android.app.RemoteInput +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.Log +import androidx.compose.animation.core.Animatable +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +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.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Reply +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.CallEnd +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.systemui.axdynamicbar.shared.IslandActions +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.shared.ActionBg +import com.android.systemui.axdynamicbar.shared.AlphaIconBg +import com.android.systemui.axdynamicbar.shared.AlphaSubtle +import com.android.systemui.axdynamicbar.shared.CardBg +import com.android.systemui.axdynamicbar.shared.CardBorderBrush +import com.android.systemui.axdynamicbar.shared.DarkCard +import com.android.systemui.axdynamicbar.shared.ExpressivePillButton +import com.android.systemui.axdynamicbar.shared.GreenAccent +import com.android.systemui.axdynamicbar.shared.OnActionText +import com.android.systemui.axdynamicbar.shared.OnCardText +import com.android.systemui.axdynamicbar.shared.RedAccent +import com.android.systemui.axdynamicbar.shared.ShapeCard +import com.android.systemui.axdynamicbar.shared.ShapeChip +import com.android.systemui.axdynamicbar.shared.ShapeIconMedium +import com.android.systemui.axdynamicbar.shared.ShapeSm +import com.android.systemui.axdynamicbar.shared.SizeCompactIcon +import com.android.systemui.axdynamicbar.shared.SizeIconSm +import com.android.systemui.axdynamicbar.shared.SpaceLg +import com.android.systemui.axdynamicbar.shared.SpaceMd +import com.android.systemui.axdynamicbar.shared.SpaceSm +import com.android.systemui.axdynamicbar.shared.SpaceXs +import com.android.systemui.axdynamicbar.shared.SpaceXxl +import com.android.systemui.axdynamicbar.shared.SubtleGray +import com.android.systemui.axdynamicbar.shared.chipAccentColorFor +import com.android.systemui.axdynamicbar.shared.chipContentColorOn +import com.android.systemui.axdynamicbar.shared.sendWithBal +import com.android.systemui.axdynamicbar.shared.toScaledBitmap + +private const val MAX_EXPANDED_ACTIONS = 3 +private val NotifCompactHeight = 56.dp +private val NotifCollapsedHeight = 164.dp +private val NotifExpandedHeight = 358.dp +private val ThumbnailSize = 44.dp + +private suspend fun PointerInputScope.detectInteractionGesture( + onStart: () -> Unit, + onEnd: () -> Unit, +) { + awaitEachGesture { + var ev: PointerEvent + do { + ev = awaitPointerEvent(PointerEventPass.Final) + } while (!ev.changes.any { it.changedToDownIgnoreConsumed() }) + onStart() + do { + ev = awaitPointerEvent(PointerEventPass.Final) + } while (ev.changes.any { it.pressed }) + onEnd() + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun NotificationAlertCard( + notification: IslandEvent.Notification, + interactor: IslandActions, + onDismiss: () -> Unit, + initiallyCompact: Boolean = false, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val extras = notification.sbn.notification?.extras + val isCall = extras?.let { + it.containsKey(Notification.EXTRA_ANSWER_INTENT) || + it.containsKey(Notification.EXTRA_DECLINE_INTENT) || + it.containsKey(Notification.EXTRA_HANG_UP_INTENT) + } ?: false + var isCompact by remember(notification.sbn.key) { mutableStateOf(initiallyCompact && !isCall) } + var showReply by remember { mutableStateOf(false) } + var isExpanded by remember { mutableStateOf(!isCompact) } + var isTextTruncated by remember { mutableStateOf(false) } + val title = notification.title + val body = notification.text + val hasTitleAndBody = title != null && title != notification.senderName && !body.isNullOrEmpty() + val hasExtraActions = notification.actions.size > 2 + val hasExpandableContent = !isCall && (isTextTruncated || hasTitleAndBody || hasExtraActions) + val accent = chipAccentColorFor(notification) + val density = LocalDensity.current + val compactHeightPx = with(density) { NotifCompactHeight.roundToPx() } + val minHeightPx = with(density) { NotifCollapsedHeight.roundToPx() } + val maxHeightPx = with(density) { NotifExpandedHeight.roundToPx() } + val effectiveMinPx = if (isCompact) compactHeightPx else minHeightPx + val effectiveMaxPx = if (isCompact) compactHeightPx else maxHeightPx + var measuredHeightPx by remember { mutableIntStateOf(effectiveMinPx) } + val heightAnim = remember { Animatable(effectiveMinPx.toFloat()) } + val motionScheme = MaterialTheme.motionScheme + val targetPx = measuredHeightPx.coerceIn(effectiveMinPx, effectiveMaxPx) + LaunchedEffect(targetPx) { + heightAnim.animateTo(targetPx.toFloat(), motionScheme.defaultSpatialSpec()) + } + + DisposableEffect(Unit) { + onDispose { interactor.onNotificationAlertInteractionEnd() } + } + + MagneticSwipeToDismiss( + onDismiss = { + if (showReply) interactor.onFocusableRequested?.invoke(false) + onDismiss() + }, + modifier = modifier, + allowUpDismiss = true, + ) { + Column( + modifier = Modifier.fillMaxWidth() + .layout { measurable, constraints -> + val placeable = measurable.measure( + constraints.copy(maxHeight = effectiveMaxPx), + ) + measuredHeightPx = placeable.height + val h = heightAnim.value.toInt().coerceIn(effectiveMinPx, effectiveMaxPx) + layout(placeable.width, h) { placeable.place(0, 0) } + } + .clipToBounds() + .pointerInput(Unit) { + detectInteractionGesture( + onStart = interactor::onNotificationAlertInteractionStart, + onEnd = interactor::onNotificationAlertInteractionEnd, + ) + } + .clip(ShapeCard) + .background(CardBg) + .border(1.dp, CardBorderBrush, ShapeCard), + ) { + if (isCompact) { + Surface( + onClick = { isCompact = false; isExpanded = true }, + color = Color.Transparent, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = SpaceXxl, vertical = SpaceMd), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + ) { + notification.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeSm), + ) + } + val label = notification.senderName + ?: notification.title + ?: notification.appName + Text( + label, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val preview = notification.text ?: notification.title ?: "" + if (preview.isNotEmpty() && preview != label) { + Text( + "· $preview", + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } else { + Spacer(Modifier.weight(1f)) + } + ExpandChip(Icons.Filled.ExpandMore, accent) { isCompact = false; isExpanded = true } + } + } + } else { + Surface( + onClick = { + if (!showReply) { + try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + if (!isCall) onDismiss() + } + }, + color = Color.Transparent, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(SpaceXxl), + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + ) { + notification.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeSm), + ) + } + Text( + if (notification.isGroupConversation && notification.conversationTitle != null) + "${notification.appName} · ${notification.conversationTitle}" + else + notification.appName.ifEmpty { notification.senderName ?: "" }, + color = accent, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (isCall) { + CallActions(notification, interactor, onDismiss) + } + if (!showReply && !isCall) { + if (initiallyCompact) { + ExpandChip(Icons.Filled.ExpandLess, accent) { + isExpanded = false + isCompact = true + } + } else if (hasExpandableContent) { + ExpandChip( + if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + accent, + ) { isExpanded = !isExpanded } + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + NotifAvatar(notification, accent, SizeCompactIcon) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + val sender = notification.senderName ?: notification.appName + Text( + sender, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + if (isExpanded) { + if (hasTitleAndBody) { + Text( + title!!, + color = OnCardText, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + body!!, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 8, + overflow = TextOverflow.Ellipsis, + ) + } else { + val preview = body ?: title ?: "" + if (preview.isNotEmpty()) { + Text( + preview, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 10, + overflow = TextOverflow.Ellipsis, + ) + } + } + } else { + val preview = body ?: title ?: "" + if (preview.isNotEmpty()) { + Text( + preview, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + onTextLayout = { isTextTruncated = it.hasVisualOverflow }, + ) + } + } + } + + notification.notificationImage?.let { img -> + Image( + bitmap = img.toScaledBitmap(ThumbnailSize), + contentDescription = null, + modifier = Modifier.size(ThumbnailSize).clip(ShapeSm), + contentScale = ContentScale.Crop, + ) + } + } + } + } + + val hasReply = notification.replyAction != null + val hasActions = notification.actions.isNotEmpty() || hasReply + if (!isCall && !showReply && hasActions) { + Box( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = SpaceXxl) + .padding(bottom = SpaceXxl), + ) { + if (isExpanded) { + ExpandedActions(notification, accent, onDismiss) { showReply = true } + } else { + CollapsedActions(notification, accent, onDismiss) { showReply = true } + } + } + } + + if (showReply && notification.replyAction != null) { + ReplyField( + reply = notification.replyAction!!, + accent = accent, + interactor = interactor, + onSent = { + showReply = false + onDismiss() + }, + ) + } + } + } + } +} + +@Composable +private fun NotifAvatar(notification: IslandEvent.Notification, accent: Color, size: Dp) { + val icon = notification.senderIcon ?: notification.appIcon + val isRound = notification.isConversation && notification.senderIcon != null + + if (notification.isConversation && notification.senderIcon != null && notification.appIcon != null) { + BadgedContactIcon(icon!!, notification.appIcon!!, size, (size.value * 0.44f).dp, true) + } else if (icon != null) { + Box( + modifier = Modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), + contentAlignment = Alignment.Center, + ) { + Image( + bitmap = icon.toScaledBitmap(size), + contentDescription = null, + modifier = Modifier.size(size), + contentScale = ContentScale.Crop, + ) + } + } else { + Box( + modifier = Modifier.size(size).clip(ShapeIconMedium).background(accent.copy(alpha = AlphaSubtle)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.Notifications, null, tint = accent, modifier = Modifier.size(SizeIconSm)) + } + } +} + +@Composable +private fun CollapsedActions( + notification: IslandEvent.Notification, + accent: Color, + onDismiss: () -> Unit, + onReplyClick: () -> Unit, +) { + val context = LocalContext.current + val hasReply = notification.replyAction != null + val visibleActions = notification.actions.take(2) + if (visibleActions.isEmpty() && !hasReply) return + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + if (hasReply) { + ExpressivePillButton( + label = notification.replyAction!!.label.toString(), + icon = Icons.AutoMirrored.Filled.Reply, + contentColor = chipContentColorOn(accent), + backgroundColor = accent, + modifier = Modifier.weight(1f), + onClick = onReplyClick, + ) + } + visibleActions.take(if (hasReply) 1 else 2).forEach { action -> + ExpressivePillButton( + label = action.label.toString(), + contentColor = accent, + backgroundColor = accent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + onDismiss() + }, + ) + } + } +} + +@Composable +private fun CallActions( + notification: IslandEvent.Notification, + interactor: IslandActions, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val extras = notification.sbn.notification?.extras + val isIncoming = extras?.containsKey(Notification.EXTRA_ANSWER_INTENT) == true + val answerPi = extras?.getParcelable(Notification.EXTRA_ANSWER_INTENT, PendingIntent::class.java) + val declinePi = extras?.getParcelable(Notification.EXTRA_DECLINE_INTENT, PendingIntent::class.java) + val hangUpPi = extras?.getParcelable(Notification.EXTRA_HANG_UP_INTENT, PendingIntent::class.java) + + Row(horizontalArrangement = Arrangement.spacedBy(SpaceMd)) { + if (isIncoming) { + notification.actions.forEach { action -> + val pi = action.action.actionIntent + val isDecline = (declinePi != null && pi == declinePi) || (hangUpPi != null && pi == hangUpPi) + val isAnswer = answerPi != null && pi == answerPi + val decline = isDecline || (!isAnswer && declinePi == null) + val bg = if (decline) RedAccent else GreenAccent + ActionCircle( + if (decline) Icons.Filled.CallEnd else Icons.Filled.Call, + bg, chipContentColorOn(bg), + ) { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + onDismiss() + } + } + } else { + notification.actions.forEach { action -> + val pi = action.action.actionIntent + val semantic = action.action.semanticAction + val isMute = semantic == Notification.Action.SEMANTIC_ACTION_MUTE || + semantic == Notification.Action.SEMANTIC_ACTION_UNMUTE + val isEnd = if (hangUpPi != null) pi == hangUpPi + else !isMute && action == notification.actions.first() + val bg = if (isEnd) RedAccent else ActionBg + val tint = if (isEnd) chipContentColorOn(RedAccent) else OnCardText + val drawable = action.action.getIcon()?.loadDrawable(context) + if (drawable != null) { + DrawableActionCircle(drawable, bg, tint) { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + if (isEnd) onDismiss() + } + } else { + ActionCircle( + if (isEnd) Icons.Filled.CallEnd else Icons.Filled.Call, + bg, tint, + ) { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + if (isEnd) onDismiss() + } + } + } + } + } +} + +@Composable +private fun ActionCircle( + icon: ImageVector, + bg: Color, + tint: Color, + onClick: () -> Unit, +) { + Surface(onClick = onClick, shape = CircleShape, color = bg, modifier = Modifier.size(36.dp)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { + Icon(icon, null, tint = tint, modifier = Modifier.size(18.dp)) + } + } +} + +@Composable +private fun ExpandChip(icon: ImageVector, accent: Color, onClick: () -> Unit) { + Surface( + onClick = onClick, + shape = CircleShape, + color = accent.copy(alpha = AlphaIconBg), + modifier = Modifier.size(36.dp), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { + Icon(icon, contentDescription = null, tint = accent, modifier = Modifier.size(18.dp)) + } + } +} + +@Composable +private fun DrawableActionCircle( + drawable: Drawable, + bg: Color, + tint: Color, + onClick: () -> Unit, +) { + Surface(onClick = onClick, shape = CircleShape, color = bg, modifier = Modifier.size(36.dp)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { + Image( + bitmap = drawable.toScaledBitmap(18.dp), + contentDescription = null, + modifier = Modifier.size(18.dp), + colorFilter = ColorFilter.tint(tint), + ) + } + } +} + +@Composable +private fun ExpandedActions( + notification: IslandEvent.Notification, + accent: Color, + onDismiss: () -> Unit, + onReplyClick: () -> Unit, +) { + val context = LocalContext.current + val hasReply = notification.replyAction != null + val visibleActions = notification.actions.take(MAX_EXPANDED_ACTIONS) + if (visibleActions.isEmpty() && !hasReply) return + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + if (hasReply) { + ExpressivePillButton( + label = notification.replyAction!!.label.toString(), + icon = Icons.AutoMirrored.Filled.Reply, + contentColor = chipContentColorOn(accent), + backgroundColor = accent, + modifier = Modifier.weight(1f), + onClick = onReplyClick, + ) + } + visibleActions.forEach { action -> + ExpressivePillButton( + label = action.label.toString(), + contentColor = accent, + backgroundColor = accent.copy(alpha = AlphaIconBg), + modifier = Modifier.weight(1f), + onClick = { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + onDismiss() + }, + ) + } + } +} + + +@Composable +private fun ReplyField( + reply: IslandEvent.ReplyAction, + accent: Color, + interactor: IslandActions, + onSent: () -> Unit, +) { + var replyText by remember { mutableStateOf("") } + val context = LocalContext.current + + Row( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = SpaceLg, vertical = SpaceMd) + .height(40.dp) + .clip(ShapeChip) + .background(DarkCard), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = replyText, + onValueChange = { replyText = it }, + modifier = Modifier.weight(1f) + .padding(horizontal = SpaceLg) + .onFocusChanged { interactor.onFocusableRequested?.invoke(it.isFocused) }, + textStyle = MaterialTheme.typography.bodySmall.copy(color = OnCardText), + singleLine = true, + cursorBrush = SolidColor(accent), + decorationBox = { inner -> + if (replyText.isEmpty()) { + Text(reply.label.toString(), color = SubtleGray, style = MaterialTheme.typography.bodySmall) + } + inner() + }, + ) + if (replyText.isNotEmpty()) { + Surface( + onClick = { + val intent = Intent().apply { + val results = Bundle().apply { + putCharSequence(reply.remoteInput.resultKey, replyText) + } + RemoteInput.addResultsToIntent(arrayOf(reply.remoteInput), this, results) + } + try { reply.action.actionIntent?.sendWithBal(context, intent) } + catch (e: Exception) { Log.w("NotificationReply", "Failed to send reply", e) } + replyText = "" + interactor.onFocusableRequested?.invoke(false) + onSent() + }, + shape = CircleShape, + color = accent, + modifier = Modifier.size(32.dp).padding(SpaceXs), + ) { + Box(contentAlignment = Alignment.Center) { + Icon(Icons.AutoMirrored.Filled.Send, null, tint = chipContentColorOn(accent), modifier = Modifier.size(14.dp)) + } + } + Spacer(Modifier.width(SpaceXs)) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt new file mode 100644 index 000000000000..aa829875a254 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -0,0 +1,1290 @@ +package com.android.systemui.axdynamicbar.ui.compose + +import android.graphics.drawable.Drawable +import android.media.AudioManager +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.FlashlightOn +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.res.stringResource +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.axdynamicbar.model.RecordingState +import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.res.R +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import java.lang.Math.toRadians +import kotlinx.coroutines.delay + +@Composable +internal fun PillEventIcon(event: IslandEvent, tint: Color? = null) { + when (event) { + is IslandEvent.ScreenRecording -> BlinkingDotIcon(tint ?: RedAccent, isAnimating = !event.isCountdown) + is IslandEvent.MicCamActive -> PrivacyDotIcon(event, tint) + is IslandEvent.AudioRecording -> + BlinkingDotIcon(tint ?: RedAccent, isAnimating = event.state == RecordingState.RECORDING) + is IslandEvent.Casting -> AnimatedCastIcon(tint ?: TealAccent) + is IslandEvent.Media -> MediaPillIcon(event) + is IslandEvent.PromotedOngoing -> PromotedOngoingPillIcon(event, tint) + is IslandEvent.Sports -> SportsPillIcon(event) + is IslandEvent.NowPlaying -> AnimatedNowPlayingIcon(tint ?: MintAccent) + is IslandEvent.Bluetooth -> AnimatedBluetoothIcon(tint ?: BlueAccent) + is IslandEvent.Hotspot -> AnimatedHotspotIcon(tint ?: TealAccent) + is IslandEvent.Charging -> AnimatedBoltIcon(tint ?: GreenAccent) + is IslandEvent.Alarm -> AnimatedBellIcon(tint ?: OrangeAccent, isAnimating = event.isRinging) + is IslandEvent.Timer -> AnimatedHourglassIcon(tint ?: BlueAccent, isAnimating = !event.isPaused) + is IslandEvent.Stopwatch -> AnimatedTickIcon(tint ?: MintAccent, isRunning = event.isRunning) + is IslandEvent.RingerMode -> RingerIcon(event, tint) + is IslandEvent.Vpn -> AnimatedShieldIcon(tint ?: IndigoAccent) + is IslandEvent.Clipboard -> AnimatedClipboardIcon(tint ?: IndigoAccent) + is IslandEvent.Notification -> NotificationPillIcon(event) + is IslandEvent.AppSwitch -> AppSwitchPillIcon(event) + is IslandEvent.Torch -> + Icon(Icons.Filled.FlashlightOn, null, tint = tint ?: YellowAccent, modifier = Modifier.size(SizeBadge)) + is IslandEvent.BiometricUnlock -> BiometricUnlockIcon(tint) + is IslandEvent.KeyguardIndication -> KeyguardIndicationIcon(event, tint) + } +} + +@Composable +private fun BlinkingDotIcon(color: Color, isAnimating: Boolean = true) { + if (isAnimating) { + val transition = rememberInfiniteTransition(label = "blink") + val alpha by + transition.animateFloat( + initialValue = 1f, + targetValue = AlphaSubtle, + animationSpec = + infiniteRepeatable(tween(550, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "blink_alpha", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { drawCircle(color = color.copy(alpha = alpha)) } + } else { + + Canvas(modifier = Modifier.size(SizeBadge)) { drawCircle(color = color.copy(alpha = AlphaTertiary)) } + } +} + +@Composable +private fun PrivacyDotIcon(event: IslandEvent.MicCamActive, tint: Color? = null) { + val transition = rememberInfiniteTransition(label = "privacy_pulse") + val alpha by + transition.animateFloat( + initialValue = 1f, + targetValue = AlphaDisabled, + animationSpec = + infiniteRepeatable(tween(600, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "privacy_alpha", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val spacing = size.width * 0.35f + if (event.isCam) { + drawCircle( + color = (tint ?: RedAccent).copy(alpha = alpha), + radius = size.minDimension * 0.25f, + center = Offset(size.width / 2 - spacing / 2, size.height / 2), + ) + } + if (event.isMic) { + drawCircle( + color = (tint ?: OrangeAccent).copy(alpha = alpha), + radius = size.minDimension * 0.25f, + center = + Offset( + if (event.isCam) size.width / 2 + spacing / 2 else size.width / 2, + size.height / 2, + ), + ) + } + } +} + +@Composable +private fun AnimatedTrophyIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "trophy") + val shimmer by transition.animateFloat( + initialValue = 0.6f, + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(1200, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "trophy_shimmer", + ) + + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + val sw = 1.5f.dp.toPx() + val cx = w / 2f + val shimColor = color.copy(alpha = shimmer) + + val cupTop = h * 0.12f + val cupBot = h * 0.58f + val cupLeft = w * 0.25f + val cupRight = w * 0.75f + val cupPath = Path().apply { + moveTo(cupLeft, cupTop) + lineTo(cupRight, cupTop) + lineTo(cupRight - w * 0.05f, cupBot) + quadraticBezierTo(cx, cupBot + h * 0.08f, cupLeft + w * 0.05f, cupBot) + close() + } + drawPath(cupPath, shimColor, style = Stroke(sw, cap = StrokeCap.Round)) + + val handleY = cupTop + (cupBot - cupTop) * 0.35f + val handleRad = w * 0.12f + drawArc(shimColor, 0f, 180f, false, Offset(cupLeft - handleRad * 1.5f, handleY - handleRad), Size(handleRad * 2, handleRad * 2), style = Stroke(sw * 0.8f, cap = StrokeCap.Round)) + drawArc(shimColor, 180f, 180f, false, Offset(cupRight - handleRad * 0.5f, handleY - handleRad), Size(handleRad * 2, handleRad * 2), style = Stroke(sw * 0.8f, cap = StrokeCap.Round)) + + val stemTop = cupBot + val stemBot = h * 0.78f + drawLine(shimColor, Offset(cx, stemTop), Offset(cx, stemBot), sw, StrokeCap.Round) + + val baseY = stemBot + val baseHalf = w * 0.22f + drawLine(shimColor, Offset(cx - baseHalf, baseY), Offset(cx + baseHalf, baseY), sw * 1.2f, StrokeCap.Round) + } +} + +@Composable +private fun MediaPillIcon(event: IslandEvent.Media) { + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } + ?: Box( + modifier = + Modifier.size(16.dp).clip(CircleShape).background(OrangeAccent.copy(alpha = AlphaSubtle + 0.05f)), + contentAlignment = Alignment.Center, + ) { + WaveformAnimation(OrangeAccent, Modifier.size(10.dp), isAnimating = event.isPlaying, barCount = 3) + } +} + +@Composable +private fun AnimatedHotspotIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "hotspot") + val sweep by + transition.animateFloat( + initialValue = 0f, + targetValue = 3f, + animationSpec = + infiniteRepeatable(tween(2000, easing = LinearEasing), RepeatMode.Restart), + label = "hotspot_sweep", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = size.width / 2 + val cy = size.height / 2 + for (i in 0 until 3) { + val r = size.minDimension * (0.18f + i * 0.18f) + val a = if (sweep > i) ((sweep - i).coerceIn(0f, 1f) * 0.8f) else AlphaFaint + drawArc( + color = color.copy(alpha = a), + startAngle = 200f, + sweepAngle = 140f, + useCenter = false, + topLeft = Offset(cx - r, cy - r), + size = Size(r * 2, r * 2), + style = Stroke(SizeStrokeThin.dp.toPx(), cap = StrokeCap.Round), + ) + } + drawCircle(color, radius = size.minDimension * 0.1f, center = Offset(cx, cy)) + } +} + +@Composable +private fun AnimatedCastIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "cast") + val sweep by + transition.animateFloat( + initialValue = 0f, + targetValue = 3f, + animationSpec = + infiniteRepeatable(tween(2000, easing = LinearEasing), RepeatMode.Restart), + label = "cast_sweep", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val sw = SizeStrokeThin.dp.toPx() + val w = size.width + val h = size.height + drawRoundRect( + color = color.copy(alpha = 0.5f), + topLeft = Offset(0f, h * 0.15f), + size = Size(w, h * 0.7f), + cornerRadius = CornerRadius(w * 0.12f), + style = Stroke(sw), + ) + val bx = w * 0.15f + val by = h * 0.85f + for (i in 0 until 3) { + val r = w * (0.1f + i * 0.12f) + val a = if (sweep > i) ((sweep - i).coerceIn(0f, 1f) * 0.7f) else AlphaFaint + drawArc( + color = color.copy(alpha = a), + startAngle = 180f, + sweepAngle = 90f, + useCenter = false, + topLeft = Offset(bx - r, by - r), + size = Size(r * 2, r * 2), + style = Stroke(sw, cap = StrokeCap.Round), + ) + } + drawCircle(color, radius = w * 0.06f, center = Offset(bx, by)) + } +} + +@Composable +private fun AnimatedNowPlayingIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "np") + val bounce by + transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = + infiniteRepeatable(tween(1200, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "np_bounce", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + val sw = SizeStrokeThin.dp.toPx() + val noteX = w * 0.55f + val noteTop = h * 0.15f + bounce * h * 0.08f + val noteBottom = h * 0.72f + bounce * h * 0.04f + drawLine(color, Offset(noteX, noteTop), Offset(noteX, noteBottom), sw, StrokeCap.Round) + drawCircle(color, radius = w * 0.14f, center = Offset(noteX - w * 0.1f, noteBottom)) + drawLine( + color, + Offset(noteX, noteTop), + Offset(noteX + w * 0.2f, noteTop + h * 0.06f), + sw, + StrokeCap.Round, + ) + } +} + +@Composable +private fun AnimatedBluetoothIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "bt") + val alpha by + transition.animateFloat( + initialValue = 0.5f, + targetValue = 1f, + animationSpec = + infiniteRepeatable(tween(1000, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "bt_alpha", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = size.width / 2 + val w = size.width + val h = size.height + val sw = SizeStrokeThin.dp.toPx() + val drawColor = color.copy(alpha = alpha) + drawLine(drawColor, Offset(cx, h * 0.1f), Offset(cx, h * 0.9f), sw, StrokeCap.Round) + drawLine(drawColor, Offset(cx, h * 0.1f), Offset(cx + w * 0.22f, h * 0.3f), sw, StrokeCap.Round) + drawLine(drawColor, Offset(cx + w * 0.22f, h * 0.3f), Offset(cx - w * 0.22f, h * 0.7f), sw, StrokeCap.Round) + drawLine(drawColor, Offset(cx, h * 0.9f), Offset(cx + w * 0.22f, h * 0.7f), sw, StrokeCap.Round) + drawLine(drawColor, Offset(cx + w * 0.22f, h * 0.7f), Offset(cx - w * 0.22f, h * 0.3f), sw, StrokeCap.Round) + } +} + +@Composable +private fun AnimatedShieldIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "shield") + val glow by + transition.animateFloat( + initialValue = 0.5f, + targetValue = 1f, + animationSpec = + infiniteRepeatable(tween(1200, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "shield_glow", + ) + val shieldPath = remember { Path() } + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + val cx = w / 2 + shieldPath.rewind() + shieldPath.moveTo(cx, h * 0.08f) + shieldPath.lineTo(w * 0.15f, h * 0.28f) + shieldPath.lineTo(w * 0.15f, h * 0.55f) + shieldPath.quadraticTo(w * 0.15f, h * 0.85f, cx, h * 0.95f) + shieldPath.quadraticTo(w * 0.85f, h * 0.85f, w * 0.85f, h * 0.55f) + shieldPath.lineTo(w * 0.85f, h * 0.28f) + shieldPath.close() + drawPath(shieldPath, color.copy(alpha = glow * 0.3f)) + drawPath(shieldPath, color.copy(alpha = glow), style = Stroke(SizeStrokeThin.dp.toPx())) + val sw = SizeStrokeThin.dp.toPx() + drawLine(color, Offset(cx - w * 0.12f, h * 0.52f), Offset(cx, h * 0.65f), sw, StrokeCap.Round) + drawLine(color, Offset(cx, h * 0.65f), Offset(cx + w * 0.18f, h * 0.38f), sw, StrokeCap.Round) + } +} + +@Composable +private fun AnimatedClipboardIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "clip") + val slideIn by + transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = + infiniteRepeatable( + tween(1500, easing = FastOutSlowInEasing), + RepeatMode.Reverse, + ), + label = "clip_slide", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + val sw = SizeStrokeThin.dp.toPx() + drawRoundRect( + color = color.copy(alpha = 0.6f), + topLeft = Offset(w * 0.18f, h * 0.2f), + size = Size(w * 0.64f, h * 0.72f), + cornerRadius = CornerRadius(w * 0.08f), + style = Stroke(sw), + ) + drawRoundRect( + color = color, + topLeft = Offset(w * 0.3f, h * 0.1f), + size = Size(w * 0.4f, h * 0.15f), + cornerRadius = CornerRadius(w * 0.06f), + ) + val lineAlpha = slideIn.coerceIn(0.3f, 1f) + val lineY1 = h * 0.48f + val lineY2 = h * 0.62f + val lineY3 = h * 0.76f + val lineLeft = w * 0.3f + val lineRight = w * 0.7f + drawLine(color.copy(alpha = lineAlpha), Offset(lineLeft, lineY1), Offset(lineRight, lineY1), sw, StrokeCap.Round) + drawLine(color.copy(alpha = lineAlpha * 0.7f), Offset(lineLeft, lineY2), Offset(lineRight * 0.85f, lineY2), sw, StrokeCap.Round) + drawLine(color.copy(alpha = lineAlpha * 0.4f), Offset(lineLeft, lineY3), Offset(lineRight * 0.6f, lineY3), sw, StrokeCap.Round) + } +} + +@Composable +private fun AnimatedBoltIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "bolt") + val glow by + transition.animateFloat( + initialValue = 0.5f, + targetValue = 1f, + animationSpec = + infiniteRepeatable(tween(800, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "bolt_glow", + ) + val boltPath = remember { Path() } + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + boltPath.rewind() + boltPath.moveTo(w * 0.55f, h * 0.05f) + boltPath.lineTo(w * 0.25f, h * 0.50f) + boltPath.lineTo(w * 0.45f, h * 0.50f) + boltPath.lineTo(w * 0.40f, h * 0.95f) + boltPath.lineTo(w * 0.75f, h * 0.42f) + boltPath.lineTo(w * 0.55f, h * 0.42f) + boltPath.close() + drawPath(boltPath, color.copy(alpha = glow)) + } +} + +@Composable +private fun AnimatedBellIcon(color: Color, isAnimating: Boolean = true) { + val swing: Float + if (isAnimating) { + val transition = rememberInfiniteTransition(label = "bell") + val animatedSwing by + transition.animateFloat( + initialValue = -8f, + targetValue = 8f, + animationSpec = + infiniteRepeatable(tween(400, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "bell_swing", + ) + swing = animatedSwing + } else { + swing = 0f + } + Canvas(modifier = Modifier.size(SizeBadge)) { + rotate(swing) { + val cx = size.width / 2 + + drawRoundRect( + color = color, + topLeft = Offset(cx - size.width * 0.28f, size.height * 0.15f), + size = Size(size.width * 0.56f, size.height * 0.55f), + cornerRadius = CornerRadius(size.width * 0.15f), + ) + + drawRoundRect( + color = color, + topLeft = Offset(cx - size.width * 0.38f, size.height * 0.62f), + size = Size(size.width * 0.76f, size.height * 0.12f), + cornerRadius = CornerRadius(size.width * 0.06f), + ) + + drawCircle(color, radius = size.width * 0.08f, center = Offset(cx, size.height * 0.85f)) + } + } +} + +@Composable +private fun AnimatedHourglassIcon(color: Color, isAnimating: Boolean = true) { + val angle: Float + if (isAnimating) { + val transition = rememberInfiniteTransition(label = "hg") + val animatedAngle by + transition.animateFloat( + initialValue = 0f, + targetValue = 180f, + animationSpec = + infiniteRepeatable(tween(2000, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "hg_angle", + ) + angle = animatedAngle + } else { + angle = 0f + } + val drawColor = if (isAnimating) color else color.copy(alpha = AlphaTertiary) + val hgPath = remember { Path() } + Canvas(modifier = Modifier.size(SizeBadge)) { + rotate(angle) { + val w = size.width + val h = size.height + hgPath.rewind() + hgPath.moveTo(w * 0.2f, h * 0.1f) + hgPath.lineTo(w * 0.8f, h * 0.1f) + hgPath.lineTo(w * 0.55f, h * 0.45f) + hgPath.lineTo(w * 0.8f, h * 0.9f) + hgPath.lineTo(w * 0.2f, h * 0.9f) + hgPath.lineTo(w * 0.45f, h * 0.45f) + hgPath.close() + drawPath(hgPath, drawColor, style = Stroke(SizeStrokeThin.dp.toPx(), cap = StrokeCap.Round)) + + drawCircle( + drawColor.copy(alpha = AlphaTertiary), + radius = w * 0.08f, + center = Offset(w / 2, h * 0.72f), + ) + } + } +} + +@Composable +private fun AnimatedTickIcon(color: Color, isRunning: Boolean) { + val angle: Float + if (isRunning) { + val transition = rememberInfiniteTransition(label = "tick") + val animatedAngle by + transition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = + infiniteRepeatable( + tween(1500, easing = LinearEasing), + RepeatMode.Restart, + ), + label = "tick_angle", + ) + angle = animatedAngle + } else { + angle = 0f + } + val drawColor = if (isRunning) color else color.copy(alpha = AlphaTertiary) + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = size.width / 2 + val cy = size.height / 2 + val r = size.minDimension / 2 * 0.85f + + drawCircle(drawColor.copy(alpha = AlphaDisabled), radius = r, style = Stroke(1.2f.dp.toPx())) + + val rad = toRadians(angle.toDouble() - 90.0) + drawLine( + drawColor, + Offset(cx, cy), + Offset(cx + (r * 0.7f * cos(rad)).toFloat(), cy + (r * 0.7f * sin(rad)).toFloat()), + strokeWidth = SizeStrokeThin.dp.toPx(), + cap = StrokeCap.Round, + ) + drawCircle(drawColor, radius = SizeStrokeThin.dp.toPx(), center = Offset(cx, cy)) + } +} + +@Composable +private fun RingerIcon(event: IslandEvent.RingerMode, tint: Color? = null) { + val color = tint ?: eventStyleFor(event).accent + val offset = if (event.mode == AudioManager.RINGER_MODE_VIBRATE) { + val transition = rememberInfiniteTransition(label = "ringer") + val anim by transition.animateFloat( + initialValue = -1f, + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(120), RepeatMode.Reverse), + label = "ringer_offset", + ) + anim + } else { + 0f + } + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = + size.width / 2 + + if (event.mode == AudioManager.RINGER_MODE_VIBRATE) offset * SizeStrokeThin.dp.toPx() else 0f + val cy = size.height / 2 + val r = size.minDimension / 2 * 0.85f + drawRoundRect( + color = color, + topLeft = Offset(cx - r * 0.4f, cy - r * 0.6f), + size = Size(r * 0.8f, r * 1.2f), + cornerRadius = CornerRadius(r * 0.2f), + ) + if (event.mode == AudioManager.RINGER_MODE_SILENT) { + + drawLine( + color, + Offset(cx - r * 0.6f, cy + r * 0.6f), + Offset(cx + r * 0.6f, cy - r * 0.6f), + strokeWidth = SizeStrokeThin.dp.toPx(), + ) + } + } +} + +@Composable +private fun AnimatedOngoingIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "ongoing") + val rotate by + transition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = + infiniteRepeatable(tween(2500, easing = LinearEasing), RepeatMode.Restart), + label = "ongoing_rot", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = size.width / 2 + val cy = size.height / 2 + val r = size.minDimension / 2 * 0.8f + val sw = SizeStrokeThin.dp.toPx() + drawCircle(color.copy(alpha = AlphaFaint), radius = r, style = Stroke(sw)) + drawArc( + color = color, + startAngle = rotate, + sweepAngle = 90f, + useCenter = false, + topLeft = Offset(cx - r, cy - r), + size = Size(r * 2, r * 2), + style = Stroke(sw, cap = StrokeCap.Round), + ) + } +} + +@Composable +private fun AnimatedRecentsIcon(color: Color) { + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width + val h = size.height + val sw = SizeStrokeThin.dp.toPx() + val pad = w * 0.18f + val gap = w * 0.08f + val half = (w - pad * 2 - gap) / 2 + drawRoundRect(color, Offset(pad, pad), Size(half, half), CornerRadius(w * 0.06f), style = Stroke(sw)) + drawRoundRect(color, Offset(pad + half + gap, pad), Size(half, half), CornerRadius(w * 0.06f), style = Stroke(sw)) + drawRoundRect(color, Offset(pad, pad + half + gap), Size(half, half), CornerRadius(w * 0.06f), style = Stroke(sw)) + drawRoundRect(color.copy(alpha = AlphaTertiary), Offset(pad + half + gap, pad + half + gap), Size(half, half), CornerRadius(w * 0.06f), style = Stroke(sw)) + } +} + +@Composable +private fun NotificationPillIcon(event: IslandEvent.Notification) { + val icon = event.senderIcon ?: event.appIcon + val isRound = event.isConversation && event.senderIcon != null + val hasBadge = event.isConversation && event.senderIcon != null && event.appIcon != null + icon?.let { + if (hasBadge) { + BadgedContactIcon( + mainIcon = it, + badgeIcon = event.appIcon!!, + mainSize = 16.dp, + badgeSize = 9.dp, + isRound = true, + ) + } else { + Image( + bitmap = it.toScaledBitmap(16.dp), + contentDescription = null, + modifier = + Modifier.size(16.dp) + .clip(if (isRound) CircleShape else ShapeXs), + ) + } + } ?: Icon(Icons.Filled.Notifications, null, tint = BlueAccent, modifier = Modifier.size(SizeBadge)) +} + +private val DOWNLOAD_KEYWORDS = Regex( + "download", + RegexOption.IGNORE_CASE, +) + +@Composable +private fun PromotedOngoingPillIcon(event: IslandEvent.PromotedOngoing, tint: Color? = null) { + val hasProgress = event.progress >= 0f || event.isIndeterminate + val color = tint ?: BlueAccent + + val isDownloadLike = hasProgress && ( + DOWNLOAD_KEYWORDS.containsMatchIn(event.title) || + DOWNLOAD_KEYWORDS.containsMatchIn(event.text) || + DOWNLOAD_KEYWORDS.containsMatchIn(event.shortText) + ) + + if (isDownloadLike) { + AnimatedDownloadIcon(color) + } else if (event.appIcon != null) { + Image( + bitmap = event.appIcon.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(ShapeXs), + contentScale = ContentScale.Crop, + ) + } else if (hasProgress) { + AnimatedDownloadIcon(color) + } else { + AnimatedOngoingIcon(color) + } +} + +@Composable +private fun AnimatedDownloadIcon(color: Color) { + val transition = rememberInfiniteTransition(label = "download") + val bounce by transition.animateFloat( + initialValue = -1.5f, + targetValue = 1.5f, + animationSpec = infiniteRepeatable( + tween(800, easing = FastOutSlowInEasing), + RepeatMode.Reverse, + ), + label = "arrow_bounce", + ) + + Canvas(modifier = Modifier.size(SizeBadge)) { + val cx = size.width / 2f + val cy = size.height / 2f + val sw = 1.6f.dp.toPx() + val arrowOffset = bounce.dp.toPx() + + val trayY = size.height * 0.82f + val trayHalf = size.width * 0.32f + val trayDepth = SizeStrokeWidth.toPx() + drawLine(color, Offset(cx - trayHalf, trayY), Offset(cx - trayHalf, trayY + trayDepth), sw, StrokeCap.Round) + drawLine(color, Offset(cx - trayHalf, trayY + trayDepth), Offset(cx + trayHalf, trayY + trayDepth), sw, StrokeCap.Round) + drawLine(color, Offset(cx + trayHalf, trayY + trayDepth), Offset(cx + trayHalf, trayY), sw, StrokeCap.Round) + + val arrowTop = size.height * 0.12f + arrowOffset + val arrowBottom = size.height * 0.62f + arrowOffset + drawLine(color, Offset(cx, arrowTop), Offset(cx, arrowBottom), sw, StrokeCap.Round) + + val headSize = size.width * 0.22f + drawLine(color, Offset(cx - headSize, arrowBottom - headSize), Offset(cx, arrowBottom), sw, StrokeCap.Round) + drawLine(color, Offset(cx + headSize, arrowBottom - headSize), Offset(cx, arrowBottom), sw, StrokeCap.Round) + } +} + +@Composable +private fun PromotedOngoingText(event: IslandEvent.PromotedOngoing, modifier: Modifier, overrideColor: Color? = null) { + val label = event.shortText.ifEmpty { event.title.ifEmpty { event.appName } } + MarqueeLabel(label, overrideColor ?: BlueAccent, modifier) +} + +@Composable +private fun SportsPillIcon(event: IslandEvent.Sports) { + val icon = event.team1Icon ?: event.team2Icon ?: event.appIcon + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + AnimatedTrophyIcon(accentColorFor(event)) + } +} + +@Composable +private fun SportsText(event: IslandEvent.Sports, modifier: Modifier, overrideColor: Color? = null) { + val color = overrideColor ?: accentColorFor(event) + val scoreText = when { + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + event.team2Name.isEmpty() -> event.team1Name + else -> "vs" + } + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Text(scoreText, color = color, style = PillAccent, maxLines = 1) + event.team2Icon?.let { icon -> + Box(modifier = Modifier.padding(start = 4.dp)) { + Image( + bitmap = icon.toScaledBitmap(14.dp), + contentDescription = null, + modifier = Modifier.size(14.dp).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } + } + } +} + +@Composable +private fun AppSwitchPillIcon(event: IslandEvent.AppSwitch) { + val app = event.previousApp ?: event.recentApps.firstOrNull() + app?.appIcon?.let { + Image( + bitmap = it.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } ?: AnimatedRecentsIcon(SubtleGray) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun BiometricUnlockIcon(tint: Color? = null) { + val color = tint ?: GreenAccent + val motionScheme = MaterialTheme.motionScheme + var started by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { started = true } + + val sweep by + animateFloatAsState( + targetValue = if (started) 360f else 0f, + animationSpec = motionScheme.defaultSpatialSpec(), + label = "bio_sweep", + ) + val checkAlpha by + animateFloatAsState( + targetValue = if (started) 1f else 0f, + animationSpec = tween(250, delayMillis = 500), + label = "bio_check", + ) + + Box(modifier = Modifier.size(18.dp), contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(18.dp)) { + val inset = 1.dp.toPx() + drawArc( + color = color, + startAngle = -90f, + sweepAngle = sweep, + useCenter = false, + style = Stroke(width = SizeStrokeThin.dp.toPx(), cap = StrokeCap.Round), + topLeft = Offset(inset, inset), + size = Size(size.width - inset * 2, size.height - inset * 2), + ) + } + + if (checkAlpha < 1f) { + Icon( + Icons.Filled.Lock, + null, + tint = color.copy(alpha = 1f - checkAlpha), + modifier = Modifier.size(10.dp), + ) + } + if (checkAlpha > 0f) { + Icon( + Icons.Filled.Check, + null, + tint = color.copy(alpha = checkAlpha), + modifier = Modifier.size(10.dp), + ) + } + } +} + +@Composable +private fun KeyguardIndicationIcon(event: IslandEvent.KeyguardIndication, tint: Color? = null) { + when (event.indicationType) { + IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC -> { + + val color = tint ?: GreenAccent + val transition = rememberInfiniteTransition(label = "kg_bio") + val sweep by transition.animateFloat( + initialValue = 60f, targetValue = 300f, + animationSpec = infiniteRepeatable(tween(1200, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "kg_bio_sweep", + ) + Canvas(modifier = Modifier.size(SizeBadge)) { + val inset = 1.dp.toPx() + drawArc( + color = color, + startAngle = -90f, + sweepAngle = sweep, + useCenter = false, + style = Stroke(width = SizeStrokeThin.dp.toPx(), cap = StrokeCap.Round), + topLeft = Offset(inset, inset), + size = Size(size.width - inset * 2, size.height - inset * 2), + ) + } + } + IslandEvent.KeyguardIndication.IndicationType.TRUST -> { + + val color = tint ?: IndigoAccent + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width; val h = size.height + val path = Path().apply { + moveTo(w * 0.5f, h * 0.05f) + lineTo(w * 0.9f, h * 0.25f) + lineTo(w * 0.85f, h * 0.65f) + quadraticTo(w * 0.5f, h * 1.0f, w * 0.15f, h * 0.65f) + lineTo(w * 0.1f, h * 0.25f) + close() + } + drawPath(path, color) + } + } + IslandEvent.KeyguardIndication.IndicationType.ALIGNMENT -> { + + AnimatedBoltIcon(tint ?: OrangeAccent) + } + IslandEvent.KeyguardIndication.IndicationType.DISCLOSURE -> { + + val color = tint ?: IndigoAccent + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width; val h = size.height + + drawRect(color, Offset(w * 0.15f, h * 0.3f), Size(w * 0.35f, h * 0.65f)) + drawRect(color, Offset(w * 0.55f, h * 0.1f), Size(w * 0.3f, h * 0.85f)) + + val windowColor = Color.Black.copy(alpha = AlphaDisabled) + drawRect(windowColor, Offset(w * 0.25f, h * 0.45f), Size(w * 0.12f, h * 0.1f)) + drawRect(windowColor, Offset(w * 0.25f, h * 0.65f), Size(w * 0.12f, h * 0.1f)) + drawRect(windowColor, Offset(w * 0.62f, h * 0.22f), Size(w * 0.12f, h * 0.1f)) + drawRect(windowColor, Offset(w * 0.62f, h * 0.42f), Size(w * 0.12f, h * 0.1f)) + drawRect(windowColor, Offset(w * 0.62f, h * 0.62f), Size(w * 0.12f, h * 0.1f)) + } + } + IslandEvent.KeyguardIndication.IndicationType.OWNER_INFO -> { + + val color = tint ?: IndigoAccent + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width; val h = size.height + + drawCircle(color, radius = w * 0.2f, center = Offset(w * 0.5f, h * 0.3f)) + + drawArc( + color, startAngle = 0f, sweepAngle = -180f, useCenter = true, + topLeft = Offset(w * 0.15f, h * 0.55f), + size = Size(w * 0.7f, h * 0.5f), + ) + } + } + IslandEvent.KeyguardIndication.IndicationType.PERSISTENT_UNLOCK -> { + + val color = tint ?: GreenAccent + Canvas(modifier = Modifier.size(SizeBadge)) { + val w = size.width; val h = size.height + + drawRoundRect(color, Offset(w * 0.2f, h * 0.45f), Size(w * 0.6f, h * 0.48f), CornerRadius(SizeStrokeWidth.toPx())) + + drawArc( + color, startAngle = -180f, sweepAngle = 180f, useCenter = false, + style = Stroke(width = SizeStrokeThin.dp.toPx(), cap = StrokeCap.Round), + topLeft = Offset(w * 0.28f, h * 0.08f), + size = Size(w * 0.44f, h * 0.44f), + ) + } + } + IslandEvent.KeyguardIndication.IndicationType.TRANSIENT -> { + + val color = tint ?: BlueAccent + Canvas(modifier = Modifier.size(SizeBadge)) { + drawCircle(color, style = Stroke(width = SizeStrokeThin.dp.toPx())) + val cx = size.width / 2f; val cy = size.height / 2f + drawCircle(Color.White, radius = 0.8.dp.toPx(), center = Offset(cx, cy - 2.2.dp.toPx())) + drawLine( + Color.White, Offset(cx, cy - 0.3.dp.toPx()), Offset(cx, cy + 2.8.dp.toPx()), + strokeWidth = 1.2.dp.toPx(), cap = StrokeCap.Round, + ) + } + } + } +} + +@Composable +internal fun PillEventText( + event: IslandEvent, + modifier: Modifier, + notifCount: Int = 0, + overrideColor: Color? = null, +) { + when (event) { + is IslandEvent.ScreenRecording -> RecordingText(event, modifier, overrideColor ?: RedAccent) + is IslandEvent.MicCamActive -> MicCamText(event, modifier, overrideColor) + is IslandEvent.AudioRecording -> AudioRecText(event, modifier, overrideColor) + is IslandEvent.Casting -> MarqueeLabel(event.deviceName.take(12), overrideColor ?: TealAccent, modifier) + is IslandEvent.Media -> MediaText(event, modifier, overrideColor) + is IslandEvent.PromotedOngoing -> PromotedOngoingText(event, modifier, overrideColor) + is IslandEvent.Sports -> SportsText(event, modifier, overrideColor) + is IslandEvent.NowPlaying -> MarqueeLabel( + "${event.songTitle} · ${event.artist}".trimEnd(' ', '·', ' '), + overrideColor ?: MintAccent, + modifier, + ) + is IslandEvent.Bluetooth -> BtText(event, modifier, overrideColor) + is IslandEvent.Hotspot -> + MarqueeLabel( + if (event.numDevices > 0) "${stringResource(R.string.ax_dynamic_bar_hotspot)} · ${stringResource(R.string.ax_dynamic_bar_hotspot_devices, event.numDevices)}" + else stringResource(R.string.ax_dynamic_bar_hotspot), + overrideColor ?: TealAccent, + modifier, + ) + is IslandEvent.Charging -> MarqueeLabel("${event.level}%", overrideColor ?: GreenAccent, modifier) + is IslandEvent.Alarm -> + MarqueeLabel(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_alarm) }, overrideColor ?: OrangeAccent, modifier) + is IslandEvent.Timer -> TimerText(event, modifier, overrideColor) + is IslandEvent.Stopwatch -> StopwatchText(event, modifier, overrideColor) + is IslandEvent.RingerMode -> MarqueeLabel(event.label, overrideColor ?: eventStyleFor(event).accent, modifier) + is IslandEvent.Vpn -> MarqueeLabel(stringResource(R.string.ax_dynamic_bar_vpn_active), overrideColor ?: IndigoAccent, modifier) + is IslandEvent.Clipboard -> + MarqueeLabel(event.preview.ifEmpty { stringResource(R.string.ax_dynamic_bar_copied) }, overrideColor ?: IndigoAccent, modifier) + is IslandEvent.Notification -> { + if (event.callStartTimeMs > 0L && event.appName.startsWith("Phone:")) { + CallTimerText(event, modifier, overrideColor) + } else { + val name = event.senderName ?: if (event.isConversation) event.title else null + if (name != null) { + val label = if (event.isGroupConversation && event.conversationTitle != null) + "$name · ${event.conversationTitle}" else name + MarqueeLabel(label, overrideColor ?: BlueAccent, modifier) + } else { + NotifBellBadge(modifier, notifCount) + } + } + } + is IslandEvent.AppSwitch -> + MarqueeLabel(stringResource(R.string.ax_dynamic_bar_recents), overrideColor ?: SubtleGray, modifier) + is IslandEvent.Torch -> { + val label = + if (event.supportsLevel) + "${(event.level.toFloat() / event.maxLevel * 100).toInt()}%" + else stringResource(R.string.ax_dynamic_bar_on) + MarqueeLabel(label, overrideColor ?: YellowAccent, modifier) + } + is IslandEvent.BiometricUnlock -> MarqueeLabel(stringResource(R.string.ax_dynamic_bar_unlocked), overrideColor ?: GreenAccent, modifier) + is IslandEvent.KeyguardIndication -> MarqueeLabel(event.text, overrideColor ?: IndigoAccent, modifier) + } +} + +@Composable +private fun MarqueeLabel(text: String, color: Color, modifier: Modifier = Modifier) { + Text( + text, + color = color, + style = PillPrimary, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = modifier.basicMarquee(), + ) +} + +@Composable +private fun RecordingText(event: IslandEvent.ScreenRecording, modifier: Modifier, color: Color) { + if (event.isCountdown) { + Text(event.countdownSeconds.toString(), color = color, style = PillMono, modifier = modifier) + return + } + var elapsedMs by remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) +} + +@Composable +private fun MicCamText(event: IslandEvent.MicCamActive, modifier: Modifier, overrideColor: Color? = null) { + val cam = stringResource(R.string.ax_dynamic_bar_cam_short) + val mic = stringResource(R.string.ax_dynamic_bar_mic_short) + val label = buildString { + if (event.isCam) append(cam) + if (event.isMic && event.isCam) append(" · ") + if (event.isMic) append(mic) + } + val color = overrideColor ?: if (event.isCam) RedAccent else OrangeAccent + MarqueeLabel(event.appName.ifEmpty { label }, color, modifier) +} + +@Composable +private fun AudioRecText(event: IslandEvent.AudioRecording, modifier: Modifier, overrideColor: Color? = null) { + when (event.state) { + RecordingState.RECORDING, RecordingState.PAUSED -> { + var elapsedMs by remember { mutableLongStateOf(0L) } + LaunchedEffect(event.startTimeMs, event.state, event.pausedDurationMs) { + if (event.state == RecordingState.RECORDING) { + while (true) { + elapsedMs = + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + delay(1000) + } + } else { + elapsedMs = + (System.currentTimeMillis() - event.startTimeMs - event.pausedDurationMs) + .coerceAtLeast(0L) + } + } + val color = overrideColor ?: eventStyleFor(event).accent + Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) + } + RecordingState.SAVED -> MarqueeLabel(stringResource(R.string.ax_dynamic_bar_saved), overrideColor ?: GreenAccent, modifier) + } +} + +@Composable +private fun MediaText(event: IslandEvent.Media, modifier: Modifier, overrideColor: Color? = null) { + val baseColor = overrideColor ?: OrangeAccent + val alpha = if (event.isPlaying) 1f else AlphaHint + val color = baseColor.copy(alpha = alpha) + WaveformAnimation(color = color, modifier = modifier.size(20.dp, 12.dp), isAnimating = event.isPlaying) +} + +@Composable +private fun BtText(event: IslandEvent.Bluetooth, modifier: Modifier, overrideColor: Color? = null) { + val color = overrideColor ?: BlueAccent + if (event.batteryLevel >= 0) { + Text( + "${event.deviceName.take(8)} ${event.batteryLevel}%", + color = color, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = modifier.basicMarquee(), + ) + } else { + MarqueeLabel(event.deviceName.take(12), color, modifier) + } +} + +@Composable +private fun TimerText(event: IslandEvent.Timer, modifier: Modifier, overrideColor: Color? = null) { + if (event.endTimeMs > 0L) { + val color = overrideColor ?: if (event.isPaused) SubtleGray else BlueAccent + if (event.isPaused) { + Text(stringResource(R.string.ax_dynamic_bar_paused), color = color, style = PillMono, modifier = modifier) + } else { + var remainingMs by + remember(event.endTimeMs) { + mutableLongStateOf((event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L)) + } + LaunchedEffect(event.endTimeMs) { + while (remainingMs > 0L) { + delay(500) + remainingMs = (event.endTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + } + } + Text(formatCountdownLong(remainingMs), color = color, style = PillMono, modifier = modifier) + } + } else { + MarqueeLabel(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_timer) }, overrideColor ?: BlueAccent, modifier) + } +} + +@Composable +private fun StopwatchText(event: IslandEvent.Stopwatch, modifier: Modifier, overrideColor: Color? = null) { + val color = overrideColor ?: if (event.isRunning) MintAccent else SubtleGray + if (!event.isRunning) { + Text(stringResource(R.string.ax_dynamic_bar_paused), color = color, style = PillMono, modifier = modifier) + } else { + var elapsedMs by + remember(event.startTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.startTimeMs) { + while (true) { + delay(200) + elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) + } + } + Text(formatStopwatch(elapsedMs), color = color, style = PillMono, modifier = modifier) + } +} + +@Composable +private fun CallTimerText(event: IslandEvent.Notification, modifier: Modifier, overrideColor: Color? = null) { + val isActive = event.appName == "Phone:active" + if (isActive) { + var elapsedMs by remember(event.callStartTimeMs) { + mutableLongStateOf((System.currentTimeMillis() - event.callStartTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(event.callStartTimeMs) { + while (true) { + delay(1000) + elapsedMs = (System.currentTimeMillis() - event.callStartTimeMs).coerceAtLeast(0L) + } + } + val color = overrideColor ?: GreenAccent + Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) + } else { + MarqueeLabel(event.senderName ?: event.title ?: stringResource(R.string.ax_dynamic_bar_incoming_call), overrideColor ?: BlueAccent, modifier) + } +} + +@Composable +private fun NotifBellBadge(modifier: Modifier, count: Int) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Icon(Icons.Filled.Notifications, null, tint = BlueAccent, modifier = Modifier.size(SizeBadge)) + if (count > 1) { + Box( + modifier = + Modifier.align(Alignment.TopEnd) + .offset(x = 5.dp, y = (-3).dp) + .defaultMinSize(minWidth = SpaceLg, minHeight = SpaceLg) + .background(RedAccent, RoundedCornerShape(SpaceSm)) + .padding(horizontal = SpaceXxs), + contentAlignment = Alignment.Center, + ) { + Text( + if (count > 99) "99+" else "$count", + color = chipContentColorOn(RedAccent), + style = TsBadge, + lineHeight = SpaceLg.value.sp, + maxLines = 1, + ) + } + } + } +} + +@Composable +fun BadgedContactIcon( + mainIcon: Drawable, + badgeIcon: Drawable, + mainSize: Dp, + badgeSize: Dp, + isRound: Boolean = true, +) { + Box(modifier = Modifier.size(mainSize)) { + Image( + bitmap = mainIcon.toScaledBitmap(mainSize), + contentDescription = null, + modifier = + Modifier.size(mainSize) + .clip(if (isRound) CircleShape else ShapeXs), + contentScale = ContentScale.Crop, + ) + Image( + bitmap = badgeIcon.toScaledBitmap(badgeSize), + contentDescription = null, + modifier = + Modifier.size(badgeSize).align(Alignment.BottomEnd).clip(RoundedCornerShape(3.dp)), + contentScale = ContentScale.Crop, + ) + } +} + +@Composable +fun PulsingDot(color: Color, size: Dp = 8.dp, durationMs: Int = 600, minAlpha: Float = AlphaDisabled) { + val transition = rememberInfiniteTransition(label = "pulse_${color.value}") + val alpha by + transition.animateFloat( + initialValue = 1f, + targetValue = minAlpha, + animationSpec = + infiniteRepeatable(tween(durationMs, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "pulse_alpha", + ) + Box(modifier = Modifier.size(size).clip(CircleShape).background(color.copy(alpha = alpha))) +} + +@Composable +fun WaveformAnimation(color: Color, modifier: Modifier = Modifier.size(34.dp, 20.dp), isAnimating: Boolean = true, barCount: Int = 4) { + val phase: Float + if (isAnimating) { + val transition = rememberInfiniteTransition(label = "waveform") + val animatedPhase by + transition.animateFloat( + initialValue = 0f, + targetValue = 2f * PI.toFloat(), + animationSpec = + infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Restart), + label = "wave_phase", + ) + phase = animatedPhase + } else { + phase = 0f + } + Canvas(modifier = modifier) { + val barW = size.width / (barCount * 2.8f) + val maxH = size.height * 0.82f + val staticH = if (!isAnimating) maxH * 0.35f else 0f + val gap = (size.width - barCount * barW) / (barCount + 1) + for (i in 0 until barCount) { + val x = gap + i * (barW + gap) + barW / 2f + val h = if (isAnimating) maxH * (0.22f + 0.78f * ((sin(phase + i * 0.9f) + 1f) / 2f)) else staticH + val y = (size.height - h) / 2f + drawLine( + color = color, + start = Offset(x, y), + end = Offset(x, y + h), + strokeWidth = barW, + cap = StrokeCap.Round, + ) + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 990426366eb8..2661b91e39b9 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -22,6 +22,8 @@ import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification import com.android.systemui.ax.AxPlatformServiceImpl +import com.android.systemui.axdynamicbar.domain.AxDynamicBarChipsRefiner +import com.android.systemui.axdynamicbar.ui.AxDynamicBarManager import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.bouncer.domain.startable.BouncerStartable @@ -53,6 +55,7 @@ import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwit import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.smartpixels.SmartPixelsReceiver import com.android.systemui.statusbar.ImmersiveModeConfirmation +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import com.android.systemui.statusbar.gesture.GesturePointerEventListener import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.notification.headsup.StatusBarHeadsUpChangeListener @@ -66,6 +69,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet /** * DEPRECATED: DO NOT ADD THINGS TO THIS FILE. b/427499553 @@ -348,4 +352,13 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(AxPlatformServiceImpl::class) abstract fun bindAxPlatformService(impl: AxPlatformServiceImpl): CoreStartable + + @Binds + @IntoMap + @ClassKey(AxDynamicBarManager::class) + abstract fun bindAxDynamicBarManager(impl: AxDynamicBarManager): CoreStartable + + @Binds + @IntoSet + abstract fun bindDynamicBarChipsRefiner(impl: AxDynamicBarChipsRefiner): OngoingActivityChipsRefiner } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index d4213cdae88c..5959b30bd719 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopu import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection +import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection @@ -77,6 +78,7 @@ constructor( infoWidgetsSection: InfoWidgetsSection, keyguardClockStyleSection: KeyguardClockStyleSection, aodStyleSection: AODStyleSection, + axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { override val id: String = DEFAULT @@ -86,6 +88,7 @@ constructor( accessibilityActionsSection, defaultIndicationAreaSection, defaultIndicationAreaTopSection, + axDynamicBarKeyguardChipSection, defaultShortcutsSection, defaultAmbientIndicationAreaSection.getOrNull(), defaultSettingsPopupMenuSection, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index fe73165df7ae..575f6844587a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAr import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection +import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule import com.android.systemui.keyguard.ui.view.layout.sections.InfoWidgetsSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardClockStyleSection @@ -76,6 +77,7 @@ constructor( mediaSection: SplitShadeMediaSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, keyguardSliceViewSection: KeyguardSliceViewSection, + axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, ) : KeyguardBlueprint { override val id: String = ID @@ -84,6 +86,7 @@ constructor( listOfNotNull( accessibilityActionsSection, defaultIndicationAreaSection, + axDynamicBarKeyguardChipSection, defaultShortcutsSection, defaultAmbientIndicationAreaSection.getOrNull(), defaultSettingsPopupMenuSection, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt new file mode 100644 index 000000000000..4b8f5515e8b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -0,0 +1,174 @@ +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.compose.theme.PlatformTheme +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel +import com.android.systemui.axdynamicbar.ui.compose.AxDynamicBarKeyguardChip +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.util.ScrimUtils +import com.android.systemui.util.Utils +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.combine + +class AxDynamicBarKeyguardChipSection +@Inject +constructor( + @ShadeDisplayAware private val context: Context, + private val viewModel: AxDynamicBarChipViewModel, + private val indicationController: KeyguardIndicationController, +) : KeyguardSection() { + + private val chipViewId = R.id.ax_dynamic_bar_keyguard_chip + private var bindHandle: DisposableHandle? = null + private var expansionHandle: DisposableHandle? = null + private var enforceAction: Runnable? = null + + private var isCurrentlyHiding = false + + override fun addViews(constraintLayout: ConstraintLayout) { + val composeView = ComposeView(context).apply { id = chipViewId } + constraintLayout.addView(composeView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + val composeView: ComposeView = constraintLayout.requireViewById(chipViewId) + + if (viewModel.isEnabled.value && viewModel.isKeyguardEnabled.value && viewModel.isOnKeyguard.value) { + indicationController.setSuppressIndication(true) + } + + composeView.setContent { + PlatformTheme { + AxDynamicBarKeyguardChip(viewModel = viewModel) + } + } + + bindHandle = composeView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + combine(viewModel.isOnKeyguard, viewModel.isEnabled, viewModel.isKeyguardEnabled) { onKeyguard, enabled, kgEnabled -> + onKeyguard && enabled && kgEnabled + }.collect { suppress -> + indicationController.setSuppressIndication(suppress) + } + } + } + + expansionHandle = composeView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.isKeyguardExpanded.collect { expanded -> + isCurrentlyHiding = expanded + enforceHiddenViews(constraintLayout, expanded) + + enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } + enforceAction = if (expanded) { + Runnable { enforceHiddenViews(constraintLayout, true) }.also { + ScrimUtils.get().addKeyguardPreDrawAction(it) + } + } else null + + val lp = composeView.layoutParams as ConstraintLayout.LayoutParams + if (expanded) { + lp.width = ConstraintLayout.LayoutParams.MATCH_PARENT + lp.height = 0 + lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID + lp.topMargin = Utils.getStatusBarHeaderHeightKeyguard(context) + lp.bottomToTop = R.id.start_button + lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID + lp.bottomMargin = 0 + + lp.topToBottom = -1 + lp.bottomToBottom = -1 + lp.startToEnd = -1 + lp.endToStart = -1 + } else { + lp.width = ViewGroup.LayoutParams.WRAP_CONTENT + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT + lp.topMargin = 0 + + lp.bottomToBottom = R.id.start_button + lp.topToTop = -1 + + lp.startToEnd = R.id.start_button + lp.endToStart = R.id.end_button + lp.bottomMargin = 0 + + lp.bottomToTop = -1 + lp.startToStart = -1 + lp.endToEnd = -1 + lp.topToBottom = -1 + } + composeView.layoutParams = lp + } + } + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + val expanded = viewModel.isKeyguardExpanded.value + constraintSet.apply { + + if (expanded) { + constrainWidth(chipViewId, ConstraintSet.MATCH_CONSTRAINT) + constrainHeight(chipViewId, ConstraintSet.MATCH_CONSTRAINT) + connect(chipViewId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, + Utils.getStatusBarHeaderHeightKeyguard(context)) + connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.TOP) + connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + } else { + constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + + connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.BOTTOM) + + connect(chipViewId, ConstraintSet.START, R.id.start_button, ConstraintSet.END) + connect(chipViewId, ConstraintSet.END, R.id.end_button, ConstraintSet.START) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } + enforceAction = null + expansionHandle?.dispose() + expansionHandle = null + bindHandle?.dispose() + bindHandle = null + indicationController.setSuppressIndication(false) + constraintLayout.removeView(chipViewId) + } + + private val preserveOnExpandIds = setOf( + chipViewId, + R.id.start_button, + R.id.end_button, + ) + + private fun enforceHiddenViews(constraintLayout: ConstraintLayout, hide: Boolean) { + for (i in 0 until constraintLayout.childCount) { + val child = constraintLayout.getChildAt(i) + if (child.id in preserveOnExpandIds) continue + val target = if (hide) View.INVISIBLE else View.VISIBLE + if (child.visibility != target) child.visibility = target + } + constraintLayout.rootView + .findViewById(R.id.shared_notification_container) + ?.let { v -> + val target = if (hide) View.INVISIBLE else View.VISIBLE + if (v.visibility != target) v.visibility = target + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 769bdc3b37a5..62c65601155d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -25,6 +25,7 @@ import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static android.security.Flags.secureLockDevice; import static android.view.View.GONE; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE; @@ -216,6 +217,7 @@ public class KeyguardIndicationController { private BiometricSourceType mBiometricMessageSource; private ColorStateList mInitialTextColorState; private boolean mVisible; + private boolean mSuppressIndication; private boolean mOrganizationOwnedDevice; // these all assume the device is plugged in (wired/wireless/docked) AND chargingOrFull: @@ -317,6 +319,29 @@ public void onScreenTurnedOff() { private final DeviceEntryBiometricSettingsInteractor mDeviceEntryBiometricSettingsInteractor; private AuthenticationFlags mAuthenticationFlags; + public interface IndicationListener { + void onIndicationUpdated(int type, @Nullable CharSequence text); + } + public static final int AX_TYPE_BIOMETRIC = 0; + public static final int AX_TYPE_TRANSIENT = 1; + public static final int AX_TYPE_TRUST = 2; + public static final int AX_TYPE_DISCLOSURE = 3; + public static final int AX_TYPE_OWNER_INFO = 4; + public static final int AX_TYPE_ALIGNMENT = 5; + public static final int AX_TYPE_PERSISTENT_UNLOCK = 6; + private final java.util.List mIndicationListeners = + new java.util.ArrayList<>(); + public void addIndicationListener(IndicationListener listener) { + mIndicationListeners.add(listener); + } + public void removeIndicationListener(IndicationListener listener) { + mIndicationListeners.remove(listener); + } + private void notifyIndicationListeners(int type, @Nullable CharSequence text) { + for (IndicationListener listener : mIndicationListeners) { + listener.onIndicationUpdated(type, text); + } + } /** * Creates a new KeyguardIndicationController and registers callbacks. */ @@ -631,11 +656,13 @@ private void updateLockScreenDisclosureMsg() { .setTextColor(getInitialTextColorState()) .build(), /* updateImmediately */ false); + notifyIndicationListeners(AX_TYPE_DISCLOSURE, disclosure); } }); }); } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_DISCLOSURE); + notifyIndicationListeners(AX_TYPE_DISCLOSURE, null); } } @@ -687,8 +714,10 @@ private void updateLockScreenOwnerInfo() { .setTextColor(getInitialTextColorState()) .build(), false); + notifyIndicationListeners(AX_TYPE_OWNER_INFO, finalInfo); } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_OWNER_INFO); + notifyIndicationListeners(AX_TYPE_OWNER_INFO, null); } }); }); @@ -752,9 +781,11 @@ private void updateBiometricMessage() { .build(), true ); + notifyIndicationListeners(AX_TYPE_BIOMETRIC, mBiometricMessage); } else { mRotateTextViewController.hideIndication( INDICATION_TYPE_BIOMETRIC_MESSAGE); + notifyIndicationListeners(AX_TYPE_BIOMETRIC, null); } if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) { mRotateTextViewController.updateIndication( @@ -780,8 +811,10 @@ private void updateTransient() { if (!TextUtils.isEmpty(mTransientIndication)) { mRotateTextViewController.showTransient(mTransientIndication); + notifyIndicationListeners(AX_TYPE_TRANSIENT, mTransientIndication); } else { mRotateTextViewController.hideTransient(); + notifyIndicationListeners(AX_TYPE_TRANSIENT, null); } } @@ -796,6 +829,7 @@ private void updateLockScreenTrustMsg(int userId, CharSequence trustGrantedIndic .setTextColor(getInitialTextColorState()) .build(), true); + notifyIndicationListeners(AX_TYPE_TRUST, trustGrantedIndication); hideBiometricMessage(); } else if (!TextUtils.isEmpty(trustManagedIndication) && mKeyguardUpdateMonitor.getUserTrustIsManaged(userId) @@ -807,8 +841,10 @@ private void updateLockScreenTrustMsg(int userId, CharSequence trustGrantedIndic .setTextColor(getInitialTextColorState()) .build(), false); + notifyIndicationListeners(AX_TYPE_TRUST, trustManagedIndication); } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_TRUST); + notifyIndicationListeners(AX_TYPE_TRUST, null); } } @@ -822,8 +858,10 @@ private void updateLockScreenAlignmentMsg() { mContext.getColor(R.color.misalignment_text_color))) .build(), true); + notifyIndicationListeners(AX_TYPE_ALIGNMENT, mAlignmentIndication); } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_ALIGNMENT); + notifyIndicationListeners(AX_TYPE_ALIGNMENT, null); } } @@ -836,8 +874,10 @@ private void updateLockScreenPersistentUnlockMsg() { .setTextColor(getInitialTextColorState()) .build(), true); + notifyIndicationListeners(AX_TYPE_PERSISTENT_UNLOCK, mPersistentUnlockMessage); } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE); + notifyIndicationListeners(AX_TYPE_PERSISTENT_UNLOCK, null); } } @@ -948,6 +988,19 @@ private int getWorkProfileUserId(int userId) { return UserHandle.USER_NULL; } + public void setSuppressIndication(boolean suppress) { + mSuppressIndication = suppress; + if (mTopIndicationView != null) { + mTopIndicationView.setSuppressVisibility(suppress); + } + if (mLockScreenIndicationView != null) { + mLockScreenIndicationView.setSuppressVisibility(suppress); + } + if (!suppress && mVisible) { + updateDeviceEntryIndication(false); + } + } + /** * Sets the visibility of keyguard bottom area, and if the indications are updatable. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 7ebdc16d1478..dca5667fe09e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -47,6 +47,7 @@ import android.service.notification.Flags import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.internal.messages.nano.SystemMessageProto.SystemMessage +import com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker @@ -115,6 +116,15 @@ class PeekDisabledSuppressor( } } +class PeekAxDynamicBarSuppressor( + private val settings: AxDynamicBarSettings, +) : VisualInterruptionCondition( + types = setOf(PEEK), + reason = "suppressed by AxDynamicBar" +) { + override fun shouldSuppress(): Boolean = settings.isNotificationEventsActive() +} + class PulseDisabledSuppressor( private val ambientDisplayConfiguration: AmbientDisplayConfiguration, private val userTracker: UserTracker, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index bd7681b2adf2..82de28a78ac9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -39,6 +39,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -87,6 +88,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private final GlobalSettings mGlobalSettings; private final EventLog mEventLog; private final Optional mBubbles; + private final AxDynamicBarSettings mAxDynamicBarSettings; @VisibleForTesting protected boolean mUseHeadsUp = false; @@ -137,7 +139,9 @@ public NotificationInterruptStateProviderImpl( SystemClock systemClock, GlobalSettings globalSettings, EventLog eventLog, - Optional bubbles) { + Optional bubbles, + AxDynamicBarSettings axDynamicBarSettings) { + mAxDynamicBarSettings = axDynamicBarSettings; mPowerManager = powerManager; mBatteryController = batteryController; mAmbientDisplayConfiguration = ambientDisplayConfiguration; @@ -431,6 +435,11 @@ private boolean shouldHeadsUpWhenAwake(NotificationEntry entry, boolean log) { return false; } + if (mAxDynamicBarSettings.isNotificationEventsActive()) { + if (log) mLogger.logNoHeadsUpFeatureDisabled(); + return false; + } + if (!canAlertCommon(entry, log)) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 13f11aad33bc..c0de0488b400 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -77,6 +77,7 @@ constructor( private val notificationManager: NotificationManager, private val settingsInteractor: NotificationSettingsInteractor, private val deviceProvisioningInteractor: DeviceProvisioningInteractor, + private val axDynamicBarSettings: com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings, ) : VisualInterruptionDecisionProvider { init { @@ -166,6 +167,7 @@ constructor( check(!started) addCondition(PeekDisabledSuppressor(globalSettings, headsUpManager, logger, mainHandler)) + addCondition(PeekAxDynamicBarSuppressor(axDynamicBarSettings)) addCondition(PulseDisabledSuppressor(ambientDisplayConfiguration, userTracker)) addCondition(PulseBatterySaverSuppressor(batteryController)) addFilter(PeekPackageSnoozedSuppressor(headsUpManager)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java index f3dcdea95af7..74b0bb9dab34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java @@ -54,11 +54,27 @@ public class KeyguardIndicationTextView extends DoubleShadowTextView { private static int sButtonStyleId = R.style.TextAppearance_Keyguard_BottomArea_Button; private boolean mAnimationsEnabled = true; + private boolean mSuppressVisibility = false; private CharSequence mMessage; private KeyguardIndication mKeyguardIndicationInfo; private Animator mLastAnimator; + public void setSuppressVisibility(boolean suppress) { + mSuppressVisibility = suppress; + if (suppress) { + super.setVisibility(View.INVISIBLE); + } + } + + @Override + public void setVisibility(int visibility) { + if (mSuppressVisibility && visibility == View.VISIBLE) { + super.setVisibility(View.INVISIBLE); + return; + } + super.setVisibility(visibility); + } public KeyguardIndicationTextView(Context context) { super(context); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 97810281e1c4..2addbd98f93b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -35,6 +35,7 @@ import android.view.DisplayCutout; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -98,6 +99,7 @@ import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder; import com.android.systemui.statusbar.ui.viewmodel.KeyguardStatusBarViewModel; +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel; import com.android.systemui.tuner.TunerService; import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.ViewController; @@ -178,6 +180,7 @@ public class KeyguardStatusBarViewController extends ViewController updateViewState()); } + TextView carrierLabel = mView.findViewById(R.id.keyguard_carrier_text); + if (carrierLabel != null) { + carrierLabel.addTextChangedListener(new android.text.TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override public void afterTextChanged(android.text.Editable s) { + mAxDynamicBarChipViewModel.updateKeyguardCarrierText(s.toString()); + } + }); + mAxDynamicBarChipViewModel.updateKeyguardCarrierText( + carrierLabel.getText().toString()); + } if (NewStatusBarIcons.isEnabled()) { if (!SceneContainerFlag.isEnabled()) { mBatteryComposeView = createAndBindComposeBattery(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 805692bb5f3c..db9090d5521b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -77,6 +78,8 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ui.composable.VariableDayDate import com.android.systemui.statusbar.StatusBarAlwaysUseRegionSampling +import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel +import com.android.systemui.axdynamicbar.ui.compose.AxDynamicBarChip import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.RudimentaryBattery @@ -143,6 +146,7 @@ constructor( @DisplayAware private val homeStatusBarViewBinder: HomeStatusBarViewBinder, @DisplayAware private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, + private val axDynamicBarChipViewModel: AxDynamicBarChipViewModel, ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) @@ -167,6 +171,7 @@ constructor( mediaViewModelFactory = mediaViewModelFactory, statusBarRegionSamplingViewModelFactory = statusBarRegionSamplingViewModelFactory, + axDynamicBarChipViewModel = axDynamicBarChipViewModel, onViewCreated = andThen, modifier = Modifier.sysUiResTagContainer(), ) @@ -206,6 +211,7 @@ fun StatusBarRoot( mediaHost: MediaHost, mediaViewModelFactory: MediaViewModel.Factory, statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, + axDynamicBarChipViewModel: AxDynamicBarChipViewModel, onViewCreated: (ViewGroup) -> Unit, modifier: Modifier = Modifier, ) { @@ -266,6 +272,7 @@ fun StatusBarRoot( statusBarViewModel = statusBarViewModel, iconViewStore = iconViewStore, appHandlesViewModel = appHandlesViewModel, + axDynamicBarChipViewModel = axDynamicBarChipViewModel, context = context, ) } @@ -407,6 +414,7 @@ private fun addStartSideComposable( statusBarViewModel: HomeStatusBarViewModel, iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, appHandlesViewModel: AppHandlesViewModel, + axDynamicBarChipViewModel: AxDynamicBarChipViewModel, context: Context, ) { val startSideExceptHeadsUp = @@ -425,9 +433,7 @@ private fun addStartSideComposable( LinearLayout.LayoutParams.WRAP_CONTENT, ) .apply { - if (showDate) { - gravity = android.view.Gravity.CENTER_VERTICAL - } + gravity = android.view.Gravity.CENTER_VERTICAL } setContent { @@ -488,15 +494,19 @@ private fun addStartSideComposable( ) } + val axEnabled by axDynamicBarChipViewModel.interactor.settings.isEnabled.collectAsState() + if (axEnabled) { + AxDynamicBarChip( + viewModel = axDynamicBarChipViewModel, + modifier = Modifier.widthIn(max = chipsMaxWidth), + ) + } val chipsVisibilityModel = statusBarViewModel.ongoingActivityChips if (chipsVisibilityModel.areChipsAllowed) { OngoingActivityChips( chips = chipsVisibilityModel.chips, iconViewStore = iconViewStore, onChipBoundsChanged = statusBarViewModel::onChipBoundsChanged, - // TODO(b/393581408): Now that we always enforce a max width on the chips, - // we should be able to convert the chips to a LazyRow and get some - // animations for free. modifier = Modifier.sysUiResTagContainer().widthIn(max = chipsMaxWidth), ) } diff --git a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt index 3fb11e43467a..b7bfc0e5c6ae 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt @@ -19,6 +19,8 @@ package com.android.systemui.util import android.os.Handler import android.os.Looper import android.service.notification.StatusBarNotification +import android.view.View +import android.view.ViewTreeObserver import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import java.util.concurrent.atomic.AtomicBoolean @@ -40,6 +42,9 @@ class ScrimUtils private constructor() { fun onUserChanged() {} fun setPulsing(pulsing: Boolean) {} fun onNotificationPosted(sbn: StatusBarNotification) {} + fun onNotificationRemoved(sbn: StatusBarNotification) {} + fun onKeyguardLayoutChanged() {} + fun onKeyguardAlphaChanged(alpha: Float) {} } private val listeners = WeakListenerManager() @@ -60,6 +65,8 @@ class ScrimUtils private constructor() { private var keyguardRetryRunnable: Runnable? = null companion object { + private const val LAYOUT_STABLE_DELAY = 350L + @Volatile private var instance: ScrimUtils? = null @JvmStatic @@ -159,6 +166,60 @@ class ScrimUtils private constructor() { listeners.notifyOnMain { it.onNotificationPosted(sbn) } } + fun onNotificationRemoved(sbn: StatusBarNotification) { + listeners.notifyOnMain { it.onNotificationRemoved(sbn) } + } + + private var keyguardRootView: View? = null + private var layoutChangePending = false + private var layoutStableRunnable: Runnable? = null + private val preDrawActions = mutableListOf() + + private val keyguardPreDrawListener = ViewTreeObserver.OnPreDrawListener { + val root = keyguardRootView ?: return@OnPreDrawListener true + + for (action in preDrawActions) { + action.run() + } + + if (mKeyguardShowing == true && root.isDirty) { + if (!layoutChangePending) { + layoutChangePending = true + listeners.notifyOnMain { it.onKeyguardLayoutChanged() } + } + layoutStableRunnable?.let { mainHandler.removeCallbacks(it) } + layoutStableRunnable = Runnable { layoutChangePending = false } + mainHandler.postDelayed(layoutStableRunnable!!, LAYOUT_STABLE_DELAY) + } + true + } + + fun addKeyguardPreDrawAction(action: Runnable) { + if (!preDrawActions.contains(action)) preDrawActions.add(action) + } + + fun removeKeyguardPreDrawAction(action: Runnable) { + preDrawActions.remove(action) + } + + fun attachKeyguardView(view: View) { + detachKeyguardView() + keyguardRootView = view + view.viewTreeObserver.addOnPreDrawListener(keyguardPreDrawListener) + } + + fun detachKeyguardView() { + keyguardRootView?.viewTreeObserver?.removeOnPreDrawListener(keyguardPreDrawListener) + keyguardRootView = null + layoutStableRunnable?.let { mainHandler.removeCallbacks(it) } + layoutStableRunnable = null + layoutChangePending = false + } + + fun setKeyguardAlpha(alpha: Float) { + listeners.notifyOnMain { it.onKeyguardAlphaChanged(alpha) } + } + fun isDozing(): Boolean = mIsDozing == true fun isAwake(): Boolean = mAwake == true fun isPulsing(): Boolean = mPulsing.get() From d66b1a0db05a27d626382ebdabf2b2cdc8fcb622 Mon Sep 17 00:00:00 2001 From: Saikrishna1504 Date: Tue, 31 Mar 2026 17:41:49 +0000 Subject: [PATCH 1107/1315] SystemUI: Suppress Dynamic Bar notifications when Danmaku is active Change-Id: I8c436985a1c7c2a96453f5163022ff279e82af90 Signed-off-by: Saikrishna1504 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../domain/AxDynamicBarInteractor.kt | 18 +++++++++++++----- .../domain/AxDynamicBarSettings.kt | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt index cdcf6eb4ea63..5fb4de53e8d7 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt @@ -2,6 +2,7 @@ package com.android.systemui.axdynamicbar.domain import android.app.Notification import android.media.AudioManager +import android.net.Uri import android.provider.Settings.Global import com.android.systemui.axdynamicbar.data.IslandEventRepository import com.android.systemui.axdynamicbar.model.IslandEvent @@ -20,17 +21,17 @@ import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.util.settings.GlobalSettings import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import android.net.Uri @SysUISingleton class AxDynamicBarInteractor @@ -47,6 +48,7 @@ constructor( private val shadeInteractor: ShadeInteractor, private val shadeRepository: ShadeRepository, private val zenModeController: ZenModeController, + private val globalSettings: GlobalSettings, private val audioManager: AudioManager, ) : IslandActions { private val _uiState = MutableStateFlow(IslandUiState()) @@ -57,10 +59,10 @@ constructor( private val dismissedEventIds: MutableSet = ConcurrentHashMap.newKeySet() - var onCollapseRequested: (() -> Unit)? = null - override var onFocusableRequested: ((Boolean) -> Unit)? = null + var onCollapseRequested: (() -> Unit)? = null + private var isInitialized = false @Volatile private var panelBlocking = false @@ -219,6 +221,12 @@ constructor( } } + applicationScope.launch { + settings.isHeadsUpEnabled.collect { enabled -> + if (!enabled) dismissNotificationAlert() + } + } + applicationScope.launch { combine( repository.events, @@ -427,6 +435,7 @@ constructor( private fun shouldSuppressForDndOrRinger(notification: IslandEvent.Notification): Boolean { if (notification.isActiveCall()) return false + if (!settings.isHeadsUpEnabled.value) return true val category = notification.sbn.notification?.category if (category == Notification.CATEGORY_CALL || category == Notification.CATEGORY_ALARM) return false val zenMode = zenModeController.zen @@ -574,4 +583,3 @@ constructor( else -> null } } - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index 926c9c30ff4d..af6101f040db 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -3,10 +3,12 @@ package com.android.systemui.axdynamicbar.domain import android.database.ContentObserver import android.os.Handler import android.os.UserHandle +import android.provider.Settings.Global import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.shared.EVENT_TYPE_IDS import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -18,6 +20,7 @@ import org.json.JSONArray class AxDynamicBarSettings @Inject constructor( @Main private val mainHandler: Handler, private val secureSettings: SecureSettings, + private val globalSettings: GlobalSettings, ) { companion object { const val KEY_ENABLED = "ax_dynamic_bar_enabled" @@ -35,6 +38,9 @@ class AxDynamicBarSettings @Inject constructor( private val _compactNotifications = MutableStateFlow(true) val compactNotifications: StateFlow = _compactNotifications.asStateFlow() + private val _isHeadsUpEnabled = MutableStateFlow(true) + val isHeadsUpEnabled: StateFlow = _isHeadsUpEnabled.asStateFlow() + private val _disabledEventTypes = MutableStateFlow>(emptySet()) val disabledEventTypes: StateFlow> = _disabledEventTypes.asStateFlow() @@ -75,12 +81,18 @@ class AxDynamicBarSettings @Inject constructor( settingsObserver, UserHandle.USER_ALL, ) + globalSettings.registerContentObserverSync( + Global.HEADS_UP_NOTIFICATIONS_ENABLED, + false, + settingsObserver, + ) } fun destroy() { if (!initialized) return initialized = false secureSettings.getContentResolver().unregisterContentObserver(settingsObserver) + globalSettings.getContentResolver().unregisterContentObserver(settingsObserver) } private fun refresh() { @@ -90,6 +102,8 @@ class AxDynamicBarSettings @Inject constructor( secureSettings.getIntForUser(KEY_KEYGUARD_ENABLED, 1, UserHandle.USER_CURRENT) == 1 _compactNotifications.value = secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 + _isHeadsUpEnabled.value = + globalSettings.getInt(Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1) == 1 val json = secureSettings.getStringForUser(KEY_EVENTS, UserHandle.USER_CURRENT) ?: "" _disabledEventTypes.value = @@ -112,4 +126,3 @@ class AxDynamicBarSettings @Inject constructor( fun isNotificationEventsActive(): Boolean = _isEnabled.value && "notification" !in _disabledEventTypes.value } - From 8330379b55e297b8648a6750bf2b98dd5ecfd1c5 Mon Sep 17 00:00:00 2001 From: bijoyv9 Date: Sun, 29 Mar 2026 13:40:52 +0000 Subject: [PATCH 1108/1315] SystemUI: DynamicBar: Align lockscreen chip with keyguard indication margin On devices with a low-positioned UDFPS icon, the lockscreen chip can overlap and render behind it in collapsed mode. Align the chip with the keyguard indication margin and use the `keyguard_indication_margin_bottom` overlay to adjust placement per device as needed. Change-Id: I4f253bc37ba3f565f4c480ce30a4f3beabfc7d9e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../AxDynamicBarKeyguardChipSection.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 4b8f5515e8b0..88e3d1717b28 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -97,17 +97,17 @@ constructor( lp.width = ViewGroup.LayoutParams.WRAP_CONTENT lp.height = ViewGroup.LayoutParams.WRAP_CONTENT lp.topMargin = 0 - - lp.bottomToBottom = R.id.start_button + + lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID + lp.bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom) lp.topToTop = -1 - - lp.startToEnd = R.id.start_button - lp.endToStart = R.id.end_button - lp.bottomMargin = 0 - + + lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID + lp.startToEnd = -1 + lp.endToStart = -1 + lp.bottomToTop = -1 - lp.startToStart = -1 - lp.endToEnd = -1 lp.topToBottom = -1 } composeView.layoutParams = lp @@ -131,11 +131,10 @@ constructor( } else { constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - - connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.BOTTOM) - - connect(chipViewId, ConstraintSet.START, R.id.start_button, ConstraintSet.END) - connect(chipViewId, ConstraintSet.END, R.id.end_button, ConstraintSet.START) + val marginBottom = context.resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom) + connect(chipViewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, marginBottom) + connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) } } } From 0000692b5cfd25b1ee197ba2fd1d8a212cf9314c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:27:53 +0800 Subject: [PATCH 1109/1315] SystemUI: DynamicBar: Fixing issues | enhance fix notification animation oscillation during expansion - use aosp transition api fix resource usage when showing media chip - remove waveform animation and use 1 iteration marquee low udfps position algorithm chip content changes transform animaton Change-Id: I9087d504bf126c7d8b5d35486f9d39e6b0564026 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/Android.bp | 1 + .../data/source/AppTrackingIslandManager.kt | 14 + .../data/source/MediaIslandManager.kt | 55 +- .../data/source/NotificationIslandManager.kt | 41 +- .../domain/AxDynamicBarInteractor.kt | 19 + .../domain/AxDynamicBarSettings.kt | 4 + .../axdynamicbar/model/IslandEvent.kt | 6 +- .../shared/IslandContentTokens.kt | 90 ++- .../ui/AxDynamicBarChipViewModel.kt | 8 +- .../ui/AxDynamicBarExpandedPanel.kt | 32 +- .../ui/compose/AxDynamicBarChip.kt | 47 +- .../ui/compose/AxDynamicBarKeyguardChip.kt | 237 ++++--- .../ui/compose/AxDynamicBarNowBar.kt | 21 +- .../ui/compose/ExpandedMediaContent.kt | 13 +- .../ui/compose/ExpandedOngoingContent.kt | 8 +- .../ui/compose/ExpandedRecordingContent.kt | 4 +- .../ui/compose/KeyguardExpandedContent.kt | 18 +- .../ui/compose/NotificationAlertCard.kt | 641 +++++++++++------- .../ui/compose/PillIslandContent.kt | 24 +- .../AxDynamicBarKeyguardChipSection.kt | 54 +- 20 files changed, 850 insertions(+), 487 deletions(-) diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 259cd3c110ff..dea91f93d406 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -596,6 +596,7 @@ android_library { "FadingEdgeLayoutLib", "smartspace-proto-java", "smartspace-proto-lite-java", + "ax_compose", ], javacflags: [ "-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas", diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt index 7529aa5db630..76c811d16b8a 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt @@ -58,6 +58,7 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont currentForegroundPkg = null return } + if (!hasLauncherIntent(pkg)) return val leavingPkg = currentForegroundPkg currentForegroundPkg = pkg @@ -134,6 +135,7 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont fun stopListening() { if (!listening) return listening = false + launcherIntentCache.clear() TaskStackChangeListeners.getInstance().unregisterTaskStackListener(listener) synchronized(recentApps) { recentApps.clear() @@ -165,6 +167,18 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont } } + private val launcherIntentCache = HashMap() + + private fun hasLauncherIntent(pkg: String): Boolean = + launcherIntentCache.getOrPut(pkg) { + val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkg) + try { + context.packageManager.queryIntentActivities(intent, 0).isNotEmpty() + } catch (_: Exception) { + false + } + } + private fun resolveLauncherPackage(): String? = try { val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt index 36e925bebdff..bc6fe23e23bd 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon as DrawableIcon import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.MediaMetadata @@ -20,6 +21,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.MediaSessionManager import com.android.systemui.media.NotificationMediaManager import com.android.systemui.media.dialog.MediaOutputDialogManager +import com.android.systemui.util.concurrency.RepeatableExecutor import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,11 +33,13 @@ class MediaIslandManager constructor( @Application private val context: Context, @Main private val mainHandler: Handler, + @Main private val mainExecutor: RepeatableExecutor, private val notificationMediaManager: NotificationMediaManager, private val mediaOutputDialogManager: MediaOutputDialogManager, ) { companion object { private const val TAG = "MediaIslandManager" + private const val POSITION_UPDATE_INTERVAL_MS = 1000L } private val _mediaEvent = MutableStateFlow(null) @@ -125,17 +129,43 @@ constructor( if (updateTime <= 0) return basePos val elapsed = SystemClock.elapsedRealtime() - updateTime val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f - val duration = _mediaEvent.value?.duration ?: Long.MAX_VALUE + val duration = _mediaEvent.value?.duration?.takeIf { it > 0L } ?: Long.MAX_VALUE return (basePos + (elapsed * speed).toLong()).coerceIn(0L, duration) } + private var cancelProgressPolling: Runnable? = null + + private fun tickProgress() { + val ev = _mediaEvent.value ?: return + if (!ev.isPlaying || ev.duration <= 0L) return + val now = SystemClock.elapsedRealtime() + val elapsed = now - ev.positionUpdateTime + val pos = (ev.position + (elapsed * ev.playbackSpeed).toLong()).coerceIn(0L, ev.duration) + val prog = (pos.toFloat() / ev.duration).coerceIn(0f, 1f) + _mediaEvent.value = ev.copy(progress = prog, position = pos, positionUpdateTime = now) + } + + fun startProgressPolling() { + if (cancelProgressPolling != null) return + cancelProgressPolling = mainExecutor.executeRepeatedly( + ::tickProgress, 0L, POSITION_UPDATE_INTERVAL_MS, + ) + } + + fun stopProgressPolling() { + cancelProgressPolling?.run() + cancelProgressPolling = null + } + private fun updatePosition(state: PlaybackState) { val current = _mediaEvent.value ?: return val duration = current.duration.takeIf { it > 0L } ?: return val posMs = computeAccuratePosition(state) val progress = (posMs.toFloat() / duration.toFloat()).coerceIn(0f, 1f) val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f + val playing = isInMotion(state) _mediaEvent.value = current.copy( + isPlaying = playing, position = posMs, progress = progress, playbackSpeed = speed, @@ -178,16 +208,21 @@ constructor( if (duration > 0L) (posMs.toFloat() / duration.toFloat()).coerceIn(0f, 1f) else 0f val outputDevice = getOutputDeviceName() + val pkg = controller?.packageName val customActions = ps?.customActions?.take(2)?.mapNotNull { ca -> val lbl = ca.name?.toString()?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null val act = ca.action?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null - IslandEvent.MediaCustomAction(label = lbl, action = act) + val icon = try { + if (ca.icon != 0 && pkg != null) { + DrawableIcon.createWithResource(pkg, ca.icon) + .loadDrawable(context) + } else null + } catch (_: Exception) { null } + IslandEvent.MediaCustomAction(label = lbl, action = act, icon = icon) } ?: emptyList() - - val pkg = controller?.packageName val appIcon = sessionAppIcon val speed = ps?.playbackSpeed?.takeIf { it > 0f } ?: 1f @@ -248,6 +283,7 @@ constructor( fun stopListening() { if (!listening) return listening = false + stopProgressPolling() notificationMediaManager.removeCallback(mediaListener) MediaSessionManager.get().removeListener(mediaSessionListener) try { @@ -263,15 +299,20 @@ constructor( } fun clear() { + stopProgressPolling() _mediaEvent.value = null } fun togglePlayPause() { val c = getActiveController() ?: return - when (c.playbackState?.state) { - PlaybackState.STATE_PLAYING -> c.transportControls.pause() - else -> c.transportControls.play() + val playing = c.playbackState?.state == PlaybackState.STATE_PLAYING + if (playing) { + c.transportControls.pause() + } else { + c.transportControls.play() } + val current = _mediaEvent.value ?: return + _mediaEvent.value = current.copy(isPlaying = !playing) } fun skipNext() { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt index 106a06f9fed7..6d3dbe585501 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt @@ -127,7 +127,7 @@ constructor( var activeMediaPackageProvider: (() -> String?)? = null - private val seenNotificationKeys = mutableSetOf() + private val seenNotificationPostTimes = mutableMapOf() var onTimerEvent: ((IslandEvent.Timer) -> Unit)? = null var onAlarmEvent: ((IslandEvent.Alarm) -> Unit)? = null @@ -145,7 +145,7 @@ constructor( object : ScrimUtils.ScrimEventListener { override fun onNotificationRemoved(sbn: StatusBarNotification) { val pkg = sbn.packageName ?: return - seenNotificationKeys.remove(sbn.key) + seenNotificationPostTimes.remove(sbn.key) if (sbn.key == timerNotificationKey) { timerNotificationKey = null @@ -362,6 +362,10 @@ constructor( if ("promoted_ongoing" !in disabledTypes) handlePromotedOngoing(sbn, extras, pkg) return } + if (!sbn.isOngoing) { + _promotedOngoingEvents.value = + _promotedOngoingEvents.value.filter { it.sbn.key != sbn.key } + } if (sbn.isOngoing) return if ("notification" in disabledTypes) return val category = sbn.notification?.category @@ -379,7 +383,33 @@ constructor( it.isNotEmpty() } ?: extras.getString("android.text") - if (!seenNotificationKeys.add(sbn.key)) return + val notif = sbn.notification + val isMessagingStyle = + notif != null && notif.isStyle(Notification.MessagingStyle::class.java) + val latestMessageTime: Long = + if (isMessagingStyle) { + val msgs = + notif?.extras?.getParcelableArray( + Notification.EXTRA_MESSAGES, + Parcelable::class.java, + ) + if (msgs != null && msgs.isNotEmpty()) { + Notification.MessagingStyle.Message + .getMessagesFromBundleArray(msgs) + .maxOfOrNull { it.timestamp } ?: 0L + } else 0L + } else 0L + + val postTime = sbn.postTime + val signature = + if (isMessagingStyle && latestMessageTime > 0L) latestMessageTime + else postTime + val lastSignature = seenNotificationPostTimes.put(sbn.key, signature) + if (lastSignature != null) { + if (signature == lastSignature) return + if (!isMessagingStyle && + notifFlags and Notification.FLAG_ONLY_ALERT_ONCE != 0) return + } val icon = try { @@ -426,10 +456,9 @@ constructor( var isConversation = false var isGroupConversation = false var conversationTitle: String? = null - val notif = sbn.notification if ( notif != null && - notif.isStyle(Notification.MessagingStyle::class.java) && + isMessagingStyle && notif.extras != null ) { isConversation = true @@ -522,7 +551,7 @@ constructor( if (!listening) return listening = false ScrimUtils.get().removeListener(scrimListener) - seenNotificationKeys.clear() + seenNotificationPostTimes.clear() timerJob?.cancel() timerJob = null _timerEvent.value = null diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt index 5fb4de53e8d7..4a9a1c769d99 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt @@ -27,6 +27,9 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -144,6 +147,22 @@ constructor( } } + applicationScope.launch { + combine( + _uiState.map { state -> + state.shouldShow && + state.events.any { it is IslandEvent.Media && it.isPlaying && it.duration > 0L } + }, + _isPanelExpanded, + qsExpansion.map { it > 0f }, + ) { mediaActive, panelExpanded, qsOpen -> + mediaActive && !panelExpanded && !qsOpen + }.distinctUntilChanged().collect { needsPolling -> + if (needsPolling) repository.media.startProgressPolling() + else repository.media.stopProgressPolling() + } + } + _isOnKeyguard.value = statusBarStateController.state == StatusBarState.KEYGUARD _isKeyguardFadingAway.value = keyguardStateController.isKeyguardFadingAway _isBouncerShowing.value = keyguardStateController.isPrimaryBouncerShowing diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index af6101f040db..7ab82754c948 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -44,6 +44,10 @@ class AxDynamicBarSettings @Inject constructor( private val _disabledEventTypes = MutableStateFlow>(emptySet()) val disabledEventTypes: StateFlow> = _disabledEventTypes.asStateFlow() + init { + refresh() + } + private val settingsObserver = object : ContentObserver(mainHandler) { override fun onChange(selfChange: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt index 5d2d8822c1aa..e0e623e0d93b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt @@ -276,7 +276,11 @@ sealed class IslandEvent(open val priority: Int, val id: String) : Comparable 0L) { - val elapsed = SystemClock.elapsedRealtime() - event.positionUpdateTime - (event.position + (elapsed * event.playbackSpeed).toLong()).coerceIn(0L, duration) - } else { - event.position - } - positionMs = computedPosition - progress = if (duration > 0L) (computedPosition.toFloat() / duration).coerceIn(0f, 1f) - else event.progress - - LaunchedEffect(event.isPlaying, event.duration) { - if (!currentEvent.isPlaying || currentEvent.duration <= 0L) return@LaunchedEffect - while (true) { - delay(1000L) - val ev = currentEvent - if (!ev.isPlaying || ev.duration <= 0L) break - val elapsed = SystemClock.elapsedRealtime() - ev.positionUpdateTime - val current = (ev.position + (elapsed * ev.playbackSpeed).toLong()) - .coerceIn(0L, ev.duration) - positionMs = current - progress = (current.toFloat() / ev.duration).coerceIn(0f, 1f) - } - } - - return MediaProgress(progress, positionMs) + return MediaProgress(event.progress, event.position) } internal fun formatElapsedTime(ms: Long): String { val secs = (ms / 1000).coerceAtLeast(0) - return "%d:%02d".format(secs / 60, secs % 60) + return "%02d:%02d".format(secs / 60, secs % 60) +} + +internal fun formatCountdownSeconds(secs: Long): String { + val s = secs.coerceAtLeast(0) + return "%02d:%02d".format(s / 60, s % 60) } internal fun formatCountdownLong(ms: Long): String { val secs = (ms / 1000).coerceAtLeast(0) val mins = secs / 60 val hrs = mins / 60 - return if (hrs > 0) "%d:%02d:%02d".format(hrs, mins % 60, secs % 60) - else "%d:%02d".format(mins, secs % 60) + return if (hrs > 0) "%02d:%02d:%02d".format(hrs, mins % 60, secs % 60) + else "%02d:%02d".format(mins, secs % 60) } internal fun formatStopwatch(ms: Long): String { @@ -536,7 +515,7 @@ internal fun textKeyFor(event: IslandEvent): Any = else -> event.id } -internal fun resolveCustomActionIcon(label: String): ImageVector { +internal fun resolveLabelIcon(label: String): ImageVector { val lower = label.lowercase() return when { lower.contains("shuffle") -> Icons.Filled.Shuffle @@ -548,14 +527,21 @@ internal fun resolveCustomActionIcon(label: String): ImageVector { } } -internal fun resolveEndActionIcon(label: String): ImageVector { - val lower = label.lowercase() - return when { - lower.contains("like") || lower.contains("love") || lower.contains("favorite") -> Icons.Filled.FavoriteBorder - lower.contains("thumb") && lower.contains("up") -> Icons.Filled.ThumbUp - lower.contains("thumb") && lower.contains("down") -> Icons.Filled.ThumbDown - lower.contains("repeat") -> Icons.Filled.Repeat - else -> Icons.Filled.FavoriteBorder +@Composable +internal fun CustomActionIcon( + ca: IslandEvent.MediaCustomAction, + tint: Color, + modifier: Modifier = Modifier, +) { + val appBitmap = ca.icon?.let { drawable -> + remember(drawable) { + try { drawable.toBitmap(48, 48).asImageBitmap() } catch (_: Exception) { null } + } + } + if (appBitmap != null) { + Icon(appBitmap, ca.label, tint = tint, modifier = modifier) + } else { + Icon(resolveLabelIcon(ca.label), ca.label, tint = tint, modifier = modifier) } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index be4050894885..8c9f0e5f7713 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -1,6 +1,5 @@ package com.android.systemui.axdynamicbar.ui -import android.os.SystemProperties import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.biometrics.AuthController @@ -54,11 +53,9 @@ constructor( udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { - val isCompactKeyguardChip: StateFlow = + val isLowUdfps: StateFlow = udfpsOverlayInteractor.udfpsOverlayParams .map { params -> - val propOverride = SystemProperties.getInt(PROP_COMPACT_CHIP, -1) - if (propOverride >= 0) return@map propOverride == 1 if (!authController.isUdfpsSupported) return@map false val sensorBottom = params.sensorBounds.bottom val displayHeight = params.naturalDisplayHeight @@ -199,8 +196,7 @@ constructor( } companion object { - private const val PROP_COMPACT_CHIP = "persist.sys.axdb.compact_chip" - private const val LOW_UDFPS_THRESHOLD = 0.75f + private const val LOW_UDFPS_THRESHOLD = 0.93f } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index 041df07145c7..b2623b351e1f 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -35,6 +35,9 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.dp +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry @@ -114,8 +117,7 @@ constructor( viewModel.isExpanded .onEach { expanded -> - updateOverlayFocusability(expanded) - updateOverlaySize(expanded) + updateOverlay(expanded) } .launchIn(applicationScope) } @@ -149,6 +151,7 @@ constructor( view.setViewTreeLifecycleOwner(lifecycleOwner) view.setViewTreeSavedStateRegistryOwner(lifecycleOwner) + view.setViewTreeOnBackPressedDispatcherOwner(lifecycleOwner) panelLifecycleOwner = lifecycleOwner val isCurrentlyExpanded = viewModel.isExpanded.value @@ -194,28 +197,20 @@ constructor( panelLifecycleOwner = null } - private fun updateOverlayFocusability(expanded: Boolean) = ensureMainThread { - val view = overlayView ?: return@ensureMainThread - val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread - if (expanded) { - params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() - } else { - params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - } - windowManager.updateViewLayout(view, params) - } - private var shrinkRunnable: Runnable? = null - private fun updateOverlaySize(expanded: Boolean) = ensureMainThread { + private fun updateOverlay(expanded: Boolean) = ensureMainThread { shrinkRunnable?.let { mainHandler.removeCallbacks(it) } shrinkRunnable = null val view = overlayView ?: return@ensureMainThread val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread if (expanded) { + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() params.height = WindowManager.LayoutParams.MATCH_PARENT windowManager.updateViewLayout(view, params) } else { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + windowManager.updateViewLayout(view, params) val runnable = Runnable { val v = overlayView ?: return@Runnable val p = v.layoutParams as? WindowManager.LayoutParams ?: return@Runnable @@ -263,11 +258,9 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight val notifVisible = remember { MutableTransitionState(false) } LaunchedEffect(isExpanded) { - delay(16) expandedVisible.targetState = isExpanded } LaunchedEffect(showNotif) { - delay(16) notifVisible.targetState = showNotif } @@ -375,9 +368,11 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight } } -private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner { +private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner, + OnBackPressedDispatcherOwner { private val lifecycleRegistry = LifecycleRegistry(this) private val savedStateRegistryController = SavedStateRegistryController.create(this) + private val backDispatcher = OnBackPressedDispatcher() override val lifecycle: Lifecycle get() = lifecycleRegistry @@ -385,6 +380,9 @@ private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner { override val savedStateRegistry get() = savedStateRegistryController.savedStateRegistry + override val onBackPressedDispatcher: OnBackPressedDispatcher + get() = backDispatcher + init { savedStateRegistryController.performRestore(null) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt index b79221e2c0c8..8a338086f7bb 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -2,8 +2,14 @@ package com.android.systemui.axdynamicbar.ui.compose import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.runtime.LaunchedEffect +import kotlin.math.abs import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.animation.fadeIn @@ -164,10 +170,9 @@ fun AxDynamicBarChip( AnimatedContent( targetState = ChipDisplay(displayEvent, isAlert), transitionSpec = { - ((fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.92f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith - (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.92f, animationSpec = motionScheme.fastSpatialSpec()))).using( - sizeTransform = null - ) + (fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.92f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.92f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) }, contentKey = { if (it.isAlert) "alert" else it.event::class.simpleName }, label = "chip_event", @@ -179,10 +184,15 @@ fun AxDynamicBarChip( ) val rawProgress = chipProgressFor(display.event) val progressTarget = rawProgress ?: 0f - val animatedProgress by animateFloatAsState( - progressTarget, MaterialTheme.motionScheme.defaultSpatialSpec(), label = "progress", - ) - val progress = if (rawProgress != null) animatedProgress else null + val progressAnim = remember { Animatable(progressTarget) } + LaunchedEffect(progressTarget) { + if (abs(progressTarget - progressAnim.value) > 0.05f) { + progressAnim.animateTo(progressTarget, tween(300, easing = FastOutSlowInEasing)) + } else { + progressAnim.snapTo(progressTarget) + } + } + val progress = if (rawProgress != null) progressAnim.value else null Box( modifier = Modifier.fillMaxHeight(), @@ -193,6 +203,7 @@ fun AxDynamicBarChip( Modifier.height(ChipHeight) .clip(ChipShape) .background(accent) + .animateContentSize(motionScheme.defaultSpatialSpec()) .then( if (progress != null) { val trackColor = lerp(accent, contentColor, 0.2f) @@ -263,7 +274,7 @@ fun AxDynamicBarChip( color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = 100.dp).basicMarquee(), + modifier = Modifier.widthIn(max = 100.dp).basicMarquee(iterations = 1), ) } } @@ -284,8 +295,11 @@ fun AxDynamicBarChip( AnimatedContent( targetState = display.event, transitionSpec = { - (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) - .using(sizeTransform = null) + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) }, contentKey = { iconKeyFor(it) }, label = "chip_icon", @@ -296,8 +310,11 @@ fun AxDynamicBarChip( AnimatedContent( targetState = display.event, transitionSpec = { - (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) - .using(sizeTransform = null) + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) }, contentKey = { textKeyFor(it) }, label = "chip_text", @@ -305,7 +322,7 @@ fun AxDynamicBarChip( ) { event -> PillEventText( event, - Modifier.widthIn(max = 100.dp), + Modifier.widthIn(max = 88.dp), overrideColor = contentColor, ) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index ac4a37158df5..0a2004a121af 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -1,9 +1,15 @@ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.systemui.axdynamicbar.ui.compose import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing +import kotlin.math.abs import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState @@ -86,20 +92,13 @@ import android.graphics.drawable.Drawable import java.util.Calendar private val ChipHeight = 36.dp -private val ChipHeightCompact = 28.dp private val ChipShape = ShapeChip private val ChipIconSize = ChipHeight - SpaceLg -private val ChipIconSizeCompact = ChipHeightCompact - SpaceMd private val ActionSize = SpacePanel -private val ActionSizeCompact = SizeIconSm private val ActionIconSize = SizeBadge -private val ActionIconSizeCompact = SpaceLg private val BatteryIconSize = ChipHeight - SpaceXxl -private val BatteryIconSizeCompact = ChipHeightCompact - SpaceXxl private val CountBadgeHeight = ChipHeight / 2 -private val CountBadgeHeightCompact = ChipHeightCompact / 2 -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun AxDynamicBarKeyguardChip( viewModel: AxDynamicBarChipViewModel, @@ -111,11 +110,8 @@ fun AxDynamicBarKeyguardChip( val isKeyguardEnabled by viewModel.isKeyguardEnabled.collectAsStateWithLifecycle() val batteryInfo by viewModel.keyguardBatteryInfo.collectAsStateWithLifecycle() val isKeyguardExpanded by viewModel.isKeyguardExpanded.collectAsStateWithLifecycle() - val isCompact by viewModel.isCompactKeyguardChip.collectAsStateWithLifecycle() val touchSlop = LocalViewConfiguration.current.touchSlop - val chipHeight = if (isCompact) ChipHeightCompact else ChipHeight - val motionScheme = MaterialTheme.motionScheme Box(modifier = modifier) { @@ -143,7 +139,7 @@ fun AxDynamicBarKeyguardChip( modifier = Modifier .fillMaxWidth() .align(Alignment.TopStart) - .padding(bottom = chipHeight + SpaceLg), + .padding(bottom = ChipHeight + SpaceLg), ) { state?.let { KeyguardExpandedContent( @@ -198,13 +194,13 @@ fun AxDynamicBarKeyguardChip( AnimatedContent( targetState = displayEvent, transitionSpec = { - ((fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn( + (fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn( initialScale = 0.95f, animationSpec = motionScheme.defaultSpatialSpec(), )) togetherWith (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut( targetScale = 0.95f, animationSpec = motionScheme.fastSpatialSpec(), - ))).using(sizeTransform = null) + )) using SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) }, contentKey = { it::class.simpleName }, label = "keyguard_chip_event", @@ -222,12 +218,15 @@ fun AxDynamicBarKeyguardChip( ) val rawProgress = chipProgressFor(event) val progressTarget = rawProgress ?: 0f - val animatedProgress by animateFloatAsState( - progressTarget, - MaterialTheme.motionScheme.defaultSpatialSpec(), - label = "kg_progress", - ) - val progress = if (rawProgress != null) animatedProgress else null + val progressAnim = remember { Animatable(progressTarget) } + LaunchedEffect(progressTarget) { + if (abs(progressTarget - progressAnim.value) > 0.05f) { + progressAnim.animateTo(progressTarget, tween(300, easing = FastOutSlowInEasing)) + } else { + progressAnim.snapTo(progressTarget) + } + } + val progress = if (rawProgress != null) progressAnim.value else null KeyguardChipBody( event = event, @@ -236,12 +235,11 @@ fun AxDynamicBarKeyguardChip( progress = progress, eventCount = chipState.eventCount, viewModel = viewModel, - compact = isCompact, ) } } else { - KeyguardBatteryChip(batteryInfo, isCompact) + KeyguardBatteryChip(batteryInfo) } } } @@ -255,26 +253,18 @@ private fun KeyguardChipBody( progress: Float?, eventCount: Int, viewModel: AxDynamicBarChipViewModel, - compact: Boolean, ) { val context = LocalContext.current - val height = if (compact) ChipHeightCompact else ChipHeight - val iconSize = if (compact) ChipIconSizeCompact else ChipIconSize - val actSize = if (compact) ActionSizeCompact else ActionSize - val actIconSize = if (compact) ActionIconSizeCompact else ActionIconSize - val badgeHeight = if (compact) CountBadgeHeightCompact else CountBadgeHeight - val textStyle = if (compact) MaterialTheme.typography.labelSmall else PillPrimary - val maxWidth = if (compact) 220.dp else 260.dp - val startPad = if (compact) SpaceXs else SpaceSm - val endPad = if (compact) SpaceSm else SpaceMd + val motionScheme = MaterialTheme.motionScheme Box(contentAlignment = Alignment.Center) { Row( modifier = Modifier - .height(height) - .widthIn(max = maxWidth) + .height(ChipHeight) + .widthIn(max = 260.dp) .clip(ChipShape) .background(accent) + .animateContentSize(motionScheme.defaultSpatialSpec()) .then( if (progress != null) { val trackColor = lerp(accent, contentColor, 0.2f) @@ -298,31 +288,58 @@ private fun KeyguardChipBody( else -> viewModel.togglePanel() } } - .padding(start = startPad, end = endPad), + .padding(start = SpaceSm, end = SpaceMd), verticalAlignment = Alignment.CenterVertically, ) { if (event is IslandEvent.Media) { - event.albumArt?.let { art -> - Image( - bitmap = art.toScaledBitmap(iconSize), - contentDescription = null, - modifier = Modifier - .size(iconSize) - .clip(ShapeXs), - contentScale = ContentScale.Crop, - ) - } ?: PillEventIcon(event, tint = contentColor) + AnimatedContent( + targetState = event.albumArt, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false) + }, + contentKey = { it?.hashCode() ?: 0 }, + label = "kg_media_icon", + ) { art -> + if (art != null) { + Image( + bitmap = art.toScaledBitmap(ChipIconSize), + contentDescription = null, + modifier = Modifier + .size(ChipIconSize) + .clip(ShapeXs), + contentScale = ContentScale.Crop, + ) + } else { + PillEventIcon(event, tint = contentColor) + } + } Spacer(Modifier.width(SpaceXs)) - Box(modifier = Modifier.weight(1f, fill = false)) { - if (event.artist.isNotBlank()) { + AnimatedContent( + targetState = event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) + }, + contentKey = { "${it.track}|${it.artist}" }, + label = "kg_media_text", + modifier = Modifier.weight(1f, fill = false), + ) { ev -> + if (ev.artist.isNotBlank()) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, - style = textStyle, + ev.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, + style = PillPrimary, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = if (compact) 70.dp else 90.dp).basicMarquee(), + modifier = Modifier.widthIn(max = 90.dp).basicMarquee(iterations = 1), ) Text( " · ", @@ -330,26 +347,35 @@ private fun KeyguardChipBody( color = contentColor.copy(alpha = AlphaHint), ) Text( - event.artist, + ev.artist, style = MaterialTheme.typography.labelSmall, color = contentColor.copy(alpha = AlphaSecondary), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = if (compact) 44.dp else 60.dp), + modifier = Modifier.widthIn(max = 60.dp), ) } } else { - MarqueeText(event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, contentColor, Modifier) + MarqueeText(ev.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_music) }, contentColor, Modifier) } } Spacer(Modifier.width(SpaceXs)) + ActionButton( + icon = ActionIcon.SKIP_PREV, + color = contentColor, + bgColor = lerp(accent, contentColor, AlphaSubtle), + onClick = { viewModel.skipPrev() }, + size = ActionSize, + iconSize = ActionIconSize, + ) + Spacer(Modifier.width(SpaceXxs)) Surface( onClick = { viewModel.togglePlayPause() }, - modifier = Modifier.size(actSize), + modifier = Modifier.size(ActionSize), shape = CircleShape, color = lerp(accent, contentColor, AlphaSubtle), ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(actSize)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(ActionSize)) { Icon( if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, contentDescription = stringResource( @@ -357,41 +383,79 @@ private fun KeyguardChipBody( else R.string.ax_dynamic_bar_play, ), tint = contentColor, - modifier = Modifier.size(actIconSize), + modifier = Modifier.size(ActionIconSize), ) } } + Spacer(Modifier.width(SpaceXxs)) + ActionButton( + icon = ActionIcon.SKIP_NEXT, + color = contentColor, + bgColor = lerp(accent, contentColor, AlphaSubtle), + onClick = { viewModel.skipNext() }, + size = ActionSize, + iconSize = ActionIconSize, + ) } else if (event is IslandEvent.Sports && event.team2Name.isNotEmpty()) { SportsChipTeamBadge(event.team1Name, event.team1Icon, contentColor) Spacer(Modifier.width(SpaceXs)) Text( if (event.score1.isNotEmpty()) "${event.score1} - ${event.score2}" else stringResource(R.string.ax_dynamic_bar_sports_vs), - style = textStyle, + style = PillPrimary, color = contentColor, maxLines = 1, ) Spacer(Modifier.width(SpaceXs)) SportsChipTeamBadge(event.team2Name, event.team2Icon, contentColor) } else { - PillEventIcon(event, tint = contentColor) + AnimatedContent( + targetState = event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) + }, + contentKey = { iconKeyFor(it) }, + label = "kg_chip_icon", + ) { ev -> + PillEventIcon(ev, tint = contentColor) + } Spacer(Modifier.width(SpaceXs)) - KeyguardPrimaryText(event, contentColor, Modifier.weight(1f, fill = false)) - val secondary = secondaryTextFor(event) - if (secondary != null) { - Text( - " · ", - style = MaterialTheme.typography.labelSmall, - color = contentColor.copy(alpha = AlphaTertiary), - ) - Text( - secondary, - style = MaterialTheme.typography.labelSmall, - color = contentColor.copy(alpha = AlphaSecondary), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = if (compact) 60.dp else 80.dp), - ) + AnimatedContent( + targetState = event, + transitionSpec = { + (fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith + (fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using + SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) + }, + contentKey = { textKeyFor(it) }, + label = "kg_chip_text", + modifier = Modifier.weight(1f, fill = false), + ) { ev -> + Row(verticalAlignment = Alignment.CenterVertically) { + KeyguardPrimaryText(ev, contentColor, Modifier.weight(1f, fill = false)) + val secondary = secondaryTextFor(ev) + if (secondary != null) { + Text( + " · ", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaTertiary), + ) + Text( + secondary, + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 80.dp), + ) + } + } } } @@ -406,8 +470,8 @@ private fun KeyguardChipBody( color = contentColor, bgColor = lerp(accent, contentColor, AlphaSubtle), onClick = { action.perform(viewModel, event, context) }, - size = actSize, - iconSize = actIconSize, + size = ActionSize, + iconSize = ActionIconSize, ) } } @@ -418,8 +482,8 @@ private fun KeyguardChipBody( Box( contentAlignment = Alignment.Center, modifier = Modifier - .height(badgeHeight) - .widthIn(min = badgeHeight) + .height(CountBadgeHeight) + .widthIn(min = CountBadgeHeight) .background(lerp(accent, contentColor, AlphaDisabled), ShapeChip) .padding(horizontal = SpaceXxs), ) { @@ -436,30 +500,28 @@ private fun KeyguardChipBody( } @Composable -private fun KeyguardBatteryChip(info: KeyguardBatteryInfo, compact: Boolean) { +private fun KeyguardBatteryChip(info: KeyguardBatteryInfo) { val accent = when { info.isCharging -> BatteryChargingColor info.isPowerSave -> BatteryPowerSaveColor else -> BatteryNeutralColor } val contentColor = chipContentColorOn(accent) - val height = if (compact) ChipHeightCompact else ChipHeight - val battIconSize = if (compact) BatteryIconSizeCompact else BatteryIconSize Box(contentAlignment = Alignment.Center) { Row( modifier = Modifier - .height(height) + .height(ChipHeight) .clip(ChipShape) .background(accent) - .padding(horizontal = if (compact) SpaceSm else SpaceMd), + .padding(horizontal = SpaceMd), verticalAlignment = Alignment.CenterVertically, ) { if (info.isCharging) { - AnimatedChargingBoltIcon(contentColor, battIconSize) + AnimatedChargingBoltIcon(contentColor, BatteryIconSize) } else { - AnimatedBatteryFillIcon(info.level, contentColor, battIconSize) + AnimatedBatteryFillIcon(info.level, contentColor, BatteryIconSize) } Spacer(Modifier.width(SpaceXs)) Text( @@ -535,7 +597,6 @@ private fun AnimatedChargingBoltIcon(color: Color, iconSize: Dp = BatteryIconSiz } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = BatteryIconSize) { val fillFraction by animateFloatAsState( @@ -578,7 +639,7 @@ private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = Bat private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modifier) { when (event) { is IslandEvent.ScreenRecording -> - if (event.isCountdown) MarqueeText(event.countdownSeconds.toString(), color, modifier) + if (event.isCountdown) MarqueeText(formatCountdownSeconds(event.countdownSeconds), color, modifier) else ElapsedTimeText(event.startTimeMs, color, modifier) is IslandEvent.AudioRecording -> when (event.state) { RecordingState.RECORDING -> ElapsedTimeText( @@ -644,7 +705,7 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi private fun secondaryTextFor(event: IslandEvent): String? = when (event) { is IslandEvent.Media -> event.artist.takeIf { it.isNotBlank() } is IslandEvent.ScreenRecording -> - if (event.isCountdown) event.countdownSeconds.toString() + if (event.isCountdown) formatCountdownSeconds(event.countdownSeconds) else stringResource(R.string.ax_dynamic_bar_rec_short) is IslandEvent.AudioRecording -> event.appName.takeIf { it.isNotBlank() } is IslandEvent.Timer -> event.label.takeIf { it.isNotBlank() } @@ -797,7 +858,7 @@ private fun MarqueeText(text: String, color: Color, modifier: Modifier) { style = PillPrimary, maxLines = 1, overflow = TextOverflow.Clip, - modifier = modifier.widthIn(max = 120.dp).basicMarquee(), + modifier = modifier.widthIn(max = 120.dp).basicMarquee(iterations = 1), ) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt index 85c4a4c21a70..c700c3ce5e48 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt @@ -3,7 +3,11 @@ package com.android.systemui.axdynamicbar.ui.compose import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.runtime.LaunchedEffect +import kotlin.math.abs import androidx.compose.animation.core.animateIntAsState import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme @@ -111,10 +115,15 @@ fun AxDynamicBarNowBar( ) val rawProgress = chipProgressFor(display.event) val progressTarget = rawProgress ?: 0f - val animatedProgress by animateFloatAsState( - progressTarget, MaterialTheme.motionScheme.defaultSpatialSpec(), label = "progress", - ) - val progress = if (rawProgress != null) animatedProgress else null + val progressAnim = remember { Animatable(progressTarget) } + LaunchedEffect(progressTarget) { + if (abs(progressTarget - progressAnim.value) > 0.05f) { + progressAnim.animateTo(progressTarget, tween(300, easing = FastOutSlowInEasing)) + } else { + progressAnim.snapTo(progressTarget) + } + } + val progress = if (rawProgress != null) progressAnim.value else null Box( modifier = Modifier @@ -232,7 +241,7 @@ private fun AlertPillContent(event: IslandEvent, contentColor: Color) { color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.basicMarquee(), + modifier = Modifier.basicMarquee(iterations = 1), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index 8cd763529ccc..1cd0fda07168 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -353,6 +353,7 @@ private fun MediaSeekBar( modifier = Modifier.fillMaxWidth(), color = accent, trackColor = accent.copy(alpha = AlphaSubtle), + amplitude = { if (event.isPlaying) 1f else 0f }, ) } } @@ -374,11 +375,7 @@ private fun MediaCustomActionButton( modifier = Modifier.size(ControlButtonSize), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Icon( - resolveCustomActionIcon(ca.label), ca.label, - tint = accent, - modifier = Modifier.size(ControlIconSize), - ) + CustomActionIcon(ca, tint = accent, modifier = Modifier.size(ControlIconSize)) } } } else { @@ -416,11 +413,7 @@ private fun MediaEndActionButton( modifier = Modifier.size(ControlButtonSize), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Icon( - resolveEndActionIcon(ca.label), ca.label, - tint = accent, - modifier = Modifier.size(ControlIconSize), - ) + CustomActionIcon(ca, tint = accent, modifier = Modifier.size(ControlIconSize)) } } } else { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt index d889b3bc3ba9..dd37f8b91d9a 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt @@ -276,8 +276,12 @@ internal fun RowScope.CompactPromotedOngoingRow(event: IslandEvent.PromotedOngoi maxLines = 1, overflow = TextOverflow.Ellipsis, ) - if (event.shortText.isNotEmpty()) { - Text(event.shortText, color = BlueAccent, style = MaterialTheme.typography.labelSmall) + val subLabel = event.shortText.ifEmpty { + if ((event.progress >= 0f || event.isIndeterminate) && event.text.isNotEmpty()) event.text + else "" + } + if (subLabel.isNotEmpty()) { + Text(subLabel, color = BlueAccent, style = MaterialTheme.typography.labelSmall) } } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt index 281b0a2ea71f..e4c854c73dcc 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt @@ -88,7 +88,7 @@ private fun ScreenRecordCountdownExpanded(event: IslandEvent.ScreenRecording) { icon = { Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(22.dp)) }, title = { Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text(event.countdownSeconds.toString(), color = RedAccent, style = MaterialTheme.typography.headlineMedium) + Text(formatCountdownSeconds(event.countdownSeconds), color = RedAccent, style = MaterialTheme.typography.headlineMedium) }, ) } @@ -246,7 +246,7 @@ internal fun RowScope.CompactRecordingRow(event: IslandEvent.ScreenRecording) { Spacer(Modifier.width(SpaceLg)) Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = OnCardText, style = MaterialTheme.typography.bodySmall) - Text(event.countdownSeconds.toString(), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text(formatCountdownSeconds(event.countdownSeconds), color = SubtleGray, style = MaterialTheme.typography.labelSmall) } return } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt index 9bd2679f3c36..0032a071ace7 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -335,7 +335,6 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio val displayMs = if (isSeeking) (seekProgress * event.duration).toLong() else mediaProgress.positionMs - Column(verticalArrangement = Arrangement.spacedBy(SpaceSm)) { Box( modifier = Modifier @@ -372,6 +371,7 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio modifier = Modifier.fillMaxWidth(), color = colors.accent, trackColor = colors.accent.copy(alpha = AlphaSubtle), + amplitude = { if (event.isPlaying) 1f else 0f }, ) } Row( @@ -408,8 +408,11 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio modifier = Modifier.size(SizeButtonLg), ) { val ca = event.customActions.firstOrNull() - val caIcon = ca?.let { resolveCustomActionIcon(it.label) } ?: Icons.Filled.Shuffle - Icon(caIcon, ca?.label ?: stringResource(R.string.ax_dynamic_bar_shuffle), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + if (ca != null) { + CustomActionIcon(ca, tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } else { + Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } } IconButton( @@ -454,8 +457,11 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio modifier = Modifier.size(SizeButtonLg), ) { val ca = event.customActions.getOrNull(1) - val endIcon = ca?.let { resolveEndActionIcon(it.label) } ?: Icons.Filled.VolumeUp - Icon(endIcon, ca?.label ?: stringResource(R.string.ax_dynamic_bar_output), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + if (ca != null) { + CustomActionIcon(ca, tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } else { + Icon(Icons.Filled.VolumeUp, stringResource(R.string.ax_dynamic_bar_output), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) + } } } } @@ -664,7 +670,7 @@ private fun KeyguardRecordingPanel(event: IslandEvent.ScreenRecording, interacto } Text( - event.countdownSeconds.toString(), + formatCountdownSeconds(event.countdownSeconds), color = OnCardText, style = MaterialTheme.typography.displayLarge, ) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt index c6402a27731e..80136b0cf966 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt @@ -3,12 +3,11 @@ package com.android.systemui.axdynamicbar.ui.compose import android.app.Notification import android.app.PendingIntent import android.app.RemoteInput -import android.content.Context import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle import android.util.Log -import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -40,18 +39,14 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -68,9 +63,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState +import com.android.compose.animation.scene.transitions import com.android.systemui.axdynamicbar.shared.IslandActions import com.android.systemui.axdynamicbar.model.IslandEvent -import com.android.systemui.axdynamicbar.shared.ActionBg import com.android.systemui.axdynamicbar.shared.AlphaIconBg import com.android.systemui.axdynamicbar.shared.AlphaSubtle import com.android.systemui.axdynamicbar.shared.CardBg @@ -78,7 +78,6 @@ import com.android.systemui.axdynamicbar.shared.CardBorderBrush import com.android.systemui.axdynamicbar.shared.DarkCard import com.android.systemui.axdynamicbar.shared.ExpressivePillButton import com.android.systemui.axdynamicbar.shared.GreenAccent -import com.android.systemui.axdynamicbar.shared.OnActionText import com.android.systemui.axdynamicbar.shared.OnCardText import com.android.systemui.axdynamicbar.shared.RedAccent import com.android.systemui.axdynamicbar.shared.ShapeCard @@ -99,11 +98,53 @@ import com.android.systemui.axdynamicbar.shared.sendWithBal import com.android.systemui.axdynamicbar.shared.toScaledBitmap private const val MAX_EXPANDED_ACTIONS = 3 -private val NotifCompactHeight = 56.dp -private val NotifCollapsedHeight = 164.dp -private val NotifExpandedHeight = 358.dp private val ThumbnailSize = 44.dp +private object AlertCardScenes { + val Compact = SceneKey("AlertCompact") + val Collapsed = SceneKey("AlertCollapsed") + val Expanded = SceneKey("AlertExpanded") +} + +private object AlertCardElements { + val AppIcon = ElementKey("AlertAppIcon") + val CompactLabel = ElementKey("AlertCompactLabel") + val CompactExpand = ElementKey("AlertCompactExpand") + val Avatar = ElementKey("AlertAvatar") + val SenderName = ElementKey("AlertSenderName") + val CardContent = ElementKey("AlertCardContent") + val CardExpand = ElementKey("AlertCardExpand") +} + +private const val SPRING_STIFFNESS = 500f +private const val SPRING_DAMPING = 0.9f + +private val alertCardTransitions = transitions { + from(AlertCardScenes.Compact, to = AlertCardScenes.Expanded) { + spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) + sharedElement(AlertCardElements.AppIcon) + fade(AlertCardElements.CompactLabel) + fade(AlertCardElements.CompactExpand) + fade(AlertCardElements.CardContent) + fade(AlertCardElements.CardExpand) + } + from(AlertCardScenes.Compact, to = AlertCardScenes.Collapsed) { + spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) + sharedElement(AlertCardElements.AppIcon) + fade(AlertCardElements.CompactLabel) + fade(AlertCardElements.CompactExpand) + fade(AlertCardElements.CardContent) + fade(AlertCardElements.CardExpand) + } + from(AlertCardScenes.Collapsed, to = AlertCardScenes.Expanded) { + spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) + sharedElement(AlertCardElements.AppIcon) + sharedElement(AlertCardElements.Avatar) + sharedElement(AlertCardElements.SenderName) + sharedElement(AlertCardElements.CardExpand) + } +} + private suspend fun PointerInputScope.detectInteractionGesture( onStart: () -> Unit, onEnd: () -> Unit, @@ -130,36 +171,27 @@ fun NotificationAlertCard( initiallyCompact: Boolean = false, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val extras = notification.sbn.notification?.extras val isCall = extras?.let { it.containsKey(Notification.EXTRA_ANSWER_INTENT) || it.containsKey(Notification.EXTRA_DECLINE_INTENT) || it.containsKey(Notification.EXTRA_HANG_UP_INTENT) } ?: false - var isCompact by remember(notification.sbn.key) { mutableStateOf(initiallyCompact && !isCall) } - var showReply by remember { mutableStateOf(false) } - var isExpanded by remember { mutableStateOf(!isCompact) } - var isTextTruncated by remember { mutableStateOf(false) } val title = notification.title val body = notification.text val hasTitleAndBody = title != null && title != notification.senderName && !body.isNullOrEmpty() val hasExtraActions = notification.actions.size > 2 - val hasExpandableContent = !isCall && (isTextTruncated || hasTitleAndBody || hasExtraActions) + val hasExpandableContent = !isCall && (hasTitleAndBody || hasExtraActions) val accent = chipAccentColorFor(notification) - val density = LocalDensity.current - val compactHeightPx = with(density) { NotifCompactHeight.roundToPx() } - val minHeightPx = with(density) { NotifCollapsedHeight.roundToPx() } - val maxHeightPx = with(density) { NotifExpandedHeight.roundToPx() } - val effectiveMinPx = if (isCompact) compactHeightPx else minHeightPx - val effectiveMaxPx = if (isCompact) compactHeightPx else maxHeightPx - var measuredHeightPx by remember { mutableIntStateOf(effectiveMinPx) } - val heightAnim = remember { Animatable(effectiveMinPx.toFloat()) } - val motionScheme = MaterialTheme.motionScheme - val targetPx = measuredHeightPx.coerceIn(effectiveMinPx, effectiveMaxPx) - LaunchedEffect(targetPx) { - heightAnim.animateTo(targetPx.toFloat(), motionScheme.defaultSpatialSpec()) - } + var showReply by remember(notification.sbn.key) { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + val initialScene = if (initiallyCompact && !isCall) AlertCardScenes.Compact + else AlertCardScenes.Expanded + val stlState = rememberMutableSceneTransitionLayoutState( + initialScene = initialScene, + transitions = alertCardTransitions, + ) DisposableEffect(Unit) { onDispose { interactor.onNotificationAlertInteractionEnd() } @@ -175,15 +207,6 @@ fun NotificationAlertCard( ) { Column( modifier = Modifier.fillMaxWidth() - .layout { measurable, constraints -> - val placeable = measurable.measure( - constraints.copy(maxHeight = effectiveMaxPx), - ) - measuredHeightPx = placeable.height - val h = heightAnim.value.toInt().coerceIn(effectiveMinPx, effectiveMaxPx) - layout(placeable.width, h) { placeable.place(0, 0) } - } - .clipToBounds() .pointerInput(Unit) { detectInteractionGesture( onStart = interactor::onNotificationAlertInteractionStart, @@ -194,159 +217,262 @@ fun NotificationAlertCard( .background(CardBg) .border(1.dp, CardBorderBrush, ShapeCard), ) { - if (isCompact) { - Surface( - onClick = { isCompact = false; isExpanded = true }, - color = Color.Transparent, + SceneTransitionLayout(state = stlState) { + scene(AlertCardScenes.Compact) { + CompactScene( + notification = notification, + accent = accent, + onExpand = { + stlState.setTargetScene(AlertCardScenes.Expanded, scope) + }, + ) + } + scene(AlertCardScenes.Collapsed) { + CardScene( + notification = notification, + accent = accent, + expanded = false, + isCall = isCall, + initiallyCompact = initiallyCompact, + hasExpandableContent = hasExpandableContent, + hasTitleAndBody = hasTitleAndBody, + showReply = showReply, + interactor = interactor, + onDismiss = onDismiss, + onExpandToggle = { + stlState.setTargetScene(AlertCardScenes.Expanded, scope) + }, + onCollapseToCompact = { + stlState.setTargetScene(AlertCardScenes.Compact, scope) + }, + onShowReply = { showReply = true }, + onSentReply = { showReply = false; onDismiss() }, + ) + } + scene(AlertCardScenes.Expanded) { + CardScene( + notification = notification, + accent = accent, + expanded = true, + isCall = isCall, + initiallyCompact = initiallyCompact, + hasExpandableContent = hasExpandableContent, + hasTitleAndBody = hasTitleAndBody, + showReply = showReply, + interactor = interactor, + onDismiss = onDismiss, + onExpandToggle = { + val target = if (initiallyCompact) AlertCardScenes.Compact + else AlertCardScenes.Collapsed + stlState.setTargetScene(target, scope) + }, + onCollapseToCompact = { + stlState.setTargetScene(AlertCardScenes.Compact, scope) + }, + onShowReply = { showReply = true }, + onSentReply = { showReply = false; onDismiss() }, + ) + } + } + } + } +} + +@Composable +private fun ContentScope.CompactScene( + notification: IslandEvent.Notification, + accent: Color, + onExpand: () -> Unit, +) { + Surface( + onClick = onExpand, + color = Color.Transparent, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = SpaceXxl, vertical = SpaceMd), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + ) { + notification.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier + .element(AlertCardElements.AppIcon) + .size(SizeIconSm) + .clip(ShapeSm), + ) + } + Row( + modifier = Modifier + .element(AlertCardElements.CompactLabel) + .weight(1f), + horizontalArrangement = Arrangement.spacedBy(SpaceSm), + verticalAlignment = Alignment.CenterVertically, + ) { + val label = notification.senderName + ?: notification.title + ?: notification.appName + Text( + label, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val preview = notification.text ?: notification.title ?: "" + if (preview.isNotEmpty() && preview != label) { + Text( + "· $preview", + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } else { + Spacer(Modifier.weight(1f)) + } + } + ExpandChip( + Icons.Filled.ExpandMore, + accent, + onExpand, + Modifier.element(AlertCardElements.CompactExpand), + ) + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun ContentScope.CardScene( + notification: IslandEvent.Notification, + accent: Color, + expanded: Boolean, + isCall: Boolean, + initiallyCompact: Boolean, + hasExpandableContent: Boolean, + hasTitleAndBody: Boolean, + showReply: Boolean, + interactor: IslandActions, + onDismiss: () -> Unit, + onExpandToggle: () -> Unit, + onCollapseToCompact: () -> Unit, + onShowReply: () -> Unit, + onSentReply: () -> Unit, +) { + val context = LocalContext.current + val title = notification.title + val body = notification.text + + Column(modifier = Modifier.element(AlertCardElements.CardContent).fillMaxWidth()) { + Surface( + onClick = { + if (!showReply) { + try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + if (!isCall) onDismiss() + } + }, + color = Color.Transparent, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(SpaceXxl), + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + Row( modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceSm), ) { - Row( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = SpaceXxl, vertical = SpaceMd), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceSm), - ) { - notification.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier.size(SizeIconSm).clip(ShapeSm), - ) - } - val label = notification.senderName - ?: notification.title - ?: notification.appName - Text( - label, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + notification.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier + .element(AlertCardElements.AppIcon) + .size(SizeIconSm) + .clip(ShapeSm), ) - val preview = notification.text ?: notification.title ?: "" - if (preview.isNotEmpty() && preview != label) { - Text( - "· $preview", - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), + } + Text( + if (notification.isGroupConversation && notification.conversationTitle != null) + "${notification.appName} · ${notification.conversationTitle}" + else + notification.appName.ifEmpty { notification.senderName ?: "" }, + color = accent, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (isCall) { + CallActions(notification, interactor, onDismiss) + } + if (!showReply && !isCall) { + val expandMod = Modifier.element(AlertCardElements.CardExpand) + if (initiallyCompact) { + ExpandChip(Icons.Filled.ExpandLess, accent, onCollapseToCompact, expandMod) + } else if (hasExpandableContent) { + ExpandChip( + if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + accent, + onExpandToggle, + expandMod, ) - } else { - Spacer(Modifier.weight(1f)) } - ExpandChip(Icons.Filled.ExpandMore, accent) { isCompact = false; isExpanded = true } } } - } else { - Surface( - onClick = { - if (!showReply) { - try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - if (!isCall) onDismiss() - } - }, - color = Color.Transparent, - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(SpaceXxl), - verticalArrangement = Arrangement.spacedBy(SpaceLg), + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(SpaceLg), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceSm), + NotifAvatar( + notification, + accent, + SizeCompactIcon, + Modifier.element(AlertCardElements.Avatar), + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), ) { - notification.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier.size(SizeIconSm).clip(ShapeSm), - ) - } + val sender = notification.senderName ?: notification.appName Text( - if (notification.isGroupConversation && notification.conversationTitle != null) - "${notification.appName} · ${notification.conversationTitle}" - else - notification.appName.ifEmpty { notification.senderName ?: "" }, - color = accent, - style = MaterialTheme.typography.labelMedium, + sender, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), + modifier = Modifier.element(AlertCardElements.SenderName), ) - if (isCall) { - CallActions(notification, interactor, onDismiss) - } - if (!showReply && !isCall) { - if (initiallyCompact) { - ExpandChip(Icons.Filled.ExpandLess, accent) { - isExpanded = false - isCompact = true - } - } else if (hasExpandableContent) { - ExpandChip( - if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, - accent, - ) { isExpanded = !isExpanded } - } - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(SpaceLg), - ) { - NotifAvatar(notification, accent, SizeCompactIcon) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(SpaceXs), - ) { - val sender = notification.senderName ?: notification.appName - Text( - sender, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - if (isExpanded) { - if (hasTitleAndBody) { - Text( - title!!, - color = OnCardText, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - body!!, - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 8, - overflow = TextOverflow.Ellipsis, - ) - } else { - val preview = body ?: title ?: "" - if (preview.isNotEmpty()) { - Text( - preview, - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 10, - overflow = TextOverflow.Ellipsis, - ) - } - } + if (expanded) { + if (hasTitleAndBody) { + Text( + title!!, + color = OnCardText, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + body!!, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 8, + overflow = TextOverflow.Ellipsis, + ) } else { val preview = body ?: title ?: "" if (preview.isNotEmpty()) { @@ -354,68 +480,81 @@ fun NotificationAlertCard( preview, color = SubtleGray, style = MaterialTheme.typography.bodySmall, - maxLines = 2, + maxLines = 10, overflow = TextOverflow.Ellipsis, - onTextLayout = { isTextTruncated = it.hasVisualOverflow }, ) } } + } else { + val preview = body ?: title ?: "" + if (preview.isNotEmpty()) { + Text( + preview, + color = SubtleGray, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } + } - notification.notificationImage?.let { img -> - Image( - bitmap = img.toScaledBitmap(ThumbnailSize), - contentDescription = null, - modifier = Modifier.size(ThumbnailSize).clip(ShapeSm), - contentScale = ContentScale.Crop, - ) - } + notification.notificationImage?.let { img -> + Image( + bitmap = img.toScaledBitmap(ThumbnailSize), + contentDescription = null, + modifier = Modifier.size(ThumbnailSize).clip(ShapeSm), + contentScale = ContentScale.Crop, + ) } } } + } - val hasReply = notification.replyAction != null - val hasActions = notification.actions.isNotEmpty() || hasReply - if (!isCall && !showReply && hasActions) { - Box( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = SpaceXxl) - .padding(bottom = SpaceXxl), - ) { - if (isExpanded) { - ExpandedActions(notification, accent, onDismiss) { showReply = true } - } else { - CollapsedActions(notification, accent, onDismiss) { showReply = true } - } + val hasReply = notification.replyAction != null + val hasActions = notification.actions.isNotEmpty() || hasReply + if (!isCall && !showReply && hasActions) { + Box( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = SpaceXxl) + .padding(bottom = SpaceXxl), + ) { + if (expanded) { + ExpandedActions(notification, accent, onDismiss, onShowReply) + } else { + CollapsedActions(notification, accent, onDismiss, onShowReply) } } + } - if (showReply && notification.replyAction != null) { - ReplyField( - reply = notification.replyAction!!, - accent = accent, - interactor = interactor, - onSent = { - showReply = false - onDismiss() - }, - ) - } - } + if (showReply && notification.replyAction != null) { + ReplyField( + reply = notification.replyAction!!, + accent = accent, + interactor = interactor, + onSent = onSentReply, + ) } } } @Composable -private fun NotifAvatar(notification: IslandEvent.Notification, accent: Color, size: Dp) { +private fun NotifAvatar( + notification: IslandEvent.Notification, + accent: Color, + size: Dp, + modifier: Modifier = Modifier, +) { val icon = notification.senderIcon ?: notification.appIcon val isRound = notification.isConversation && notification.senderIcon != null if (notification.isConversation && notification.senderIcon != null && notification.appIcon != null) { - BadgedContactIcon(icon!!, notification.appIcon!!, size, (size.value * 0.44f).dp, true) + Box(modifier = modifier) { + BadgedContactIcon(icon!!, notification.appIcon!!, size, (size.value * 0.44f).dp, true) + } } else if (icon != null) { Box( - modifier = Modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), + modifier = modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), contentAlignment = Alignment.Center, ) { Image( @@ -427,7 +566,7 @@ private fun NotifAvatar(notification: IslandEvent.Notification, accent: Color, s } } else { Box( - modifier = Modifier.size(size).clip(ShapeIconMedium).background(accent.copy(alpha = AlphaSubtle)), + modifier = modifier.size(size).clip(ShapeIconMedium).background(accent.copy(alpha = AlphaSubtle)), contentAlignment = Alignment.Center, ) { Icon(Icons.Filled.Notifications, null, tint = accent, modifier = Modifier.size(SizeIconSm)) @@ -492,46 +631,50 @@ private fun CallActions( Row(horizontalArrangement = Arrangement.spacedBy(SpaceMd)) { if (isIncoming) { - notification.actions.forEach { action -> - val pi = action.action.actionIntent - val isDecline = (declinePi != null && pi == declinePi) || (hangUpPi != null && pi == hangUpPi) - val isAnswer = answerPi != null && pi == answerPi - val decline = isDecline || (!isAnswer && declinePi == null) - val bg = if (decline) RedAccent else GreenAccent - ActionCircle( - if (decline) Icons.Filled.CallEnd else Icons.Filled.Call, - bg, chipContentColorOn(bg), - ) { + val answerAction = notification.actions.firstOrNull { a -> + answerPi != null && a.action.actionIntent == answerPi + } + val declineAction = notification.actions.firstOrNull { a -> + val pi = a.action.actionIntent + (declinePi != null && pi == declinePi) || (hangUpPi != null && pi == hangUpPi) + } + declineAction?.let { action -> + ActionCircle(Icons.Filled.CallEnd, RedAccent, chipContentColorOn(RedAccent)) { + try { action.action.actionIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + onDismiss() + } + } + answerAction?.let { action -> + ActionCircle(Icons.Filled.Call, GreenAccent, chipContentColorOn(GreenAccent)) { try { action.action.actionIntent?.sendWithBal(context) } catch (_: PendingIntent.CanceledException) {} onDismiss() } } } else { - notification.actions.forEach { action -> - val pi = action.action.actionIntent - val semantic = action.action.semanticAction - val isMute = semantic == Notification.Action.SEMANTIC_ACTION_MUTE || - semantic == Notification.Action.SEMANTIC_ACTION_UNMUTE - val isEnd = if (hangUpPi != null) pi == hangUpPi - else !isMute && action == notification.actions.first() - val bg = if (isEnd) RedAccent else ActionBg - val tint = if (isEnd) chipContentColorOn(RedAccent) else OnCardText + val endAction = if (hangUpPi != null) { + notification.actions.firstOrNull { it.action.actionIntent == hangUpPi } + } else { + notification.actions.firstOrNull { a -> + val semantic = a.action.semanticAction + semantic != Notification.Action.SEMANTIC_ACTION_MUTE && + semantic != Notification.Action.SEMANTIC_ACTION_UNMUTE + } + } + endAction?.let { action -> val drawable = action.action.getIcon()?.loadDrawable(context) if (drawable != null) { - DrawableActionCircle(drawable, bg, tint) { + DrawableActionCircle(drawable, RedAccent, chipContentColorOn(RedAccent)) { try { action.action.actionIntent?.sendWithBal(context) } catch (_: PendingIntent.CanceledException) {} - if (isEnd) onDismiss() + onDismiss() } } else { - ActionCircle( - if (isEnd) Icons.Filled.CallEnd else Icons.Filled.Call, - bg, tint, - ) { + ActionCircle(Icons.Filled.CallEnd, RedAccent, chipContentColorOn(RedAccent)) { try { action.action.actionIntent?.sendWithBal(context) } catch (_: PendingIntent.CanceledException) {} - if (isEnd) onDismiss() + onDismiss() } } } @@ -554,12 +697,17 @@ private fun ActionCircle( } @Composable -private fun ExpandChip(icon: ImageVector, accent: Color, onClick: () -> Unit) { +private fun ExpandChip( + icon: ImageVector, + accent: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { Surface( onClick = onClick, shape = CircleShape, color = accent.copy(alpha = AlphaIconBg), - modifier = Modifier.size(36.dp), + modifier = modifier.size(36.dp), ) { Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { Icon(icon, contentDescription = null, tint = accent, modifier = Modifier.size(18.dp)) @@ -628,7 +776,6 @@ private fun ExpandedActions( } } - @Composable private fun ReplyField( reply: IslandEvent.ReplyAction, diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index aa829875a254..be22efa34e0b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -33,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf @@ -743,7 +745,10 @@ private fun AnimatedDownloadIcon(color: Color) { @Composable private fun PromotedOngoingText(event: IslandEvent.PromotedOngoing, modifier: Modifier, overrideColor: Color? = null) { - val label = event.shortText.ifEmpty { event.title.ifEmpty { event.appName } } + val label = event.shortText.ifEmpty { + if ((event.progress >= 0f || event.isIndeterminate) && event.text.isNotEmpty()) event.text + else event.title.ifEmpty { event.appName } + } MarqueeLabel(label, overrideColor ?: BlueAccent, modifier) } @@ -1034,14 +1039,14 @@ private fun MarqueeLabel(text: String, color: Color, modifier: Modifier = Modifi style = PillPrimary, maxLines = 1, overflow = TextOverflow.Clip, - modifier = modifier.basicMarquee(), + modifier = modifier.basicMarquee(iterations = 1), ) } @Composable private fun RecordingText(event: IslandEvent.ScreenRecording, modifier: Modifier, color: Color) { if (event.isCountdown) { - Text(event.countdownSeconds.toString(), color = color, style = PillMono, modifier = modifier) + Text(formatCountdownSeconds(event.countdownSeconds), color = color, style = PillMono, modifier = modifier) return } var elapsedMs by remember(event.startTimeMs) { @@ -1100,7 +1105,8 @@ private fun MediaText(event: IslandEvent.Media, modifier: Modifier, overrideColo val baseColor = overrideColor ?: OrangeAccent val alpha = if (event.isPlaying) 1f else AlphaHint val color = baseColor.copy(alpha = alpha) - WaveformAnimation(color = color, modifier = modifier.size(20.dp, 12.dp), isAnimating = event.isPlaying) + val text = if (event.artist.isNotBlank()) "${event.track} - ${event.artist}" else event.track + MarqueeLabel(text, color, modifier.widthIn(max = 66.dp)) } @Composable @@ -1113,7 +1119,7 @@ private fun BtText(event: IslandEvent.Bluetooth, modifier: Modifier, overrideCol style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Clip, - modifier = modifier.basicMarquee(), + modifier = modifier.basicMarquee(iterations = 1), ) } else { MarqueeLabel(event.deviceName.take(12), color, modifier) @@ -1253,10 +1259,10 @@ fun PulsingDot(color: Color, size: Dp = 8.dp, durationMs: Int = 600, minAlpha: F @Composable fun WaveformAnimation(color: Color, modifier: Modifier = Modifier.size(34.dp, 20.dp), isAnimating: Boolean = true, barCount: Int = 4) { - val phase: Float + val phaseState: State? if (isAnimating) { val transition = rememberInfiniteTransition(label = "waveform") - val animatedPhase by + phaseState = transition.animateFloat( initialValue = 0f, targetValue = 2f * PI.toFloat(), @@ -1264,11 +1270,11 @@ fun WaveformAnimation(color: Color, modifier: Modifier = Modifier.size(34.dp, 20 infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Restart), label = "wave_phase", ) - phase = animatedPhase } else { - phase = 0f + phaseState = null } Canvas(modifier = modifier) { + val phase = phaseState?.value ?: 0f val barW = size.width / (barCount * 2.8f) val maxH = size.height * 0.82f val staticH = if (!isAnimating) maxH * 0.35f else 0f diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 88e3d1717b28..5f3f068c5a34 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -3,7 +3,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView +import com.android.axion.compose.host.AxComposeView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle @@ -38,12 +38,12 @@ constructor( private var isCurrentlyHiding = false override fun addViews(constraintLayout: ConstraintLayout) { - val composeView = ComposeView(context).apply { id = chipViewId } + val composeView = AxComposeView(context).apply { id = chipViewId } constraintLayout.addView(composeView) } override fun bindData(constraintLayout: ConstraintLayout) { - val composeView: ComposeView = constraintLayout.requireViewById(chipViewId) + val composeView: AxComposeView = constraintLayout.requireViewById(chipViewId) if (viewModel.isEnabled.value && viewModel.isKeyguardEnabled.value && viewModel.isOnKeyguard.value) { indicationController.setSuppressIndication(true) @@ -67,7 +67,9 @@ constructor( expansionHandle = composeView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.isKeyguardExpanded.collect { expanded -> + combine(viewModel.isKeyguardExpanded, viewModel.isLowUdfps) { expanded, lowUdfps -> + expanded to lowUdfps + }.collect { (expanded, lowUdfps) -> isCurrentlyHiding = expanded enforceHiddenViews(constraintLayout, expanded) @@ -81,25 +83,26 @@ constructor( val lp = composeView.layoutParams as ConstraintLayout.LayoutParams if (expanded) { lp.width = ConstraintLayout.LayoutParams.MATCH_PARENT - lp.height = 0 + lp.height = 0 lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID lp.topMargin = Utils.getStatusBarHeaderHeightKeyguard(context) lp.bottomToTop = R.id.start_button lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID lp.bottomMargin = 0 - + lp.topToBottom = -1 lp.bottomToBottom = -1 lp.startToEnd = -1 lp.endToStart = -1 - } else { + } else if (lowUdfps) { lp.width = ViewGroup.LayoutParams.WRAP_CONTENT lp.height = ViewGroup.LayoutParams.WRAP_CONTENT lp.topMargin = 0 - lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID - lp.bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom) + lp.bottomToTop = R.id.device_entry_icon_view + lp.bottomMargin = (CHIP_ABOVE_LOCK_MARGIN_DP * context.resources.displayMetrics.density + 0.5f).toInt() + lp.bottomToBottom = -1 lp.topToTop = -1 lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID @@ -107,7 +110,22 @@ constructor( lp.startToEnd = -1 lp.endToStart = -1 + lp.topToBottom = -1 + } else { + lp.width = ViewGroup.LayoutParams.WRAP_CONTENT + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT + lp.topMargin = 0 + + lp.bottomToBottom = R.id.start_button + lp.bottomMargin = 0 lp.bottomToTop = -1 + lp.topToTop = -1 + + lp.startToEnd = R.id.start_button + lp.endToStart = R.id.end_button + lp.startToStart = -1 + lp.endToEnd = -1 + lp.topToBottom = -1 } composeView.layoutParams = lp @@ -118,8 +136,8 @@ constructor( override fun applyConstraints(constraintSet: ConstraintSet) { val expanded = viewModel.isKeyguardExpanded.value + val lowUdfps = viewModel.isLowUdfps.value constraintSet.apply { - if (expanded) { constrainWidth(chipViewId, ConstraintSet.MATCH_CONSTRAINT) constrainHeight(chipViewId, ConstraintSet.MATCH_CONSTRAINT) @@ -128,13 +146,19 @@ constructor( connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.TOP) connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) - } else { + } else if (lowUdfps) { constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - val marginBottom = context.resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom) - connect(chipViewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, marginBottom) + val margin = (CHIP_ABOVE_LOCK_MARGIN_DP * context.resources.displayMetrics.density + 0.5f).toInt() + connect(chipViewId, ConstraintSet.BOTTOM, R.id.device_entry_icon_view, ConstraintSet.TOP, margin) connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + } else { + constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) + connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.BOTTOM) + connect(chipViewId, ConstraintSet.START, R.id.start_button, ConstraintSet.END) + connect(chipViewId, ConstraintSet.END, R.id.end_button, ConstraintSet.START) } } } @@ -150,6 +174,10 @@ constructor( constraintLayout.removeView(chipViewId) } + companion object { + private const val CHIP_ABOVE_LOCK_MARGIN_DP = 12f + } + private val preserveOnExpandIds = setOf( chipViewId, R.id.start_button, From a1b701001bbae6acbda67e782e29a556d2aa74f8 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:31:37 +0800 Subject: [PATCH 1110/1315] SystemUI: DynamicBar: Fixing expanded content top padding https://github.com/AxionAOSP/issue_tracker/issues/110 Change-Id: Ib95eb4807a374d362734c940c3f82a118b3a301b Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/AxDynamicBarExpandedPanel.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index b2623b351e1f..6ac9eddbc7c0 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -143,10 +143,13 @@ constructor( val statusBarTop = windowMetrics.windowInsets .getInsets(WindowInsets.Type.statusBars()) .top + val hasCutout = windowMetrics.windowInsets + .getInsets(WindowInsets.Type.displayCutout()) + .top > 0 val view = ComposeView(context).apply { - setContent { PlatformTheme { OverlayContent(viewModel, statusBarTop) } } + setContent { PlatformTheme { OverlayContent(viewModel, statusBarTop, hasCutout) } } } view.setViewTreeLifecycleOwner(lifecycleOwner) @@ -235,13 +238,13 @@ constructor( } @Composable -private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeightPx: Int) { +private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeightPx: Int, hasCutout: Boolean) { val density = LocalDensity.current val isLargeScreen = Utilities.isLargeScreen(LocalContext.current) - - val topPad = if (isLargeScreen) { - with(density) { statusBarHeightPx.toDp() } + 4.dp - } else 0.dp + + val largeScreenExtra = if (isLargeScreen) 4.dp else 0.dp + val topPad = if (hasCutout) largeScreenExtra + else with(density) { statusBarHeightPx.toDp() } + largeScreenExtra val chipState by viewModel.chipState.collectAsStateWithLifecycle() val isExpanded by viewModel.isExpanded.collectAsStateWithLifecycle() val uiState by viewModel.interactor.uiState.collectAsStateWithLifecycle() From 7a3c1166497057e20fbdd0bcad02f29e6736458b Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:23:30 +0800 Subject: [PATCH 1111/1315] SystemUI: DynamicBar: Fixing recorder | notification issues Change-Id: I3777f3899ede8b2bc94e64aeaeac7fa4dd262ab1 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../data/source/NotificationIslandManager.kt | 23 +++--- .../ui/compose/AxDynamicBarKeyguardChip.kt | 15 +++- .../ui/compose/ExpandedOngoingContent.kt | 80 ++++++++++++------- 3 files changed, 73 insertions(+), 45 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt index 6d3dbe585501..18cf3f324186 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt @@ -127,7 +127,8 @@ constructor( var activeMediaPackageProvider: (() -> String?)? = null - private val seenNotificationPostTimes = mutableMapOf() + private val seenNotificationKeys = mutableSetOf() + private val seenMessagingTimestamps = mutableMapOf() var onTimerEvent: ((IslandEvent.Timer) -> Unit)? = null var onAlarmEvent: ((IslandEvent.Alarm) -> Unit)? = null @@ -145,7 +146,8 @@ constructor( object : ScrimUtils.ScrimEventListener { override fun onNotificationRemoved(sbn: StatusBarNotification) { val pkg = sbn.packageName ?: return - seenNotificationPostTimes.remove(sbn.key) + seenNotificationKeys.remove(sbn.key) + seenMessagingTimestamps.remove(sbn.key) if (sbn.key == timerNotificationKey) { timerNotificationKey = null @@ -400,15 +402,11 @@ constructor( } else 0L } else 0L - val postTime = sbn.postTime - val signature = - if (isMessagingStyle && latestMessageTime > 0L) latestMessageTime - else postTime - val lastSignature = seenNotificationPostTimes.put(sbn.key, signature) - if (lastSignature != null) { - if (signature == lastSignature) return - if (!isMessagingStyle && - notifFlags and Notification.FLAG_ONLY_ALERT_ONCE != 0) return + if (isMessagingStyle && latestMessageTime > 0L) { + val previous = seenMessagingTimestamps.put(sbn.key, latestMessageTime) + if (previous != null && previous == latestMessageTime) return + } else { + if (!seenNotificationKeys.add(sbn.key)) return } val icon = @@ -551,7 +549,8 @@ constructor( if (!listening) return listening = false ScrimUtils.get().removeListener(scrimListener) - seenNotificationPostTimes.clear() + seenNotificationKeys.clear() + seenMessagingTimestamps.clear() timerJob?.cancel() timerJob = null _timerEvent.value = null diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index 0a2004a121af..a3151effbd94 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -778,14 +778,25 @@ private fun actionsFor(event: IslandEvent): List = when (event) { if (event.isCountdown) emptyList() else listOf(ChipAction(ActionIcon.STOP) { vm, _, _ -> vm.stopScreenRecording() }) is IslandEvent.AudioRecording -> { - val pauseResume = event.actions.firstOrNull() + val pauseResume = event.actions.firstOrNull { a -> + val label = a.label.toString().lowercase() + label.contains("pause") || label.contains("resume") + } + val stop = event.actions.firstOrNull { a -> + val label = a.label.toString().lowercase() + label.contains("stop") || label.contains("delete") + } listOfNotNull( pauseResume?.let { action -> ChipAction( if (event.state == RecordingState.RECORDING) ActionIcon.PAUSE else ActionIcon.PLAY, ) { _, _, ctx -> action.action.actionIntent?.sendWithBal(ctx) } }, - ChipAction(ActionIcon.STOP) { vm, e, _ -> vm.dismissEvent(e) }, + stop?.let { action -> + ChipAction(ActionIcon.STOP) { _, _, ctx -> + action.action.actionIntent?.sendWithBal(ctx) + } + }, ) } is IslandEvent.Timer -> { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt index dd37f8b91d9a..dc5a42475108 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -68,41 +67,60 @@ private fun resolveRemoteViews(ctx: Context, notification: Notification): Remote return null } +private fun applyOrReapplyRemoteViews(frame: FrameLayout, notification: Notification): Boolean { + val rv = + try { + resolveRemoteViews(frame.context, notification) + } catch (_: Exception) { + return false + } ?: return false + val existing = if (frame.childCount > 0) frame.getChildAt(0) else null + if (existing != null) { + try { + rv.reapply(frame.context, existing) + prepareForIsland(existing) + return true + } catch (_: Exception) { + frame.removeAllViews() + } + } + return try { + val inflated = rv.apply(frame.context, frame) + prepareForIsland(inflated) + frame.addView( + inflated, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + true + } catch (_: Exception) { + false + } +} + @Composable private fun SbnContentView(sbn: StatusBarNotification, fallback: @Composable () -> Unit) { var failed by remember(sbn.key) { mutableStateOf(false) } - if (!failed) { - key(sbn.key) { - AndroidView( - factory = { ctx -> - FrameLayout(ctx).apply { - setBackgroundColor(android.graphics.Color.TRANSPARENT) - try { - val rv = resolveRemoteViews(ctx, sbn.notification) - val inflated = rv?.apply(ctx, this) - if (inflated != null) { - prepareForIsland(inflated) - addView( - inflated, - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - ), - ) - } else { - failed = true - } - } catch (_: Exception) { - failed = true - } - } - }, - modifier = Modifier.fillMaxWidth().clip(ShapeIconMedium), - ) - } - } else { + if (failed) { fallback() + return } + AndroidView( + factory = { ctx -> + FrameLayout(ctx).apply { + setBackgroundColor(android.graphics.Color.TRANSPARENT) + } + }, + update = { frame -> + val notification = sbn.notification + if (notification == null || !applyOrReapplyRemoteViews(frame, notification)) { + failed = true + } + }, + modifier = Modifier.fillMaxWidth().clip(ShapeIconMedium), + ) } private val COLLAPSE_CHIP_IDS = arrayOf("expand_button_touch_container", "expand_button") From 9f3098c2d0b7b749fb56a257becbc51a5d0fb86f Mon Sep 17 00:00:00 2001 From: Saikrishna1504 Date: Sat, 11 Apr 2026 12:34:30 +0000 Subject: [PATCH 1112/1315] SystemUI: DynamicBar: Auto-focus reply field when clicking reply button Change-Id: I29bca3b6435ac95bf4dc76ac95bf4dc76ac95bf5 Signed-off-by: Saikrishna1504 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/compose/NotificationAlertCard.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt index 80136b0cf966..aa56ff5613de 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -47,6 +48,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -785,6 +788,12 @@ private fun ReplyField( ) { var replyText by remember { mutableStateOf("") } val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + interactor.onFocusableRequested?.invoke(true) + focusRequester.requestFocus() + } Row( modifier = Modifier.fillMaxWidth() @@ -799,6 +808,7 @@ private fun ReplyField( onValueChange = { replyText = it }, modifier = Modifier.weight(1f) .padding(horizontal = SpaceLg) + .focusRequester(focusRequester) .onFocusChanged { interactor.onFocusableRequested?.invoke(it.isFocused) }, textStyle = MaterialTheme.typography.bodySmall.copy(color = OnCardText), singleLine = true, From b060ede7ee5873bde09f3085d9442a60a3f89935 Mon Sep 17 00:00:00 2001 From: Saikrishna1504 Date: Sat, 11 Apr 2026 13:38:40 +0000 Subject: [PATCH 1113/1315] SystemUI: DynamicBar: Allow opening app when reply field is focused Change-Id: I29bca3b6435ac95bf4dc76ac95bf4dc76ac95bf6 Signed-off-by: Saikrishna1504 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/compose/NotificationAlertCard.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt index aa56ff5613de..9e2aef4f33c5 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt @@ -375,11 +375,10 @@ private fun ContentScope.CardScene( Column(modifier = Modifier.element(AlertCardElements.CardContent).fillMaxWidth()) { Surface( onClick = { - if (!showReply) { - try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - if (!isCall) onDismiss() - } + try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } + catch (_: PendingIntent.CanceledException) {} + if (showReply) interactor.onFocusableRequested?.invoke(false) + if (!isCall) onDismiss() }, color = Color.Transparent, modifier = Modifier.fillMaxWidth(), From fddd5578ba4b86bca45582bd4aff3f54196776d1 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:39:40 +0800 Subject: [PATCH 1114/1315] SystemUI: DynamicBar: Limiting status bar chip width Change-Id: Iec04c3b59cfb0200ddd33e7ee92c212a1a6f44ae Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt index 8a338086f7bb..553c2987bb91 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import android.graphics.drawable.Drawable @@ -95,6 +96,7 @@ fun AxDynamicBarChip( val keyguardCarrier by viewModel.keyguardCarrierText.collectAsStateWithLifecycle() val carrierName = if (isOnKeyguard && ignoreKeyguard) keyguardCarrier.takeIf { it.isNotBlank() } else null + val chipTextMaxWidth = dimensionResource(R.dimen.ongoing_activity_chip_max_text_width) val screenWidthPx = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() } @@ -274,7 +276,7 @@ fun AxDynamicBarChip( color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = 100.dp).basicMarquee(iterations = 1), + modifier = Modifier.widthIn(max = chipTextMaxWidth).basicMarquee(iterations = 1), ) } } @@ -322,7 +324,7 @@ fun AxDynamicBarChip( ) { event -> PillEventText( event, - Modifier.widthIn(max = 88.dp), + Modifier.widthIn(max = chipTextMaxWidth), overrideColor = contentColor, ) } From 26af53b5ab77d9e47570ab31560bcaa41a4aff25 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 31 Mar 2026 21:28:36 +0530 Subject: [PATCH 1115/1315] SystemUI: DynamicBar: Fix our torch impl Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/axdynamicbar/data/source/TorchIslandManager.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt index 747f69f9d152..d5808b534216 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/TorchIslandManager.kt @@ -52,6 +52,8 @@ constructor( _torchEvent.value = null } } + + override fun onFlashlightStrengthChanged(level: Int) {} } private fun startLevelObserver() { From 4c0b74ead4a4d37d56e1b6014f60551da26d9ec6 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 1 Apr 2026 08:38:23 +0530 Subject: [PATCH 1116/1315] SystemUI: DynamicBar: Add smooth squiggly progress for media content * This follows AOSP implementation. * Shows animation only when playing. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/ExpandedMediaContent.kt | 195 +++++++++++-- .../ui/compose/KeyguardExpandedContent.kt | 265 ++++++++++++++---- .../controls/ui/drawable/SquigglyProgress.kt | 10 +- 3 files changed, 384 insertions(+), 86 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index 1cd0fda07168..2deeee996636 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -1,7 +1,9 @@ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package com.android.systemui.axdynamicbar.ui.compose +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.util.TypedValue +import android.widget.SeekBar import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -29,37 +31,41 @@ import androidx.compose.material.icons.filled.Shuffle import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.filled.VolumeUp -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue 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.ColorFilter +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.axdynamicbar.shared.IslandActions import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.shared.* +import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import com.android.systemui.res.R +import kotlinx.coroutines.delay private val AlbumArtSize = 80.dp private val PlayPauseSize = 56.dp private val ControlButtonSize = 44.dp private val ControlIconSize = 22.dp +private val SeekBarHeight = 28.dp @Composable internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { @@ -301,46 +307,81 @@ private fun MediaSeekBar( accent: Color, ) { val mediaProgress = rememberMediaProgress(event) - val clamped = mediaProgress.progress - var isSeeking by remember { mutableStateOf(false) } - var seekProgress by remember { mutableFloatStateOf(clamped) } - if (!isSeeking) seekProgress = clamped - val displayMs = - if (isSeeking) (seekProgress * event.duration).toLong() - else mediaProgress.positionMs + val isPlaying = event.isPlaying + val durationMs = event.duration + val positionMs = mediaProgress.positionMs + val serverFraction = mediaProgress.progress + + var isScrubbing by remember { mutableStateOf(false) } + var displayFraction by remember { mutableStateOf(serverFraction) } + + val interactorRef = rememberUpdatedState(interactor) + + // Smooth frame-interpolated progress when playing, snaps when paused or scrubbing + LaunchedEffect(positionMs, durationMs, isPlaying) { + if (isScrubbing) return@LaunchedEffect + + displayFraction = serverFraction + + if (!isPlaying || durationMs <= 0L) return@LaunchedEffect + + val startWallMs = System.currentTimeMillis() + val startProgressMs = positionMs + while (true) { + delay(16L) // ~60 fps + if (isScrubbing) break + val elapsed = System.currentTimeMillis() - startWallMs + val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f, 1f) + displayFraction = interpolated + if (interpolated >= 1f) break + } + } + + val displayMs = if (isScrubbing) (displayFraction * durationMs).toLong() else (displayFraction * durationMs).toLong() + val accentArgb = accent.toArgb() + val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb() Column(verticalArrangement = Arrangement.spacedBy(SpaceXs)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - Text(formatElapsedTime(event.duration), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text( + formatElapsedTime(displayMs), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) + Text( + formatElapsedTime(durationMs), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) } + Box( modifier = Modifier .fillMaxWidth() - .height(SizeSeekHeight) + .height(SeekBarHeight) .pointerInput("tap") { detectTapGestures { offset -> val fraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - seekProgress = fraction - interactor.seekTo((fraction * event.duration).toLong()) + displayFraction = fraction + interactorRef.value.seekTo((fraction * durationMs).toLong()) } } .pointerInput("drag") { detectHorizontalDragGestures( onDragStart = { offset -> - isSeeking = true - seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + isScrubbing = true + displayFraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) }, onDragEnd = { - isSeeking = false - interactor.seekTo((seekProgress * event.duration).toLong()) + interactorRef.value.seekTo((displayFraction * durationMs).toLong()) + isScrubbing = false }, - onDragCancel = { isSeeking = false }, + onDragCancel = { isScrubbing = false }, onHorizontalDrag = { change, _ -> - seekProgress = + displayFraction = (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) change.consume() }, @@ -348,17 +389,113 @@ private fun MediaSeekBar( }, contentAlignment = Alignment.Center, ) { - LinearWavyProgressIndicator( - progress = { seekProgress }, - modifier = Modifier.fillMaxWidth(), - color = accent, - trackColor = accent.copy(alpha = AlphaSubtle), - amplitude = { if (event.isPlaying) 1f else 0f }, + AndroidView( + factory = { context -> + SeekBar(context).apply { + max = 10_000 + splitTrack = false + setPadding(0, 0, 0, 0) + // Disable direct touch — Compose handles all gestures above + isEnabled = false + + // Pill-shaped thumb + thumb = createSeekBarThumb(context, accentArgb) + thumbOffset = thumb.intrinsicWidth / 2 + + // Set up SquigglyProgress on the progress layer + val layer = (progressDrawable?.mutate() as? LayerDrawable) + if (layer != null) { + layer.findDrawableByLayerId(android.R.id.background) + ?.mutate()?.setTint(trackAlphaArgb) + + layer.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.mutate()?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + + val squiggle = SquigglyProgress().apply { + waveLength = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_wavelength + ).toFloat() + lineAmplitude = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_amplitude + ).toFloat() + phaseSpeed = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_phase + ).toFloat() + strokeWidth = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_stroke_width + ).toFloat() + setTint(accentArgb) + drawRemainingLine = false + transitionEnabled = false + animate = false + } + layer.setDrawableByLayerId(android.R.id.progress, squiggle) + progressDrawable = layer + } + } + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + bar.progress = target + + // Re-tint thumb for accent color changes (e.g. track switch) + (bar.thumb as? GradientDrawable)?.setColor(accentArgb) + + val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() + bar.thumb?.alpha = alpha + + val layer = bar.progressDrawable as? LayerDrawable + + // Re-tint track colors + layer?.findDrawableByLayerId(android.R.id.background) + ?.setTint(trackAlphaArgb) + layer?.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + + val squiggle = layer + ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress + + squiggle?.apply { + setTint(accentArgb) + setAlpha(alpha) + animate = isPlaying && !isScrubbing + } + + layer?.alpha = alpha + }, + modifier = Modifier.fillMaxWidth().height(SeekBarHeight), ) } } } +/** + * Creates a pill-shaped thumb drawable for the seekbar. + */ +private fun createSeekBarThumb(context: android.content.Context, tintColor: Int): GradientDrawable { + val wPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 4f, context.resources.displayMetrics + ).toInt().coerceAtLeast(1) + val hPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics + ).toInt().coerceAtLeast(1) + val radiusPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics + ) + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setSize(wPx, hPx) + cornerRadius = radiusPx + setColor(tintColor) + } +} + @Composable private fun MediaCustomActionButton( event: IslandEvent.Media, diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt index 0032a071ace7..00a485d05fba 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -2,6 +2,10 @@ package com.android.systemui.axdynamicbar.ui.compose +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.util.TypedValue +import android.widget.SeekBar import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -14,6 +18,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -56,11 +61,13 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -73,6 +80,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.viewinterop.AndroidView import kotlin.math.cos import kotlin.math.sin import androidx.compose.ui.unit.Dp @@ -83,8 +91,11 @@ import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.model.RecordingState import com.android.systemui.axdynamicbar.shared.* import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import kotlinx.coroutines.delay +private val KeyguardSeekBarHeight = 28.dp + @Composable internal fun KeyguardExpandedContent( event: IslandEvent, @@ -328,60 +339,7 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio } if (event.duration > 0L) { - val mediaProgress = rememberMediaProgress(event) - var isSeeking by remember { mutableStateOf(false) } - var seekProgress by remember { mutableFloatStateOf(mediaProgress.progress) } - if (!isSeeking) seekProgress = mediaProgress.progress - val displayMs = - if (isSeeking) (seekProgress * event.duration).toLong() - else mediaProgress.positionMs - Column(verticalArrangement = Arrangement.spacedBy(SpaceSm)) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(SpaceSection) - .pointerInput("tap") { - detectTapGestures { offset -> - val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - seekProgress = f - interactor.seekTo((f * event.duration).toLong()) - } - } - .pointerInput("drag") { - detectHorizontalDragGestures( - onDragStart = { offset -> - isSeeking = true - seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - }, - onDragEnd = { - isSeeking = false - interactor.seekTo((seekProgress * event.duration).toLong()) - }, - onDragCancel = { isSeeking = false }, - onHorizontalDrag = { change, _ -> - seekProgress = (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) - change.consume() - }, - ) - }, - contentAlignment = Alignment.Center, - ) { - LinearWavyProgressIndicator( - progress = { seekProgress }, - modifier = Modifier.fillMaxWidth(), - color = colors.accent, - trackColor = colors.accent.copy(alpha = AlphaSubtle), - amplitude = { if (event.isPlaying) 1f else 0f }, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - Text(formatElapsedTime(event.duration), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - } - } + KeyguardMediaSeekBar(event, interactor, colors.accent) } } @@ -469,6 +427,203 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio } } +@Composable +private fun KeyguardMediaSeekBar( + event: IslandEvent.Media, + interactor: IslandActions, + accent: Color, +) { + val mediaProgress = rememberMediaProgress(event) + val isPlaying = event.isPlaying + val durationMs = event.duration + val positionMs = mediaProgress.positionMs + val serverFraction = mediaProgress.progress + + var isScrubbing by remember { mutableStateOf(false) } + var displayFraction by remember { mutableStateOf(serverFraction) } + + val interactorRef = rememberUpdatedState(interactor) + + // Read the dismiss swipe lock provided by MagneticSwipeToDismiss + val swipeLock = LocalDismissSwipeLock.current + + // Smooth frame-interpolated progress + LaunchedEffect(positionMs, durationMs, isPlaying) { + if (isScrubbing) return@LaunchedEffect + + displayFraction = serverFraction + + if (!isPlaying || durationMs <= 0L) return@LaunchedEffect + + val startWallMs = System.currentTimeMillis() + val startProgressMs = positionMs + while (true) { + delay(16L) + if (isScrubbing) break + val elapsed = System.currentTimeMillis() - startWallMs + val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f, 1f) + displayFraction = interpolated + if (interpolated >= 1f) break + } + } + + val displayMs = (displayFraction * durationMs).toLong() + val accentArgb = accent.toArgb() + val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb() + + Column(verticalArrangement = Arrangement.spacedBy(SpaceSm)) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(KeyguardSeekBarHeight) + .pointerInput(swipeLock) { + awaitEachGesture { + awaitPointerEvent() + swipeLock.value = true + try { + do { + val event = awaitPointerEvent() + } while (event.changes.any { it.pressed }) + } finally { + swipeLock.value = false + } + } + } + .pointerInput("tap") { + detectTapGestures { offset -> + val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + displayFraction = f + interactorRef.value.seekTo((f * durationMs).toLong()) + } + } + .pointerInput("drag") { + detectHorizontalDragGestures( + onDragStart = { offset -> + isScrubbing = true + displayFraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + interactorRef.value.seekTo((displayFraction * durationMs).toLong()) + isScrubbing = false + }, + onDragCancel = { isScrubbing = false }, + onHorizontalDrag = { change, _ -> + displayFraction = + (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) + change.consume() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + AndroidView( + factory = { context -> + SeekBar(context).apply { + max = 10_000 + splitTrack = false + setPadding(0, 0, 0, 0) + isEnabled = false + + thumb = createKeyguardSeekBarThumb(context, accentArgb) + thumbOffset = thumb.intrinsicWidth / 2 + + val layer = (progressDrawable?.mutate() as? LayerDrawable) + if (layer != null) { + layer.findDrawableByLayerId(android.R.id.background) + ?.mutate()?.setTint(trackAlphaArgb) + + layer.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.mutate()?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + + val squiggle = SquigglyProgress().apply { + waveLength = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_wavelength + ).toFloat() + lineAmplitude = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_amplitude + ).toFloat() + phaseSpeed = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_phase + ).toFloat() + strokeWidth = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_stroke_width + ).toFloat() + setTint(accentArgb) + drawRemainingLine = false + transitionEnabled = false + animate = false + } + layer.setDrawableByLayerId(android.R.id.progress, squiggle) + progressDrawable = layer + } + } + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + bar.progress = target + + // Re-tint thumb for accent color changes (e.g. track switch) + (bar.thumb as? GradientDrawable)?.setColor(accentArgb) + + val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() + bar.thumb?.alpha = alpha + + val layer = bar.progressDrawable as? LayerDrawable + + // Re-tint track colors + layer?.findDrawableByLayerId(android.R.id.background) + ?.setTint(trackAlphaArgb) + layer?.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + + val squiggle = layer + ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress + + squiggle?.apply { + setTint(accentArgb) + setAlpha(alpha) + animate = isPlaying && !isScrubbing + } + + layer?.alpha = alpha + }, + modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + Text(formatElapsedTime(durationMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) + } + } +} + +private fun createKeyguardSeekBarThumb(context: android.content.Context, tintColor: Int): GradientDrawable { + val wPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 4f, context.resources.displayMetrics + ).toInt().coerceAtLeast(1) + val hPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics + ).toInt().coerceAtLeast(1) + val radiusPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics + ) + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setSize(wPx, hPx) + cornerRadius = radiusPx + setColor(tintColor) + } +} + @Composable private fun KeyguardTimerPanel(event: IslandEvent.Timer, interactor: IslandActions) { val context = LocalContext.current @@ -670,7 +825,7 @@ private fun KeyguardRecordingPanel(event: IslandEvent.ScreenRecording, interacto } Text( - formatCountdownSeconds(event.countdownSeconds), + event.countdownSeconds.toString(), color = OnCardText, style = MaterialTheme.typography.displayLarge, ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgress.kt index c417fe60219a..4e11b35555de 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgress.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgress.kt @@ -84,6 +84,12 @@ class SquigglyProgress : Drawable() { invalidateSelf() } + var drawRemainingLine: Boolean = true + set(value) { + field = value + invalidateSelf() + } + init { wavePaint.strokeCap = Paint.Cap.ROUND linePaint.strokeCap = Paint.Cap.ROUND @@ -199,14 +205,14 @@ class SquigglyProgress : Drawable() { canvas.drawPath(path, wavePaint) canvas.restore() - if (transitionEnabled) { + if (drawRemainingLine && transitionEnabled) { // If there's a smooth transition, we draw the rest of the // path in a different color (using different clip params) canvas.save() canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop) canvas.drawPath(path, linePaint) canvas.restore() - } else { + } else if (drawRemainingLine) { // No transition, just draw a flat line to the end of the region. // The discontinuity is hidden by the progress bar thumb shape. canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint) From 10fe73d74dd575d307a0e5991d6c5e4bc7103154 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 1 Apr 2026 21:56:33 +0530 Subject: [PATCH 1117/1315] SystemUI: DynamicBar: Fix seekbar scrubbing UX for media content * Add and activate dismiss swipe lock when seekbar is dragged. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/ExpandedMediaContent.kt | 19 +- .../ui/compose/MagneticSwipeToDismiss.kt | 214 ++++++++++-------- 2 files changed, 134 insertions(+), 99 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index 2deeee996636..893bb13d9617 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement @@ -317,6 +318,9 @@ private fun MediaSeekBar( val interactorRef = rememberUpdatedState(interactor) + // Read the dismiss swipe lock provided by MagneticSwipeToDismiss + val swipeLock = LocalDismissSwipeLock.current + // Smooth frame-interpolated progress when playing, snaps when paused or scrubbing LaunchedEffect(positionMs, durationMs, isPlaying) { if (isScrubbing) return@LaunchedEffect @@ -337,7 +341,7 @@ private fun MediaSeekBar( } } - val displayMs = if (isScrubbing) (displayFraction * durationMs).toLong() else (displayFraction * durationMs).toLong() + val displayMs = (displayFraction * durationMs).toLong() val accentArgb = accent.toArgb() val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb() @@ -362,6 +366,19 @@ private fun MediaSeekBar( modifier = Modifier .fillMaxWidth() .height(SeekBarHeight) + .pointerInput(swipeLock) { + awaitEachGesture { + awaitPointerEvent() // DOWN + swipeLock.value = true + try { + do { + val event = awaitPointerEvent() + } while (event.changes.any { it.pressed }) + } finally { + swipeLock.value = false + } + } + } .pointerInput("tap") { detectTapGestures { offset -> val fraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt index 02046d1b4fdf..3cc28213b1e1 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/MagneticSwipeToDismiss.kt @@ -5,6 +5,9 @@ import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,6 +28,13 @@ import kotlin.math.sign import kotlinx.coroutines.launch import androidx.compose.ui.input.pointer.PointerInputChange +/** + * A child (e.g. media seekbar) can set this to `true` to prevent + * [MagneticSwipeToDismiss] from claiming horizontal drags. + * Set it on ACTION_DOWN so the lock is active before slop detection. + */ +val LocalDismissSwipeLock = compositionLocalOf> { mutableStateOf(false) } + private val DETACH_THRESHOLD = 72.dp private const val MAGNETIC_PULL = 0.5f private val DISMISS_VELOCITY = 500.dp @@ -53,133 +63,141 @@ internal fun MagneticSwipeToDismiss( var detached by remember { mutableStateOf(false) } var dismissed by remember { mutableStateOf(false) } + val swipeLock = remember { mutableStateOf(false) } + if (dismissed) return val absOff = maxOf(abs(offsetX.value), abs(offsetY.value)) val progress = (absOff / (detachPx * 4f)).coerceIn(0f, 1f) - Box( - modifier = - modifier - .graphicsLayer { - translationX = offsetX.value - translationY = offsetY.value - alpha = 1f - progress * 0.5f - scaleX = 1f - progress * 0.04f - scaleY = 1f - progress * 0.04f - rotationZ = (offsetX.value / detachPx).coerceIn(-1f, 1f) * 2f - } - .pointerInput(allowUpDismiss) { - val touchSlop = viewConfig.touchSlop - - awaitEachGesture { + CompositionLocalProvider(LocalDismissSwipeLock provides swipeLock) { + Box( + modifier = + modifier + .graphicsLayer { + translationX = offsetX.value + translationY = offsetY.value + alpha = 1f - progress * 0.5f + scaleX = 1f - progress * 0.04f + scaleY = 1f - progress * 0.04f + rotationZ = (offsetX.value / detachPx).coerceIn(-1f, 1f) * 2f + } + .pointerInput(allowUpDismiss) { + val touchSlop = viewConfig.touchSlop + + awaitEachGesture { - val down: PointerInputChange - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - val d = event.changes.firstOrNull { - it.changedToDownIgnoreConsumed() + val down: PointerInputChange + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val d = event.changes.firstOrNull { + it.changedToDownIgnoreConsumed() + } + if (d != null) { down = d; break } } - if (d != null) { down = d; break } - } - var axis = 0 - var velocity = 0f - var prevTime = 0L - detached = false + var axis = 0 + var velocity = 0f + var prevTime = 0L + detached = false - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - val change = event.changes.firstOrNull() ?: break + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val change = event.changes.firstOrNull() ?: break - if (!change.pressed) { + if (!change.pressed) { - if (axis != 0) change.consume() - if (axis == 0) break - - val offset = if (axis == 2) offsetY else offsetX - val vel = velocity - val off = offset.value - scope.launch { - val shouldDismiss = - abs(vel) >= dismissVelPx || - (detached && abs(off) > detachPx * 0.5f) - if (shouldDismiss) { - val target = when (axis) { - 2 -> -size.height * 1.5f - else -> { - val dir = if (abs(vel) > 100f) sign(vel) else sign(off) - dir * size.width * 1.5f + if (axis != 0) change.consume() + if (axis == 0) break + + val offset = if (axis == 2) offsetY else offsetX + val vel = velocity + val off = offset.value + scope.launch { + val shouldDismiss = + abs(vel) >= dismissVelPx || + (detached && abs(off) > detachPx * 0.5f) + if (shouldDismiss) { + val target = when (axis) { + 2 -> -size.height * 1.5f + else -> { + val dir = if (abs(vel) > 100f) sign(vel) else sign(off) + dir * size.width * 1.5f + } } - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - offset.animateTo( - target, - spring(dampingRatio = 1f, stiffness = FLING_STIFFNESS), - ) - dismissed = true - onDismiss() - } else { - if (detached) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) + offset.animateTo( + target, + spring(dampingRatio = 1f, stiffness = FLING_STIFFNESS), + ) + dismissed = true + onDismiss() + } else { + if (detached) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + offset.animateTo(0f, spring(SNAP_DAMPING, SNAP_STIFFNESS)) + detached = false } - offset.animateTo(0f, spring(SNAP_DAMPING, SNAP_STIFFNESS)) - detached = false } + break } - break - } - val dx = change.position.x - down.position.x - val dy = change.position.y - down.position.y + val dx = change.position.x - down.position.x + val dy = change.position.y - down.position.y - if (axis == 0) { - if (abs(dx) > touchSlop || abs(dy) > touchSlop) { - axis = if (abs(dx) >= abs(dy)) { - 1 - } else if (allowUpDismiss && dy < 0) { - 2 + if (axis == 0) { + if (abs(dx) > touchSlop || abs(dy) > touchSlop) { + val wouldBeHorizontal = abs(dx) >= abs(dy) + // If a child (seekbar) has locked horizontal swipe, skip. + if (wouldBeHorizontal && swipeLock.value) { + break + } + axis = if (wouldBeHorizontal) { + 1 + } else if (allowUpDismiss && dy < 0) { + 2 + } else { + break + } } else { - break + continue } - } else { - continue } - } - change.consume() + change.consume() - val delta = change.position - change.previousPosition - val dragAmount = if (axis == 2) delta.y else delta.x - val offset = if (axis == 2) offsetY else offsetX + val delta = change.position - change.previousPosition + val dragAmount = if (axis == 2) delta.y else delta.x + val offset = if (axis == 2) offsetY else offsetX - val now = change.uptimeMillis - if (prevTime > 0L) { - val dt = (now - prevTime).coerceAtLeast(1) / 1000f - val instantVel = dragAmount / dt - velocity = - velocity * (1f - VELOCITY_SMOOTHING) + - instantVel * VELOCITY_SMOOTHING - } - prevTime = now + val now = change.uptimeMillis + if (prevTime > 0L) { + val dt = (now - prevTime).coerceAtLeast(1) / 1000f + val instantVel = dragAmount / dt + velocity = + velocity * (1f - VELOCITY_SMOOTHING) + + instantVel * VELOCITY_SMOOTHING + } + prevTime = now - if (!detached) { - val target = offset.value + dragAmount * MAGNETIC_PULL - if (abs(target) >= detachPx) { - detached = true - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - scope.launch { offset.snapTo(offset.value + dragAmount) } + if (!detached) { + val target = offset.value + dragAmount * MAGNETIC_PULL + if (abs(target) >= detachPx) { + detached = true + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + scope.launch { offset.snapTo(offset.value + dragAmount) } + } else { + scope.launch { offset.snapTo(target) } + } } else { - scope.launch { offset.snapTo(target) } + scope.launch { offset.snapTo(offset.value + dragAmount) } } - } else { - scope.launch { offset.snapTo(offset.value + dragAmount) } } } } - } - ) { - content() + ) { + content() + } } } - From c9394af9db65a0b15c8ea166c087b8ae54778f76 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 4 Apr 2026 13:59:59 +0530 Subject: [PATCH 1118/1315] SystemUI: DynamicBar: Use actual charging info on lock screen * Added periodic ticker which refreshes charging info. * Added customization to disable static battery chip on lock screen. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../domain/AxDynamicBarSettings.kt | 12 +++++++ .../ui/AxDynamicBarChipViewModel.kt | 31 ++++++++++++++-- .../ui/compose/AxDynamicBarKeyguardChip.kt | 36 ++++++++++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index 7ab82754c948..ac4a8890d842 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -26,6 +26,7 @@ class AxDynamicBarSettings @Inject constructor( const val KEY_ENABLED = "ax_dynamic_bar_enabled" const val KEY_EVENTS = "ax_dynamic_bar_events" const val KEY_KEYGUARD_ENABLED = "ax_dynamic_bar_keyguard_enabled" + const val KEY_KEYGUARD_BATTERY_CHIP_MODE = "ax_dynamic_bar_keyguard_battery_chip_mode" const val KEY_COMPACT_NOTIFICATIONS = "ax_dynamic_bar_compact_notifications" } @@ -35,6 +36,9 @@ class AxDynamicBarSettings @Inject constructor( private val _isKeyguardEnabled = MutableStateFlow(true) val isKeyguardEnabled: StateFlow = _isKeyguardEnabled.asStateFlow() + private val _keyguardBatteryChipMode = MutableStateFlow(1) + val keyguardBatteryChipMode: StateFlow = _keyguardBatteryChipMode.asStateFlow() + private val _compactNotifications = MutableStateFlow(true) val compactNotifications: StateFlow = _compactNotifications.asStateFlow() @@ -79,6 +83,12 @@ class AxDynamicBarSettings @Inject constructor( settingsObserver, UserHandle.USER_ALL, ) + secureSettings.registerContentObserverForUserSync( + KEY_KEYGUARD_BATTERY_CHIP_MODE, + false, + settingsObserver, + UserHandle.USER_ALL, + ) secureSettings.registerContentObserverForUserSync( KEY_COMPACT_NOTIFICATIONS, false, @@ -104,6 +114,8 @@ class AxDynamicBarSettings @Inject constructor( secureSettings.getIntForUser(KEY_ENABLED, 0, UserHandle.USER_CURRENT) == 1 _isKeyguardEnabled.value = secureSettings.getIntForUser(KEY_KEYGUARD_ENABLED, 1, UserHandle.USER_CURRENT) == 1 + _keyguardBatteryChipMode.value = + secureSettings.getIntForUser(KEY_KEYGUARD_BATTERY_CHIP_MODE, 1, UserHandle.USER_CURRENT) _compactNotifications.value = secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 _isHeadsUpEnabled.value = diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 8c9f0e5f7713..6f7a28617ebe 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -6,10 +6,12 @@ import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor import com.android.systemui.statusbar.policy.BatteryController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -19,6 +21,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -41,6 +46,7 @@ data class KeyguardBatteryInfo( val timeRemaining: String?, ) +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class AxDynamicBarChipViewModel @Inject @@ -51,8 +57,8 @@ constructor( private val batteryController: BatteryController, authController: AuthController, udfpsOverlayInteractor: UdfpsOverlayInteractor, + keyguardIndicationController: KeyguardIndicationController, ) { - val isLowUdfps: StateFlow = udfpsOverlayInteractor.udfpsOverlayParams .map { params -> @@ -83,6 +89,7 @@ constructor( val isEnabled: StateFlow = interactor.settings.isEnabled val isKeyguardEnabled: StateFlow = interactor.settings.isKeyguardEnabled + val keyguardBatteryChipMode: StateFlow = interactor.settings.keyguardBatteryChipMode val keyguardBatteryInfo: StateFlow = combine( @@ -104,6 +111,26 @@ constructor( KeyguardBatteryInfo(0, false, false, false, null), ) + // Re-compute charging string whenever battery info changes + val batteryString: StateFlow = + keyguardBatteryInfo + .map { it.isCharging } + .distinctUntilChanged() + .flatMapLatest { charging -> + if (charging) { + flow { + while (true) { + emit(keyguardIndicationController.powerChargingString) + delay(BATTERY_STRING_REFRESH_MS) + } + } + } else { + flowOf(keyguardIndicationController.powerChargingString) + } + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.Eagerly, "") + val isOnKeyguard: StateFlow = interactor.isOnKeyguard val isKeyguardFadingAway: StateFlow = interactor.isKeyguardFadingAway @@ -197,6 +224,6 @@ constructor( companion object { private const val LOW_UDFPS_THRESHOLD = 0.93f + private const val BATTERY_STRING_REFRESH_MS = 2_000L } } - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index a3151effbd94..038ad9d20303 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -108,9 +108,11 @@ fun AxDynamicBarKeyguardChip( val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() val isEnabled by viewModel.isEnabled.collectAsStateWithLifecycle() val isKeyguardEnabled by viewModel.isKeyguardEnabled.collectAsStateWithLifecycle() + val keyguardBatteryChipMode by viewModel.keyguardBatteryChipMode.collectAsStateWithLifecycle() val batteryInfo by viewModel.keyguardBatteryInfo.collectAsStateWithLifecycle() val isKeyguardExpanded by viewModel.isKeyguardExpanded.collectAsStateWithLifecycle() val touchSlop = LocalViewConfiguration.current.touchSlop + val batteryString by viewModel.batteryString.collectAsStateWithLifecycle() val motionScheme = MaterialTheme.motionScheme @@ -238,8 +240,12 @@ fun AxDynamicBarKeyguardChip( ) } } else { - - KeyguardBatteryChip(batteryInfo) + KeyguardBatteryChip( + batteryInfo, + keyguardBatteryChipMode, + batteryString, + modifier, + ) } } } @@ -500,7 +506,16 @@ private fun KeyguardChipBody( } @Composable -private fun KeyguardBatteryChip(info: KeyguardBatteryInfo) { +private fun KeyguardBatteryChip( + info: KeyguardBatteryInfo, + keyguardBatteryChipMode: Int, + batteryString: String, + modifier: Modifier, +) { + if (keyguardBatteryChipMode <= 0) return + + if (keyguardBatteryChipMode == 1 && !info.isCharging) return + val accent = when { info.isCharging -> BatteryChargingColor info.isPowerSave -> BatteryPowerSaveColor @@ -510,10 +525,11 @@ private fun KeyguardBatteryChip(info: KeyguardBatteryInfo) { Box(contentAlignment = Alignment.Center) { Row( - modifier = Modifier + modifier = modifier .height(ChipHeight) .clip(ChipShape) .background(accent) + .widthIn(max = 260.dp) .padding(horizontal = SpaceMd), verticalAlignment = Alignment.CenterVertically, ) { @@ -524,6 +540,17 @@ private fun KeyguardBatteryChip(info: KeyguardBatteryInfo) { AnimatedBatteryFillIcon(info.level, contentColor, BatteryIconSize) } Spacer(Modifier.width(SpaceXs)) + if (info.isCharging) { + Text( + batteryString, + style = PillPrimary, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 2, + overflow = TextOverflow.Clip, + modifier = modifier.basicMarquee(), + ) + return + } Text( "${info.level}%", style = PillPrimary, @@ -930,4 +957,3 @@ private fun StopwatchTimeText(event: IslandEvent.Stopwatch, color: Color, modifi Text(formatStopwatch(elapsedMs), color = color, style = PillMono, modifier = modifier) } } - From 5c4aae1ffbc24bd602f6f74a4c96d781ae7fe9dd Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 11 Apr 2026 10:24:58 +0530 Subject: [PATCH 1119/1315] SystemUI: DynamicBar: Fix crash while streaming media Log: 04-05 20:50:06.914 3367 3367 E AndroidRuntime: FATAL EXCEPTION: main 04-05 20:50:06.914 3367 3367 E AndroidRuntime: Process: com.android.systemui, PID: 3367 04-05 20:50:06.914 3367 3367 E AndroidRuntime: java.lang.IllegalArgumentException: Cannot coerce value to an empty range: maximum -1 is less than minimum 0. 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at kotlin.ranges.RangesKt.coerceIn(Unknown Source:4) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at com.android.systemui.axdynamicbar.data.source.MediaIslandManager.computeAccuratePosition(go/retraceme 61f176ec53da69ea03ac24bf6b1fe9234e01c1a6dba2adebc10fa8a4d739f574:98) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at com.android.systemui.axdynamicbar.data.source.MediaIslandManager$mediaListener$1.onPrimaryMetadataOrStateChanged(go/retraceme 61f176ec53da69ea03ac24bf6b1fe9234e01c1a6dba2adebc10fa8a4d739f574:147) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at com.android.systemui.media.NotificationMediaManager$$ExternalSyntheticLambda3.run(go/retraceme 61f176ec53da69ea03ac24bf6b1fe9234e01c1a6dba2adebc10fa8a4d739f574:35) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:1070) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:125) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.os.Looper.dispatchMessage(Looper.java:333) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:263) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.os.Looper.loop(Looper.java:367) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:9298) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:569) 04-05 20:50:06.914 3367 3367 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/data/source/MediaIslandManager.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt index bc6fe23e23bd..820ce587fbc5 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt @@ -129,8 +129,11 @@ constructor( if (updateTime <= 0) return basePos val elapsed = SystemClock.elapsedRealtime() - updateTime val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f - val duration = _mediaEvent.value?.duration?.takeIf { it > 0L } ?: Long.MAX_VALUE - return (basePos + (elapsed * speed).toLong()).coerceIn(0L, duration) + val rawDuration = _mediaEvent.value?.duration ?: Long.MAX_VALUE + val safeDuration = if (rawDuration > 0L) rawDuration else Long.MAX_VALUE + + return (basePos + (elapsed * speed).toLong()) + .coerceIn(0L, safeDuration) } private var cancelProgressPolling: Runnable? = null @@ -192,8 +195,9 @@ constructor( ?: metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "" val artist = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "" - val duration = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L - + val durationRaw = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + val duration = if (durationRaw > 0L) durationRaw else 0L + val albumArt = sessionAlbumArt ?: run { val bmp = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) From 73d30f541aacc81cbca6140e637efadb1009dea7 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 3 Apr 2026 01:21:19 +0530 Subject: [PATCH 1120/1315] SystemUI: DynamicBar: Align expanded cards to top center Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/AxDynamicBarExpandedPanel.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index 6ac9eddbc7c0..dd98c96c7bac 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.input.pointer.PointerEventPass @@ -269,11 +268,6 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight val originX = chipX val origin = TransformOrigin(originX, 0f) - - val chipAlignment = BiasAlignment( - horizontalBias = originX * 2f - 1f, - verticalBias = -1f, - ) AnimatedVisibility( visibleState = expandedVisible, @@ -321,7 +315,7 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight } } .padding(top = topPad), - contentAlignment = chipAlignment, + contentAlignment = Alignment.TopCenter, ) { chipState?.let { state -> ExpandedIslandContent( @@ -335,24 +329,26 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight } } + val alertOrigin = TransformOrigin(0.5f, 0f) + AnimatedVisibility( visibleState = notifVisible, enter = fadeIn(tween(300)) + scaleIn( animationSpec = tween(300), initialScale = 0.4f, - transformOrigin = origin, + transformOrigin = alertOrigin, ), exit = fadeOut(tween(250)) + scaleOut( animationSpec = tween(250), targetScale = 0.4f, - transformOrigin = origin, + transformOrigin = alertOrigin, ), ) { Box( modifier = Modifier .fillMaxWidth() .padding(top = topPad), - contentAlignment = chipAlignment, + contentAlignment = Alignment.TopCenter, ) { val alert = lastAlert.value if (alert != null) { @@ -394,4 +390,3 @@ private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner, lifecycleRegistry.handleLifecycleEvent(event) } } - From 784e1e743ee2f9325f3a349509632515686c656d Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:04:36 +0800 Subject: [PATCH 1121/1315] SystemUI: Add quicklook client [neobuddy89: This is trimmed version for dynamic bar usage.] Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/Android.bp | 1 + .../systemui/quicklook/QuickLookClient.kt | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index dea91f93d406..df0039492f53 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -597,6 +597,7 @@ android_library { "smartspace-proto-java", "smartspace-proto-lite-java", "ax_compose", + "axquicklook-client", ], javacflags: [ "-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas", diff --git a/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt new file mode 100644 index 000000000000..271c35717b5b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.quicklook + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.RemoteException +import android.util.Log +import com.android.axion.quicklook.IAxQuickLookService +import com.android.axion.quicklook.IQuickLookCallback +import com.android.axion.quicklook.QuickLookTarget +import com.android.axion.quicklook.SportsData +import com.android.axion.quicklook.nowPlayingData +import com.android.axion.quicklook.sportsData +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.WeakListenerManager +import javax.inject.Inject + +@SysUISingleton +class QuickLookClient @Inject constructor( + private val context: Context +) { + + interface Callback { + fun onNowPlayingUpdate(nowPlayingText: String, tapAction: PendingIntent?) {} + fun onSportsUpdate(sports: List) {} + } + + private val handler = Handler(Looper.getMainLooper()) + private var service: IAxQuickLookService? = null + private var isBound = false + private var rebindAttempts = 0 + + private var cachedNowPlaying: String = "" + private var cachedNowPlayingAction: PendingIntent? = null + private var cachedSports: List = emptyList() + + private val callbacks = WeakListenerManager() + + private val quickLookCallback = object : IQuickLookCallback.Stub() { + override fun onTargetsUpdated(targets: MutableList) { + handler.post { processTargets(targets) } + } + } + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + service = IAxQuickLookService.Stub.asInterface(binder) + rebindAttempts = 0 + try { + service?.registerCallback(quickLookCallback) + service?.getCurrentTargets()?.let { targets -> + handler.post { processTargets(targets) } + } + } catch (e: RemoteException) { + Log.e(TAG, "Failed to register callback", e) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + service = null + scheduleRebind() + } + } + + fun addCallback(callback: Callback) { + callbacks.addListener(callback) + if (!isBound) { + bindService() + } + deliverCachedData(callback) + } + + fun removeCallback(callback: Callback) { + callbacks.removeListener(callback) + if (callbacks.isEmpty()) { + unbindService() + } + } + + private fun deliverCachedData(callback: Callback) { + if (cachedNowPlaying.isNotEmpty()) { + callback.onNowPlayingUpdate(cachedNowPlaying, cachedNowPlayingAction) + } + if (cachedSports.isNotEmpty()) { + callback.onSportsUpdate(cachedSports) + } + } + + private fun processTargets(targets: List) { + var hasNowPlaying = false + val sportsTargets = mutableListOf() + + for (target in targets) { + when (target.targetType) { + QuickLookTarget.TYPE_NOW_PLAYING -> { + hasNowPlaying = true + val np = target.nowPlayingData ?: continue + val npAction = target.primaryAction?.pendingIntent + if (np.title != cachedNowPlaying || npAction != cachedNowPlayingAction) { + cachedNowPlaying = np.title + cachedNowPlayingAction = npAction + callbacks.notify { it.onNowPlayingUpdate(np.title, npAction) } + } + } + QuickLookTarget.TYPE_SPORTS -> { + val s = target.sportsData ?: continue + sportsTargets.add(s) + val sportTitle = when { + s.score1.isNotEmpty() && s.score2.isNotEmpty() -> + "${s.team1Name} ${s.score1} - ${s.score2} ${s.team2Name}" + s.team1Name.isNotEmpty() && s.team2Name.isNotEmpty() -> + "${s.team1Name} vs ${s.team2Name}" + else -> target.title ?: "" + } + } + } + } + + if (sportsTargets != cachedSports) { + cachedSports = sportsTargets + callbacks.notify { it.onSportsUpdate(sportsTargets) } + } + + if (!hasNowPlaying && cachedNowPlaying.isNotEmpty()) { + cachedNowPlaying = "" + cachedNowPlayingAction = null + callbacks.notify { it.onNowPlayingUpdate("", null) } + } + } + + private fun bindService() { + if (isBound) return + val intent = Intent(SERVICE_ACTION).apply { + setPackage(SERVICE_PACKAGE) + } + try { + isBound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to bind to AxQuickLook service", e) + } + } + + private fun unbindService() { + if (!isBound) return + try { + service?.unregisterCallback(quickLookCallback) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to unregister callback", e) + } + try { + context.unbindService(serviceConnection) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Service not registered", e) + } + service = null + isBound = false + handler.removeCallbacksAndMessages(null) + } + + private fun scheduleRebind() { + if (!isBound || callbacks.isEmpty()) return + rebindAttempts++ + val delay = (REBIND_DELAY_MS * rebindAttempts).coerceAtMost(MAX_REBIND_DELAY_MS) + handler.postDelayed({ bindService() }, delay) + } + + companion object { + private const val TAG = "QuickLookClient" + private const val SERVICE_ACTION = "com.android.axion.quicklook.SERVICE" + private const val SERVICE_PACKAGE = "com.android.axion.quicklook" + private const val REBIND_DELAY_MS = 5000L + private const val MAX_REBIND_DELAY_MS = 30000L + } +} From c4a9b60e3efc91c5fa1252d83081777afd8d1a9f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 15 Apr 2026 00:29:48 +0530 Subject: [PATCH 1122/1315] SystemUI: Adapt platform hooks for dynamic bar usage only Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/values/matrixx_strings.xml | 3 +++ .../systemui/ax/AxPlatformObservers.kt | 24 ++----------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index a145aab02880..da296f2c8346 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -321,4 +321,7 @@ Final Halftime Upcoming + + + Screenshot diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt index a40d22de79b1..d323b044e2c1 100644 --- a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt @@ -42,10 +42,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.NotificationMediaManager -import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData -import com.android.systemui.plugins.keyguard.ui.clocks.ClockData import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.quicklook.QuickLookClient import com.android.systemui.screenrecord.ScreenRecordUxController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.connectivity.AccessPointController @@ -107,7 +104,6 @@ class AxPlatformObservers @Inject constructor( private val localBluetoothManager: LocalBluetoothManager?, private val notificationMediaManager: NotificationMediaManager, private val nextAlarmController: NextAlarmController, - private val quickLookClient: QuickLookClient, private val configurationController: ConfigurationController, private val statusBarStateController: StatusBarStateController, private val keyguardStateController: KeyguardStateController, @@ -157,7 +153,6 @@ class AxPlatformObservers @Inject constructor( dataSaverController.addCallback(dataSaverCallback) notificationMediaManager.addCallback(mediaListener) nextAlarmController.addCallback(nextAlarmCallback) - quickLookClient.addCallback(quickLookCallback) configurationController.addCallback(configurationListener) statusBarStateController.addCallback(dozeCallback) keyguardStateController.addCallback(keyguardCallback) @@ -461,6 +456,8 @@ class AxPlatformObservers @Inject constructor( }) } } + + override fun onFlashlightStrengthChanged(level: Int) {} } private val rotationCallback = @@ -615,23 +612,6 @@ class AxPlatformObservers @Inject constructor( } } - private val quickLookCallback = object : QuickLookClient.Callback { - override fun onClockDataChanged(data: ClockData) { - val cal = data.calendar - if (cal != CalendarSimpleData.EMPTY) { - stateManager.broadcastState(AxPlatformClient.KEY_CALENDAR, Bundle().apply { - putLong("id", cal.id) - putString("title", cal.title ?: "") - putLong("startTime", cal.startTime) - putLong("endTime", cal.endTime) - putString("location", cal.location ?: "") - }) - } else { - stateManager.broadcastState(AxPlatformClient.KEY_CALENDAR, Bundle()) - } - } - } - private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onConfigChanged(newConfig: Configuration) = exportConfigInfo(newConfig) override fun onDensityOrFontScaleChanged() = exportConfigInfo(null) From 0e6c7f421a5d715f68b078e5f62168e068d53d4f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 2 Apr 2026 07:04:57 +0000 Subject: [PATCH 1123/1315] SystemUI: DynamicBar: Redesign media pill style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/PillIslandContent.kt | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index be22efa34e0b..4ba314c45cda 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -978,7 +978,7 @@ internal fun PillEventText( is IslandEvent.MicCamActive -> MicCamText(event, modifier, overrideColor) is IslandEvent.AudioRecording -> AudioRecText(event, modifier, overrideColor) is IslandEvent.Casting -> MarqueeLabel(event.deviceName.take(12), overrideColor ?: TealAccent, modifier) - is IslandEvent.Media -> MediaText(event, modifier, overrideColor) + is IslandEvent.Media -> MediaTitleText(event, modifier, overrideColor) is IslandEvent.PromotedOngoing -> PromotedOngoingText(event, modifier, overrideColor) is IslandEvent.Sports -> SportsText(event, modifier, overrideColor) is IslandEvent.NowPlaying -> MarqueeLabel( @@ -1101,12 +1101,24 @@ private fun AudioRecText(event: IslandEvent.AudioRecording, modifier: Modifier, } @Composable -private fun MediaText(event: IslandEvent.Media, modifier: Modifier, overrideColor: Color? = null) { - val baseColor = overrideColor ?: OrangeAccent - val alpha = if (event.isPlaying) 1f else AlphaHint - val color = baseColor.copy(alpha = alpha) - val text = if (event.artist.isNotBlank()) "${event.track} - ${event.artist}" else event.track - MarqueeLabel(text, color, modifier.widthIn(max = 66.dp)) +private fun MediaTitleText(event: IslandEvent.Media, modifier: Modifier, overrideColor: Color? = null) { + val color = (overrideColor ?: OrangeAccent).copy( + alpha = if (event.isPlaying) 1f else AlphaHint + ) + val title = event.track.ifEmpty { + event.artist.ifEmpty { "Music" } + } + Text( + text = title, + color = color, + style = PillPrimary, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = modifier.basicMarquee( + initialDelayMillis = 3_000, + repeatDelayMillis = 5_000, + ), + ) } @Composable From 38c67471ca50be7bdbfcf2f64337aaf3eb403f20 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 2 Apr 2026 08:25:45 +0000 Subject: [PATCH 1124/1315] SystemUI: DynamicBar: Follow qs media seekbar style Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../domain/AxDynamicBarInteractor.kt | 1 + .../domain/AxDynamicBarSettings.kt | 17 ++ .../ui/AxDynamicBarChipViewModel.kt | 1 + .../ui/compose/ExpandedMediaContent.kt | 192 ++++++++++-------- .../ui/compose/KeyguardExpandedContent.kt | 171 +++++++++------- 5 files changed, 227 insertions(+), 155 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt index 4a9a1c769d99..7756b1b3d537 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt @@ -76,6 +76,7 @@ constructor( val qsExpansion: StateFlow = shadeInteractor.qsExpansion val legacyShadeExpansion: StateFlow = shadeRepository.legacyShadeExpansion + val mediaUseWaveform: StateFlow = settings.useWaveformSeekBar private val _isOnKeyguard = MutableStateFlow(false) val isOnKeyguard: StateFlow = _isOnKeyguard.asStateFlow() private val _isKeyguardFadingAway = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index ac4a8890d842..27e2fa3a8779 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -3,6 +3,7 @@ package com.android.systemui.axdynamicbar.domain import android.database.ContentObserver import android.os.Handler import android.os.UserHandle +import android.provider.Settings import android.provider.Settings.Global import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.shared.EVENT_TYPE_IDS @@ -21,6 +22,7 @@ class AxDynamicBarSettings @Inject constructor( @Main private val mainHandler: Handler, private val secureSettings: SecureSettings, private val globalSettings: GlobalSettings, + private val context: android.content.Context, ) { companion object { const val KEY_ENABLED = "ax_dynamic_bar_enabled" @@ -30,6 +32,8 @@ class AxDynamicBarSettings @Inject constructor( const val KEY_COMPACT_NOTIFICATIONS = "ax_dynamic_bar_compact_notifications" } + private val contentResolver = context.contentResolver + private val _isEnabled = MutableStateFlow(false) @get:JvmName("getIsEnabled") val isEnabled: StateFlow = _isEnabled.asStateFlow() @@ -45,6 +49,9 @@ class AxDynamicBarSettings @Inject constructor( private val _isHeadsUpEnabled = MutableStateFlow(true) val isHeadsUpEnabled: StateFlow = _isHeadsUpEnabled.asStateFlow() + private val _useWaveformSeekBar = MutableStateFlow(false) + val useWaveformSeekBar: StateFlow = _useWaveformSeekBar.asStateFlow() + private val _disabledEventTypes = MutableStateFlow>(emptySet()) val disabledEventTypes: StateFlow> = _disabledEventTypes.asStateFlow() @@ -100,6 +107,12 @@ class AxDynamicBarSettings @Inject constructor( false, settingsObserver, ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.MEDIA_WAVEFORM_SEEKBAR), + false, + settingsObserver, + UserHandle.USER_ALL, + ) } fun destroy() { @@ -107,6 +120,7 @@ class AxDynamicBarSettings @Inject constructor( initialized = false secureSettings.getContentResolver().unregisterContentObserver(settingsObserver) globalSettings.getContentResolver().unregisterContentObserver(settingsObserver) + contentResolver.unregisterContentObserver(settingsObserver) } private fun refresh() { @@ -120,6 +134,9 @@ class AxDynamicBarSettings @Inject constructor( secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 _isHeadsUpEnabled.value = globalSettings.getInt(Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1) == 1 + _useWaveformSeekBar.value = + Settings.System.getIntForUser( + contentResolver, Settings.System.MEDIA_WAVEFORM_SEEKBAR, 0, UserHandle.USER_CURRENT,) == 1 val json = secureSettings.getStringForUser(KEY_EVENTS, UserHandle.USER_CURRENT) ?: "" _disabledEventTypes.value = diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 6f7a28617ebe..03da9ddfdadf 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -90,6 +90,7 @@ constructor( val isEnabled: StateFlow = interactor.settings.isEnabled val isKeyguardEnabled: StateFlow = interactor.settings.isKeyguardEnabled val keyguardBatteryChipMode: StateFlow = interactor.settings.keyguardBatteryChipMode + val mediaUseWaveform: StateFlow = interactor.mediaUseWaveform val keyguardBatteryInfo: StateFlow = combine( diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index 893bb13d9617..a52441629879 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -1,6 +1,7 @@ package com.android.systemui.axdynamicbar.ui.compose import android.graphics.drawable.GradientDrawable +import android.media.AudioManager import android.graphics.drawable.LayerDrawable import android.util.TypedValue import android.widget.SeekBar @@ -61,6 +62,8 @@ import com.android.systemui.axdynamicbar.shared.* import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import com.android.systemui.res.R import kotlinx.coroutines.delay +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.media.controls.ui.view.WaveformSeekBar private val AlbumArtSize = 80.dp private val PlayPauseSize = 56.dp @@ -72,6 +75,11 @@ private val SeekBarHeight = 28.dp internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { val colors = rememberMediaColors(event) val accent = colors.accent + val useWaveform = if (interactor is com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor) { + interactor.mediaUseWaveform.collectAsStateWithLifecycle().value + } else { + false + } Surface( modifier = Modifier.fillMaxWidth().border(1.dp, CardBorderBrush, ShapeCard), @@ -146,7 +154,7 @@ internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { verticalArrangement = Arrangement.spacedBy(SpaceLg), ) { if (event.duration > 0L) { - MediaSeekBar(event, interactor, accent) + MediaSeekBar(event, interactor, accent, useWaveform) } MediaControls(event, interactor, accent) } @@ -160,6 +168,11 @@ internal fun MediaExpanded( interactor: IslandActions, modifier: Modifier = Modifier, ) { + val useWaveform = if (interactor is com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor) { + interactor.mediaUseWaveform.collectAsStateWithLifecycle().value + } else { + false + } val colors = rememberMediaColors(event) val accent = colors.accent @@ -227,7 +240,7 @@ internal fun MediaExpanded( MediaControls(event, interactor, accent) if (event.duration > 0L) { - MediaSeekBar(event, interactor, accent) + MediaSeekBar(event, interactor, accent, useWaveform) } } } @@ -306,6 +319,7 @@ private fun MediaSeekBar( event: IslandEvent.Media, interactor: IslandActions, accent: Color, + useWaveform: Boolean = false, ) { val mediaProgress = rememberMediaProgress(event) val isPlaying = event.isPlaying @@ -406,88 +420,102 @@ private fun MediaSeekBar( }, contentAlignment = Alignment.Center, ) { - AndroidView( - factory = { context -> - SeekBar(context).apply { - max = 10_000 - splitTrack = false - setPadding(0, 0, 0, 0) - // Disable direct touch — Compose handles all gestures above - isEnabled = false - - // Pill-shaped thumb - thumb = createSeekBarThumb(context, accentArgb) - thumbOffset = thumb.intrinsicWidth / 2 - - // Set up SquigglyProgress on the progress layer - val layer = (progressDrawable?.mutate() as? LayerDrawable) - if (layer != null) { - layer.findDrawableByLayerId(android.R.id.background) - ?.mutate()?.setTint(trackAlphaArgb) - - layer.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.mutate()?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) - - val squiggle = SquigglyProgress().apply { - waveLength = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_wavelength - ).toFloat() - lineAmplitude = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_amplitude - ).toFloat() - phaseSpeed = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_phase - ).toFloat() - strokeWidth = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_stroke_width - ).toFloat() - setTint(accentArgb) - drawRemainingLine = false - transitionEnabled = false - animate = false + if (useWaveform) { + AndroidView( + factory = { context -> + WaveformSeekBar(context).apply { + max = 10_000 + setWaveformColor(accentArgb) + setThumbColor(accentArgb) + isEnabled = false + } + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + if (bar.progress != target) bar.progress = target + when { + isPlaying && !bar.isPlaying -> bar.startWaveAnimation() + !isPlaying && bar.isPlaying -> bar.stopWaveAnimation() + } + }, + modifier = Modifier.fillMaxWidth().height(SeekBarHeight), + ) + } else { + AndroidView( + factory = { context -> + SeekBar(context).apply { + max = 10_000 + splitTrack = false + setPadding(0, 0, 0, 0) + isEnabled = false + + thumb = createSeekBarThumb(context, accentArgb) + thumbOffset = thumb.intrinsicWidth / 2 + + val layer = (progressDrawable?.mutate() as? LayerDrawable) + if (layer != null) { + layer.findDrawableByLayerId(android.R.id.background) + ?.mutate()?.setTint(trackAlphaArgb) + layer.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.mutate()?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + val squiggle = SquigglyProgress().apply { + waveLength = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_wavelength + ).toFloat() + lineAmplitude = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_amplitude + ).toFloat() + phaseSpeed = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_phase + ).toFloat() + strokeWidth = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_stroke_width + ).toFloat() + setTint(accentArgb) + drawRemainingLine = false + transitionEnabled = false + animate = false + } + layer.setDrawableByLayerId(android.R.id.progress, squiggle) + progressDrawable = layer } - layer.setDrawableByLayerId(android.R.id.progress, squiggle) - progressDrawable = layer } - } - }, - update = { bar -> - val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) - bar.progress = target - - // Re-tint thumb for accent color changes (e.g. track switch) - (bar.thumb as? GradientDrawable)?.setColor(accentArgb) - - val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() - bar.thumb?.alpha = alpha - - val layer = bar.progressDrawable as? LayerDrawable - - // Re-tint track colors - layer?.findDrawableByLayerId(android.R.id.background) - ?.setTint(trackAlphaArgb) - layer?.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) - - val squiggle = layer - ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress - - squiggle?.apply { - setTint(accentArgb) - setAlpha(alpha) - animate = isPlaying && !isScrubbing - } - - layer?.alpha = alpha - }, - modifier = Modifier.fillMaxWidth().height(SeekBarHeight), - ) + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + bar.progress = target + + (bar.thumb as? GradientDrawable)?.setColor(accentArgb) + + val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() + bar.thumb?.alpha = alpha + + val layer = bar.progressDrawable as? LayerDrawable + + // Re-tint track colors + layer?.findDrawableByLayerId(android.R.id.background) + ?.setTint(trackAlphaArgb) + layer?.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + + val squiggle = layer + ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress + squiggle?.apply { + setTint(accentArgb) + setAlpha(alpha) + animate = isPlaying && !isScrubbing + } + layer?.alpha = alpha + }, + modifier = Modifier.fillMaxWidth().height(SeekBarHeight), + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt index 00a485d05fba..b183400d691e 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -93,6 +93,8 @@ import com.android.systemui.axdynamicbar.shared.* import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import kotlinx.coroutines.delay +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.media.controls.ui.view.WaveformSeekBar private val KeyguardSeekBarHeight = 28.dp @@ -212,6 +214,11 @@ private fun TonalBanner( @Composable private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActions) { + val useWaveform = if (interactor is com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor) { + interactor.mediaUseWaveform.collectAsStateWithLifecycle().value + } else { + false + } val colors = rememberMediaColors(event) val eqBars = if (event.isPlaying) { @@ -339,7 +346,7 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio } if (event.duration > 0L) { - KeyguardMediaSeekBar(event, interactor, colors.accent) + KeyguardMediaSeekBar(event, interactor, colors.accent, useWaveform) } } @@ -432,6 +439,7 @@ private fun KeyguardMediaSeekBar( event: IslandEvent.Media, interactor: IslandActions, accent: Color, + useWaveform: Boolean = false, ) { val mediaProgress = rememberMediaProgress(event) val isPlaying = event.isPlaying @@ -516,85 +524,102 @@ private fun KeyguardMediaSeekBar( }, contentAlignment = Alignment.Center, ) { - AndroidView( - factory = { context -> - SeekBar(context).apply { - max = 10_000 - splitTrack = false - setPadding(0, 0, 0, 0) - isEnabled = false - - thumb = createKeyguardSeekBarThumb(context, accentArgb) - thumbOffset = thumb.intrinsicWidth / 2 - - val layer = (progressDrawable?.mutate() as? LayerDrawable) - if (layer != null) { - layer.findDrawableByLayerId(android.R.id.background) - ?.mutate()?.setTint(trackAlphaArgb) - - layer.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.mutate()?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) - - val squiggle = SquigglyProgress().apply { - waveLength = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_wavelength - ).toFloat() - lineAmplitude = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_amplitude - ).toFloat() - phaseSpeed = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_phase - ).toFloat() - strokeWidth = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_stroke_width - ).toFloat() - setTint(accentArgb) - drawRemainingLine = false - transitionEnabled = false - animate = false + if (useWaveform) { + AndroidView( + factory = { context -> + WaveformSeekBar(context).apply { + max = 10_000 + setWaveformColor(accentArgb) + setThumbColor(accentArgb) + isEnabled = false + } + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + if (bar.progress != target) bar.progress = target + when { + isPlaying && !bar.isPlaying -> bar.startWaveAnimation() + !isPlaying && bar.isPlaying -> bar.stopWaveAnimation() + } + }, + modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), + ) + } else { + AndroidView( + factory = { context -> + SeekBar(context).apply { + max = 10_000 + splitTrack = false + setPadding(0, 0, 0, 0) + isEnabled = false + + thumb = createKeyguardSeekBarThumb(context, accentArgb) + thumbOffset = thumb.intrinsicWidth / 2 + + val layer = (progressDrawable?.mutate() as? LayerDrawable) + if (layer != null) { + layer.findDrawableByLayerId(android.R.id.background) + ?.mutate()?.setTint(trackAlphaArgb) + layer.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.mutate()?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) + val squiggle = SquigglyProgress().apply { + waveLength = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_wavelength + ).toFloat() + lineAmplitude = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_amplitude + ).toFloat() + phaseSpeed = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_phase + ).toFloat() + strokeWidth = context.resources.getDimensionPixelSize( + R.dimen.qs_media_seekbar_progress_stroke_width + ).toFloat() + setTint(accentArgb) + drawRemainingLine = false + transitionEnabled = false + animate = false + } + layer.setDrawableByLayerId(android.R.id.progress, squiggle) + progressDrawable = layer } - layer.setDrawableByLayerId(android.R.id.progress, squiggle) - progressDrawable = layer } - } - }, - update = { bar -> - val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) - bar.progress = target - - // Re-tint thumb for accent color changes (e.g. track switch) - (bar.thumb as? GradientDrawable)?.setColor(accentArgb) - - val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() - bar.thumb?.alpha = alpha + }, + update = { bar -> + val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) + bar.progress = target - val layer = bar.progressDrawable as? LayerDrawable + (bar.thumb as? GradientDrawable)?.setColor(accentArgb) - // Re-tint track colors - layer?.findDrawableByLayerId(android.R.id.background) - ?.setTint(trackAlphaArgb) - layer?.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) + val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() + bar.thumb?.alpha = alpha - val squiggle = layer - ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress + val layer = bar.progressDrawable as? LayerDrawable - squiggle?.apply { - setTint(accentArgb) - setAlpha(alpha) - animate = isPlaying && !isScrubbing - } + // Re-tint track colors + layer?.findDrawableByLayerId(android.R.id.background) + ?.setTint(trackAlphaArgb) + layer?.findDrawableByLayerId(android.R.id.secondaryProgress) + ?.setTint( + com.android.internal.graphics.ColorUtils + .setAlphaComponent(accentArgb, 60) + ) - layer?.alpha = alpha - }, - modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), - ) + val squiggle = layer + ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress + squiggle?.apply { + setTint(accentArgb) + setAlpha(alpha) + animate = isPlaying && !isScrubbing + } + layer?.alpha = alpha + }, + modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), + ) + } } Row( modifier = Modifier.fillMaxWidth(), From fa28ea3bb356e630328d007c6e21b81c0bd32efc Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 2 Apr 2026 12:09:24 +0000 Subject: [PATCH 1125/1315] SystemUI: DynamicBar: Improve chip padding and size Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/compose/AxDynamicBarChip.kt | 2 ++ .../axdynamicbar/ui/compose/PillIslandContent.kt | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt index 553c2987bb91..7f4c9fbd9e56 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -110,6 +110,8 @@ fun AxDynamicBarChip( enter = fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.8f, animationSpec = motionScheme.defaultSpatialSpec()), exit = fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.8f, animationSpec = motionScheme.fastSpatialSpec()), modifier = modifier + .padding(start = 4.dp, end = 2.dp) + .widthIn(min = 25.dp, max = 90.dp) .pointerInput(viewModel) { awaitEachGesture { val down = awaitFirstDown(pass = PointerEventPass.Initial) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index 4ba314c45cda..f5bd992ff06f 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -1114,10 +1115,12 @@ private fun MediaTitleText(event: IslandEvent.Media, modifier: Modifier, overrid style = PillPrimary, maxLines = 1, overflow = TextOverflow.Clip, - modifier = modifier.basicMarquee( - initialDelayMillis = 3_000, - repeatDelayMillis = 5_000, - ), + modifier = modifier + .widthIn(max = 90.dp) + .basicMarquee( + initialDelayMillis = 3_000, + repeatDelayMillis = 5_000, + ), ) } From 4b714708243908e2b66b2f4d8a72d28ab1127eec Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 4 Apr 2026 15:43:34 +0000 Subject: [PATCH 1126/1315] SystemUI: DynamicBar: Improve battery icon with rounded corners and proper nub Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/AxDynamicBarKeyguardChip.kt | 88 ++++++++++++++----- 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index 038ad9d20303..f9e79ecdf27b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -69,6 +69,9 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale @@ -631,34 +634,77 @@ private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = Bat animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), label = "kg_battery_fill", ) - Canvas(modifier = Modifier.size(iconSize)) { + Canvas(modifier = Modifier.size(iconSize * 1.6f, iconSize)) { val w = size.width val h = size.height - val bodyW = w * 0.7f - val bodyH = h * 0.85f - val bodyX = (w - bodyW) / 2f - val bodyY = h - bodyH - val tipW = bodyW * 0.4f - val tipH = h * 0.1f - - drawRect( - color.copy(alpha = AlphaTertiary), - topLeft = Offset((w - tipW) / 2f, 0f), - size = Size(tipW, tipH), + val cornerRadius = h * 0.18f + + val bodyW = w * 0.88f + val bodyH = h * 0.62f + val bodyX = 0f + val bodyY = (h - bodyH) / 2f + + val nubW = w * 0.07f + val nubH = bodyH * 0.38f + val nubX = bodyW + val nubY = bodyY + (bodyH - nubH) / 2f + val nubRadius = nubW * 0.45f + + val inset = h * 0.05f + val innerRadius = (cornerRadius - inset).coerceAtLeast(2f) + + drawRoundRect( + color = color.copy(alpha = 0.75f), + topLeft = Offset(nubX, nubY), + size = Size(nubW, nubH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(nubRadius, nubRadius), ) - - drawRect( - color.copy(alpha = AlphaTertiary), + + drawRoundRect( + color = color.copy(alpha = 0.35f), topLeft = Offset(bodyX, bodyY), size = Size(bodyW, bodyH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius, cornerRadius), + style = androidx.compose.ui.graphics.drawscope.Stroke(width = h * 0.09f), ) - - val fillH = bodyH * fillFraction - drawRect( - color, - topLeft = Offset(bodyX, bodyY + bodyH - fillH), - size = Size(bodyW, fillH), + + drawRoundRect( + color = color.copy(alpha = 0.12f), + topLeft = Offset(bodyX + inset, bodyY + inset), + size = Size(bodyW - inset * 2, bodyH - inset * 2), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(innerRadius, innerRadius), + style = androidx.compose.ui.graphics.drawscope.Stroke(width = h * 0.03f), ) + + if (fillFraction > 0f) { + val fillPad = h * 0.10f + val fillAreaX = bodyX + fillPad + val fillAreaY = bodyY + fillPad + val fillAreaW = bodyW - fillPad * 2 + val fillAreaH = bodyH - fillPad * 2 + val fillRadius = (cornerRadius - fillPad).coerceAtLeast(2f) + + val clipPath = Path().apply { + addRoundRect( + RoundRect( + left = fillAreaX, + top = fillAreaY, + right = fillAreaX + fillAreaW, + bottom = fillAreaY + fillAreaH, + radiusX = fillRadius, + radiusY = fillRadius, + ) + ) + } + + withTransform({ clipPath(clipPath) }) { + drawRect( + color = color, + topLeft = Offset(fillAreaX, fillAreaY), + size = Size(fillAreaW * fillFraction, fillAreaH), + ) + } + } } } From 7a8408056c95a79ee9dfd7b9c36918b341694d65 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:41:11 +0800 Subject: [PATCH 1127/1315] SystemUI: Fixing jank by media player layer type jank scripts detects this as jank source blocking the main/ui thread during qs expansion animation Change-Id: I537bc9154c19ed1ac6558ae4e81c2e6e1f5ce260 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/controller/MediaCarouselController.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index ab0d9b5af4ef..3e3cff0fa821 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -182,6 +182,8 @@ constructor( /** The progress of the transition or 1.0 if there is no transition happening */ private var currentTransitionProgress: Float = 1.0f + private var playersInTransition = false + /** The measured width of the carousel */ private var carouselMeasureWidth: Int = 0 @@ -794,6 +796,17 @@ constructor( currentStartLocation = startLocation currentEndLocation = endLocation currentTransitionProgress = progress + val inTransition = + startLocation != endLocation && + startLocation != MediaHierarchyManager.LOCATION_UNKNOWN + if (inTransition != playersInTransition) { + playersInTransition = inTransition + val layerType = + if (inTransition) View.LAYER_TYPE_NONE else View.LAYER_TYPE_HARDWARE + for (mediaPlayer in MediaPlayerData.players()) { + mediaPlayer.mediaViewHolder?.player?.setLayerType(layerType, null) + } + } for (mediaPlayer in MediaPlayerData.players()) { updateViewControllerToState(mediaPlayer.mediaViewController, immediately) } From ac5afc765edb4cfd8d4c2b5312bc12296b72c503 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 12 Apr 2026 12:56:06 +0000 Subject: [PATCH 1128/1315] SystemUI: DynamicBar: Make keyguard music pill optional [1/2] Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../domain/AxDynamicBarSettings.kt | 12 +++++++++++ .../ui/AxDynamicBarChipViewModel.kt | 1 + .../ui/compose/AxDynamicBarKeyguardChip.kt | 21 ++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index 27e2fa3a8779..1855820b5f73 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -28,6 +28,7 @@ class AxDynamicBarSettings @Inject constructor( const val KEY_ENABLED = "ax_dynamic_bar_enabled" const val KEY_EVENTS = "ax_dynamic_bar_events" const val KEY_KEYGUARD_ENABLED = "ax_dynamic_bar_keyguard_enabled" + const val KEY_KEYGUARD_MUSIC_PILL_ENABLED = "ax_dynamic_bar_keyguard_music_pill" const val KEY_KEYGUARD_BATTERY_CHIP_MODE = "ax_dynamic_bar_keyguard_battery_chip_mode" const val KEY_COMPACT_NOTIFICATIONS = "ax_dynamic_bar_compact_notifications" } @@ -40,6 +41,9 @@ class AxDynamicBarSettings @Inject constructor( private val _isKeyguardEnabled = MutableStateFlow(true) val isKeyguardEnabled: StateFlow = _isKeyguardEnabled.asStateFlow() + private val _isKeyguardMusicPillEnabled = MutableStateFlow(false) + val isKeyguardMusicPillEnabled: StateFlow = _isKeyguardMusicPillEnabled.asStateFlow() + private val _keyguardBatteryChipMode = MutableStateFlow(1) val keyguardBatteryChipMode: StateFlow = _keyguardBatteryChipMode.asStateFlow() @@ -90,6 +94,12 @@ class AxDynamicBarSettings @Inject constructor( settingsObserver, UserHandle.USER_ALL, ) + secureSettings.registerContentObserverForUserSync( + KEY_KEYGUARD_MUSIC_PILL_ENABLED, + false, + settingsObserver, + UserHandle.USER_ALL, + ) secureSettings.registerContentObserverForUserSync( KEY_KEYGUARD_BATTERY_CHIP_MODE, false, @@ -130,6 +140,8 @@ class AxDynamicBarSettings @Inject constructor( secureSettings.getIntForUser(KEY_KEYGUARD_ENABLED, 1, UserHandle.USER_CURRENT) == 1 _keyguardBatteryChipMode.value = secureSettings.getIntForUser(KEY_KEYGUARD_BATTERY_CHIP_MODE, 1, UserHandle.USER_CURRENT) + _isKeyguardMusicPillEnabled.value = + secureSettings.getIntForUser(KEY_KEYGUARD_MUSIC_PILL_ENABLED, 0, UserHandle.USER_CURRENT) == 1 _compactNotifications.value = secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 _isHeadsUpEnabled.value = diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 03da9ddfdadf..0acc123dafc9 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -90,6 +90,7 @@ constructor( val isEnabled: StateFlow = interactor.settings.isEnabled val isKeyguardEnabled: StateFlow = interactor.settings.isKeyguardEnabled val keyguardBatteryChipMode: StateFlow = interactor.settings.keyguardBatteryChipMode + val isKeyguardMusicPillEnabled: StateFlow = interactor.settings.isKeyguardMusicPillEnabled val mediaUseWaveform: StateFlow = interactor.mediaUseWaveform val keyguardBatteryInfo: StateFlow = diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index f9e79ecdf27b..a2a0ed2f7e48 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -111,6 +111,7 @@ fun AxDynamicBarKeyguardChip( val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() val isEnabled by viewModel.isEnabled.collectAsStateWithLifecycle() val isKeyguardEnabled by viewModel.isKeyguardEnabled.collectAsStateWithLifecycle() + val isKeyguardMusicPillEnabled by viewModel.isKeyguardMusicPillEnabled.collectAsStateWithLifecycle() val keyguardBatteryChipMode by viewModel.keyguardBatteryChipMode.collectAsStateWithLifecycle() val batteryInfo by viewModel.keyguardBatteryInfo.collectAsStateWithLifecycle() val isKeyguardExpanded by viewModel.isKeyguardExpanded.collectAsStateWithLifecycle() @@ -194,7 +195,25 @@ fun AxDynamicBarKeyguardChip( ) { val chipState = state if (chipState != null) { - val displayEvent = chipState.notificationAlert ?: chipState.event + val rawEvent = chipState.notificationAlert ?: chipState.event + val displayEvent: com.android.systemui.axdynamicbar.model.IslandEvent? = + when { + isKeyguardMusicPillEnabled -> rawEvent + rawEvent is com.android.systemui.axdynamicbar.model.IslandEvent.Media -> { + chipState.allEvents.firstOrNull { it !is com.android.systemui.axdynamicbar.model.IslandEvent.Media } + } + else -> rawEvent + } + + if (displayEvent == null) { + KeyguardBatteryChip( + batteryInfo, + keyguardBatteryChipMode, + batteryString, + modifier, + ) + return@AnimatedVisibility + } AnimatedContent( targetState = displayEvent, From a7594cef46fe78941cd11a0ef6838ebe607b0d84 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 16 Apr 2026 17:47:00 +0000 Subject: [PATCH 1129/1315] SystemUI: DynamicBar: Improve expand media ui Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/ExpandedMediaContent.kt | 149 +++++++++++------- 1 file changed, 90 insertions(+), 59 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index a52441629879..9b6dc28eaf4b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -46,7 +46,9 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.toArgb @@ -86,77 +88,106 @@ internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { shape = ShapeCard, color = CardBg, ) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( + Box(modifier = Modifier.fillMaxWidth()) { + + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(350.dp), + contentDescription = null, + modifier = Modifier + .matchParentSize() + .blur(24.dp), + contentScale = ContentScale.Crop, + ) + } ?: Box( modifier = Modifier - .fillMaxWidth() - .clickable { - interactor.openMediaApp() - interactor.collapseIsland() - } - .padding(SpaceXxl), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceXxl), - ) { - event.albumArt?.let { art -> - Image( - bitmap = art.toScaledBitmap(AlbumArtSize), - contentDescription = null, - modifier = Modifier.size(AlbumArtSize).clip(ShapeLg), - contentScale = ContentScale.Crop, + .matchParentSize() + .background(CardBg), + ) + + Box( + modifier = Modifier + .matchParentSize() + .background( + Brush.verticalGradient( + 0.0f to Color(0xFF000000).copy(alpha = 0.45f), + 0.45f to Color(0xFF000000).copy(alpha = 0.7f), + 1.0f to Color(0xFF000000).copy(alpha = 1.0f), + ) ) - } ?: Box( + ) + + Column(modifier = Modifier.fillMaxWidth()) { + Row( modifier = Modifier - .size(AlbumArtSize) - .clip(ShapeLg) - .background(accent.copy(alpha = AlphaFaint)), - contentAlignment = Alignment.Center, + .fillMaxWidth() + .clickable { + interactor.openMediaApp() + interactor.collapseIsland() + } + .padding(SpaceXxl), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXxl), ) { - Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(36.dp)) - } + event.albumArt?.let { art -> + Image( + bitmap = art.toScaledBitmap(AlbumArtSize), + contentDescription = null, + modifier = Modifier.size(AlbumArtSize).clip(ShapeLg), + contentScale = ContentScale.Crop, + ) + } ?: Box( + modifier = Modifier + .size(AlbumArtSize) + .clip(ShapeLg) + .background(accent.copy(alpha = AlphaFaint)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(36.dp)) + } - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(SpaceXs), - ) { - Text( - event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, - color = OnCardText, - style = MaterialTheme.typography.titleMedium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - if (event.artist.isNotEmpty()) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(SpaceXs), + ) { Text( - event.artist, - color = accent, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, + event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, + color = OnCardText, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) + if (event.artist.isNotEmpty()) { + Text( + event.artist, + color = accent, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(OnCardText), + ) } } - event.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier.size(SizeIconSm).clip(ShapeXs), - colorFilter = ColorFilter.tint(OnCardText), - ) - } - } - Column( - modifier = Modifier - .fillMaxWidth() - .background(accent.copy(alpha = AlphaFaint)) - .padding(horizontal = SpaceXxl, vertical = SpaceLg), - verticalArrangement = Arrangement.spacedBy(SpaceLg), - ) { - if (event.duration > 0L) { - MediaSeekBar(event, interactor, accent, useWaveform) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SpaceXxl, vertical = SpaceLg), + verticalArrangement = Arrangement.spacedBy(SpaceLg), + ) { + if (event.duration > 0L) { + MediaSeekBar(event, interactor, accent, useWaveform) + } + MediaControls(event, interactor, accent) } - MediaControls(event, interactor, accent) } } } From ba38524d68973f3149bb0ba0013530de471ff452 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 17 Apr 2026 13:05:37 +0000 Subject: [PATCH 1130/1315] SystemUI: DynamicBar: Make music chip bg more vibrant Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/axdynamicbar/ui/compose/PillIslandContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index f5bd992ff06f..517755e2f942 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -208,7 +208,7 @@ private fun MediaPillIcon(event: IslandEvent.Media) { } ?: Box( modifier = - Modifier.size(16.dp).clip(CircleShape).background(OrangeAccent.copy(alpha = AlphaSubtle + 0.05f)), + Modifier.size(16.dp).clip(CircleShape).background(OrangeAccent.copy(alpha = 0.42f)), contentAlignment = Alignment.Center, ) { WaveformAnimation(OrangeAccent, Modifier.size(10.dp), isAnimating = event.isPlaying, barCount = 3) From 237de79f794de3b7915afc19f2eb3b8c6aefc6ea Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:01:25 +0800 Subject: [PATCH 1131/1315] SystemUI: DynamicBar: Cleaning up ux & animations enhance removing unncecessary events | rewiring to aosp status bar chip Change-Id: Ib1c2bbcd09c871ff14a2c18f485bee22874679a1 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/ids.xml | 1 + .../data/IslandEventRepository.kt | 63 +- .../data/source/AospChipIslandManager.kt | 45 + .../data/source/ConnectivityIslandManager.kt | 43 - .../data/source/MediaIslandManager.kt | 6 + .../data/source/NotificationIslandManager.kt | 361 +++----- .../data/source/PrivacyIslandManager.kt | 164 ---- .../data/source/ScreenRecordIslandManager.kt | 93 -- .../domain/AxDynamicBarChipsRefiner.kt | 7 + .../domain/AxDynamicBarInteractor.kt | 113 +-- .../domain/AxDynamicBarSettings.kt | 29 - .../axdynamicbar/model/IslandEvent.kt | 51 +- .../axdynamicbar/model/IslandUiState.kt | 4 +- .../axdynamicbar/shared/IslandActions.kt | 7 +- .../shared/IslandContentTokens.kt | 5 + .../axdynamicbar/shared/IslandEventMapper.kt | 38 +- .../shared/NotificationActionType.kt | 87 ++ .../ui/AxDynamicBarChipViewModel.kt | 83 +- .../ui/AxDynamicBarExpandedPanel.kt | 81 +- .../ui/AxDynamicBarKeyguardExpansion.kt | 105 +++ .../ui/AxDynamicBarStatusBarExpansion.kt | 75 ++ .../ui/compose/AxDynamicBarChip.kt | 27 +- .../ui/compose/AxDynamicBarKeyguardChip.kt | 98 +- .../ui/compose/AxDynamicBarNowBar.kt | 6 +- .../ui/compose/ExpandedIslandContent.kt | 6 +- .../ui/compose/ExpandedNotificationContent.kt | 185 +--- .../ui/compose/ExpandedOngoingContent.kt | 41 - .../ui/compose/ExpandedRecordingContent.kt | 198 +--- .../ui/compose/ExpandedSystemContent.kt | 8 +- .../ui/compose/KeyguardExpandedContent.kt | 794 ++++++---------- .../ui/compose/NotificationAlertCard.kt | 848 ------------------ .../ui/compose/PillIslandContent.kt | 498 +++++----- .../AxDynamicBarKeyguardChipSection.kt | 267 +++--- .../CommonVisualInterruptionSuppressors.kt | 10 - ...otificationInterruptStateProviderImpl.java | 11 +- .../VisualInterruptionDecisionProviderImpl.kt | 2 - 36 files changed, 1365 insertions(+), 3095 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AospChipIslandManager.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/NotificationActionType.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarKeyguardExpansion.kt create mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 02ac38b4147b..728379101562 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -230,6 +230,7 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt index c008b7a7ecc3..962425ecd650 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/IslandEventRepository.kt @@ -1,13 +1,12 @@ package com.android.systemui.axdynamicbar.data import android.util.Log +import com.android.systemui.axdynamicbar.data.source.AospChipIslandManager import com.android.systemui.axdynamicbar.data.source.AppTrackingIslandManager import com.android.systemui.axdynamicbar.data.source.BiometricIslandManager import com.android.systemui.axdynamicbar.data.source.ConnectivityIslandManager import com.android.systemui.axdynamicbar.data.source.MediaIslandManager import com.android.systemui.axdynamicbar.data.source.NotificationIslandManager -import com.android.systemui.axdynamicbar.data.source.PrivacyIslandManager -import com.android.systemui.axdynamicbar.data.source.ScreenRecordIslandManager import com.android.systemui.axdynamicbar.data.source.SmartspaceIslandManager import com.android.systemui.axdynamicbar.data.source.SystemIslandManager import com.android.systemui.axdynamicbar.data.source.TorchIslandManager @@ -26,8 +25,6 @@ import kotlinx.coroutines.flow.map class IslandEventRepository @Inject constructor( - val screenRecord: ScreenRecordIslandManager, - val privacy: PrivacyIslandManager, val media: MediaIslandManager, val connectivity: ConnectivityIslandManager, val system: SystemIslandManager, @@ -36,6 +33,7 @@ constructor( val torch: TorchIslandManager, val biometric: BiometricIslandManager, val smartspace: SmartspaceIslandManager, + val aospChip: AospChipIslandManager, private val settings: AxDynamicBarSettings, ) { companion object { @@ -69,16 +67,10 @@ constructor( if (listenersStarted) return listenersStarted = true Log.d(TAG, "Starting event listeners") - notification.onScreenRecordNotificationTime = { timeMs -> - screenRecord.updateNotificationStartTime(timeMs) - } syncDisabledTypes() - if (isTypeEnabled("screen_recording")) screenRecord.startListening() - if (isTypeEnabled("privacy")) privacy.startListening() if (isTypeEnabled("media")) media.startListening() if (isTypeEnabled("bluetooth")) connectivity.startBluetooth() if (isTypeEnabled("hotspot")) connectivity.startHotspot() - if (isTypeEnabled("casting")) connectivity.startCast() if (isTypeEnabled("vpn")) connectivity.startVpn() if (isTypeEnabled("charging")) system.startCharging() if (isTypeEnabled("ringer")) system.startRinger() @@ -94,8 +86,6 @@ constructor( if (!listenersStarted) return listenersStarted = false Log.d(TAG, "Stopping event listeners") - screenRecord.stopListening() - privacy.stopListening() media.stopListening() connectivity.stopListening() system.stopListening() @@ -110,10 +100,6 @@ constructor( if (!listenersStarted) return syncDisabledTypes() - if (isTypeEnabled("screen_recording")) screenRecord.startListening() - else screenRecord.stopListening() - if (isTypeEnabled("privacy")) privacy.startListening() - else privacy.stopListening() if (isTypeEnabled("media")) media.startListening() else media.stopListening() @@ -121,8 +107,6 @@ constructor( else connectivity.stopBluetooth() if (isTypeEnabled("hotspot")) connectivity.startHotspot() else connectivity.stopHotspot() - if (isTypeEnabled("casting")) connectivity.startCast() - else connectivity.stopCast() if (isTypeEnabled("vpn")) connectivity.startVpn() else connectivity.stopVpn() @@ -150,32 +134,6 @@ constructor( private fun buildEventsFlow(): Flow> { - val micCamFiltered = - combine(privacy.micCamEvent, notification.audioRecordingEvent) { micCam, audioRec -> - if (audioRec != null && micCam != null && micCam.isMic && !micCam.isCam) null - else micCam - } - - val castingFiltered = - combine( - connectivity.castingEvent, - screenRecord.screenRecordEvent, - ) { cast, rec -> - if (rec != null) null else cast - } - - val highGroupA = - combine( - screenRecord.screenRecordEvent, - micCamFiltered, - castingFiltered, - ) { rec, micCam, cast -> - listOfNotNull( - rec?.takeIf { isTypeEnabled("screen_recording") }, - micCam?.takeIf { isTypeEnabled("privacy") }, - cast?.takeIf { isTypeEnabled("casting") }, - ) - } val sportsGroup = combine( smartspace.sportsEvents, notification.sportsEvents, @@ -194,13 +152,12 @@ constructor( ) { promoted, sports -> (if (isTypeEnabled("promoted_ongoing")) promoted else emptyList()) + sports } - val highGroupB = - combine(highGroupA, torch.torchEvent) { events, t -> - events + listOfNotNull(t?.takeIf { isTypeEnabled("torch") }) - } val highGroup = - combine(highGroupB, biometric.biometricEvent) { events, bio -> - events + listOfNotNull(bio?.takeIf { isTypeEnabled("biometric_unlock") }) + combine(torch.torchEvent, biometric.biometricEvent) { t, bio -> + listOfNotNull( + t?.takeIf { isTypeEnabled("torch") }, + bio?.takeIf { isTypeEnabled("biometric_unlock") }, + ) } val midGroup = combine( @@ -256,8 +213,9 @@ constructor( transientGroup, promotedGroup, indicationGroup, - ) { high, transient, promoted, indication -> - high + transient + promoted + indication + aospChip.aospChipEvents, + ) { high, transient, promoted, indication, aosp -> + high + transient + promoted + indication + aosp } return allEvents.map { events -> @@ -269,4 +227,3 @@ constructor( } } } - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AospChipIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AospChipIslandManager.kt new file mode 100644 index 000000000000..74730cef3596 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AospChipIslandManager.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.data.source + +import com.android.systemui.axdynamicbar.domain.AxDynamicBarChipsRefiner +import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@SysUISingleton +class AospChipIslandManager @Inject constructor( + private val refiner: AxDynamicBarChipsRefiner, +) { + val aospChipEvents: Flow> = + refiner.chipsFlow.map { model -> + model.active + .filter { isAbsorbed(it) } + .map { IslandEvent.AospChip(active = it) } + } + + private fun isAbsorbed(chip: OngoingActivityChipModel.Active): Boolean { + val key = chip.key + return key.startsWith("callChip-") || + key == "ShareToApp" || + key == "ScreenRecord" || + key == "CastToOtherDevice" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt index fa8abe7bff52..5889f4d9cbc0 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ConnectivityIslandManager.kt @@ -5,8 +5,6 @@ import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.MediaRouterChipInteractor -import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel import com.android.systemui.statusbar.policy.BluetoothController import com.android.systemui.statusbar.policy.HotspotController import com.android.systemui.statusbar.policy.vpn.domain.interactor.VpnInteractor @@ -27,7 +25,6 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val bluetoothController: BluetoothController, private val hotspotController: HotspotController, - private val mediaRouterChipInteractor: MediaRouterChipInteractor, private val vpnInteractor: VpnInteractor, ) { companion object { @@ -40,9 +37,6 @@ constructor( private val _hotspotEvent = MutableStateFlow(null) val hotspotEvent: StateFlow = _hotspotEvent.asStateFlow() - private val _castingEvent = MutableStateFlow(null) - val castingEvent: StateFlow = _castingEvent.asStateFlow() - private val _vpnEvent = MutableStateFlow(null) val vpnEvent: StateFlow = _vpnEvent.asStateFlow() @@ -50,7 +44,6 @@ constructor( private var listening = false private var wasVpnEnabled = false private var vpnJob: Job? = null - private var castJob: Job? = null private val bluetoothCallback = object : BluetoothController.Callback { @@ -106,21 +99,6 @@ constructor( } } - private fun startCastListener() { - castJob?.cancel() - castJob = - applicationScope.launch(backgroundDispatcher) { - mediaRouterChipInteractor.mediaRouterCastingState.collect { state -> - _castingEvent.value = - when (state) { - is MediaRouterCastModel.Casting -> - IslandEvent.Casting(deviceName = state.deviceName ?: "Screen") - is MediaRouterCastModel.DoingNothing -> null - } - } - } - } - private fun startVpnListener() { vpnJob?.cancel() vpnJob = @@ -150,7 +128,6 @@ constructor( private var btListening = false private var hotspotListening = false - private var castListening = false private var vpnListening = false fun startBluetooth() { @@ -185,20 +162,6 @@ constructor( _hotspotEvent.value = null } - fun startCast() { - if (castListening) return - castListening = true - startCastListener() - } - - fun stopCast() { - if (!castListening) return - castListening = false - castJob?.cancel() - castJob = null - _castingEvent.value = null - } - fun startVpn() { if (vpnListening) return vpnListening = true @@ -219,7 +182,6 @@ constructor( listening = true startBluetooth() startHotspot() - startCast() startVpn() } @@ -228,7 +190,6 @@ constructor( listening = false stopBluetooth() stopHotspot() - stopCast() stopVpn() } @@ -250,10 +211,6 @@ constructor( _hotspotEvent.value = null } - fun clearCasting() { - _castingEvent.value = null - } - fun clearVpn() { _vpnEvent.value = null } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt index 820ce587fbc5..caceb1fb8c93 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/MediaIslandManager.kt @@ -102,6 +102,12 @@ constructor( val current = _mediaEvent.value ?: return _mediaEvent.value = current.copy(appIcon = drawable) } + + override fun onMetadataChanged(track: String, artist: String) { + val current = _mediaEvent.value ?: return + if (current.track == track && current.artist == artist) return + _mediaEvent.value = current.copy(track = track, artist = artist) + } } private fun bindController(controllers: List?) { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt index 18cf3f324186..bf4f9441e2e1 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/NotificationIslandManager.kt @@ -3,10 +3,7 @@ package com.android.systemui.axdynamicbar.data.source import android.app.Notification import android.content.Context import com.android.systemui.res.R -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import android.graphics.drawable.Icon import android.os.Bundle import android.os.Parcelable import android.os.SystemClock @@ -41,7 +38,6 @@ constructor( companion object { private const val TAG = "NotificationIslandManager" - private const val SCREEN_RECORD_PACKAGE = "com.android.systemui" private val CLOCK_PACKAGES = setOf("com.google.android.deskclock", "com.android.deskclock") private val ALARM_PACKAGES = setOf("com.google.android.deskclock", "com.android.deskclock") @@ -133,7 +129,6 @@ constructor( var onTimerEvent: ((IslandEvent.Timer) -> Unit)? = null var onAlarmEvent: ((IslandEvent.Alarm) -> Unit)? = null var onNotificationPosted: ((IslandEvent.Notification) -> Unit)? = null - var onScreenRecordNotificationTime: ((Long) -> Unit)? = null @Volatile private var listening = false @Volatile private var timerJob: Job? = null @@ -191,37 +186,46 @@ constructor( val pkg = sbn.packageName ?: return val extras = sbn.notification?.extras ?: return - if ( - pkg == SCREEN_RECORD_PACKAGE && - sbn.isOngoing && - extras.getBoolean("android.showChronometer", false) && - sbn.notification.`when` > 0L - ) { - onScreenRecordNotificationTime?.invoke(sbn.notification.`when`) - } - if (pkg in CLOCK_PACKAGES) { val channelId = sbn.notification?.channelId?.lowercase() ?: "" val actionLabels = sbn.notification?.actions?.map { it.title?.toString()?.lowercase() ?: "" } ?: emptyList() + val actionIntents = + sbn.notification?.actions?.map { actionIntentString(it) ?: "" } + ?: emptyList() + val hasStopwatchIntent = actionIntents.any { + it == DeskClockActions.START_STOPWATCH || + it == DeskClockActions.PAUSE_STOPWATCH || + it == DeskClockActions.RESET_STOPWATCH || + it == DeskClockActions.LAP_STOPWATCH || + it == DeskClockActions.SHOW_STOPWATCH + } + val hasTimerIntent = actionIntents.any { + it == DeskClockActions.START_TIMER || + it == DeskClockActions.PAUSE_TIMER || + it == DeskClockActions.RESET_TIMER || + it == DeskClockActions.ADD_MINUTE_TIMER || + it == DeskClockActions.SHOW_TIMER + } val hasLap = actionLabels.any { it.contains("lap") } val isCountDown = extras.getBoolean("android.chronometerCountDown", false) - val isStopwatch = channelId.contains("stopwatch") || hasLap - val isTimer = - !isStopwatch && - (channelId.contains("timer") || - channelId.contains("firing") || - isCountDown || - actionLabels.any { it.contains("+1") || it.contains("add") }) + val isStopwatch = hasStopwatchIntent || channelId.contains("stopwatch") || hasLap + val isTimer = !isStopwatch && ( + hasTimerIntent || + channelId.contains("timer") || + channelId.contains("firing") || + isCountDown || + actionLabels.any { it.contains("+1") || it.contains("add") } + ) if (isStopwatch && "stopwatch" !in disabledTypes) { - handleStopwatch(sbn, extras, actionLabels) + handleStopwatch(sbn, extras, actionLabels, actionIntents) return } if (isTimer && "timer" !in disabledTypes) { - handleTimer(sbn, extras, actionLabels) + handleTimer(sbn, extras, actionLabels, actionIntents) return } if (isStopwatch || isTimer) return @@ -234,14 +238,7 @@ constructor( } if (sbn.notification?.category == Notification.CATEGORY_CALL) { - val isCallStyle = - extras.containsKey(Notification.EXTRA_ANSWER_INTENT) || - extras.containsKey(Notification.EXTRA_DECLINE_INTENT) || - extras.containsKey(Notification.EXTRA_HANG_UP_INTENT) - if (isCallStyle) { - handleCallNotification(sbn, extras) - return - } + return } val allActions = sbn.notification?.actions ?: emptyArray() @@ -250,20 +247,38 @@ constructor( Notification.EXTRA_MEDIA_SESSION ) == true if (sbn.isOngoing && !isMedia && "audio_recording" !in disabledTypes) { - val recActions = - allActions.filter { a -> - val lbl = a.title?.toString()?.lowercase() ?: "" - lbl.contains("stop") || lbl.contains("pause") || lbl.contains("resume") + val actionIntentMap = allActions.associateWith { actionIntentString(it) } + val iconResMap = allActions.associateWith { actionIconResName(it, pkg)?.lowercase() } + val recActions = allActions.filter { a -> + val intent = actionIntentMap[a] + val icon = iconResMap[a] ?: "" + when { + intent == "STOP" || intent == "PAUSE" || intent == "RESUME" -> true + icon.matchesMaterial(MaterialIconSet.Stop) -> true + icon.matchesMaterial(MaterialIconSet.Pause) -> true + icon.matchesMaterial(MaterialIconSet.Play) -> true + else -> { + val lbl = a.title?.toString()?.lowercase() ?: "" + lbl.contains("stop") || lbl.contains("pause") || lbl.contains("resume") + } } - val hasStop = - recActions.any { a -> + } + val hasStop = recActions.any { a -> + actionIntentMap[a] == "STOP" || + (iconResMap[a] ?: "").matchesMaterial(MaterialIconSet.Stop) || (a.title?.toString()?.lowercase() ?: "").contains("stop") - } + } if (hasStop && recActions.size >= 2) { - val isPaused = - recActions.any { a -> - (a.title?.toString()?.lowercase() ?: "").contains("resume") + val hasResumeAction = recActions.any { actionIntentMap[it] == "RESUME" } + val hasPauseAction = recActions.any { actionIntentMap[it] == "PAUSE" } + val isPaused = when { + hasResumeAction -> true + hasPauseAction -> false + else -> recActions.any { a -> + (iconResMap[a] ?: "").matchesMaterial(MaterialIconSet.Play) || + (a.title?.toString()?.lowercase() ?: "").contains("resume") } + } val notifActions = recActions.mapNotNull { a -> a.title?.let { @@ -272,26 +287,21 @@ constructor( } val appName = resolveAppName(pkg) val existing = _audioRecordingEvent.value - val prevState = existing?.state + val now = System.currentTimeMillis() + val parsedElapsedMs = parseRecorderElapsedMs(extras) - val startTime = - if ( - sbn.notification?.extras?.getBoolean("android.showChronometer") == - true - ) + val startTime = when { + parsedElapsedMs != null -> now - parsedElapsedMs + existing != null && existing.state != RecordingState.SAVED && + recorderNotifKey == sbn.key -> existing.startTimeMs + sbn.notification?.extras?.getBoolean("android.showChronometer") == true -> sbn.notification.`when` - else existing?.startTimeMs ?: System.currentTimeMillis() - - val now = System.currentTimeMillis() - if (isPaused && prevState != RecordingState.PAUSED) { - pauseStartMs = now - } else if ( - !isPaused && prevState == RecordingState.PAUSED - ) { - accumulatedPauseMs += (now - pauseStartMs).coerceAtLeast(0L) - pauseStartMs = 0L + else -> now } + accumulatedPauseMs = 0L + pauseStartMs = 0L + recorderPackage = pkg recorderNotifKey = sbn.key _audioRecordingEvent.value = @@ -302,7 +312,7 @@ constructor( else RecordingState.RECORDING, startTimeMs = startTime, actions = notifActions, - pausedDurationMs = accumulatedPauseMs, + pausedDurationMs = 0L, ) return } @@ -434,105 +444,18 @@ constructor( a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } } - val replyAction = - allNotifActions - .firstOrNull { a -> - !a.remoteInputs.isNullOrEmpty() && a.actionIntent != null - } - ?.let { a -> - val remoteInput = a.remoteInputs!!.first() - IslandEvent.ReplyAction( - label = a.title ?: remoteInput.label ?: "Reply", - action = a, - remoteInput = remoteInput, - ) - } - - var senderIcon: Drawable? = null - var senderName: String? = null - var latestMessageText: String? = null - var isConversation = false - var isGroupConversation = false - var conversationTitle: String? = null - if ( - notif != null && - isMessagingStyle && - notif.extras != null - ) { - isConversation = true - isGroupConversation = notif.extras.getBoolean( - Notification.EXTRA_IS_GROUP_CONVERSATION, false - ) - conversationTitle = notif.extras.getCharSequence( - Notification.EXTRA_CONVERSATION_TITLE - )?.toString()?.takeIf { it.isNotEmpty() } - val messagesArray = - notif.extras.getParcelableArray( - Notification.EXTRA_MESSAGES, - Parcelable::class.java, - ) - if (messagesArray != null && messagesArray.isNotEmpty()) { - val messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - messagesArray - ) - val lastMessage = messages.maxByOrNull { it.timestamp } - val sender = lastMessage?.senderPerson - senderName = sender?.name?.toString() - latestMessageText = lastMessage?.text?.toString() - senderIcon = - try { - sender?.icon?.loadDrawable(context) - } catch (_: Exception) { - null - } - } - - if (senderIcon == null) { - senderIcon = - try { - notif.extras - .getParcelable( - Notification.EXTRA_CONVERSATION_ICON, - Icon::class.java, - ) - ?.loadDrawable(context) - } catch (_: Exception) { - null - } - } - - if (senderIcon == null) { - senderIcon = - try { - notif.getLargeIcon()?.loadDrawable(context) - } catch (_: Exception) { - null - } - } - } - - val notificationImage = extractNotificationImage(extras, sbn) - val event = IslandEvent.Notification( sbn = sbn, title = title, - text = latestMessageText ?: text, + text = text, appIcon = icon, appName = appName, progress = if (hasProgress && !indeterminate) progressRaw else -1, progressMax = progressMax.coerceAtLeast(1), isProgressIndeterminate = indeterminate, actions = actions, - replyAction = replyAction, - isConversation = isConversation, - isGroupConversation = isGroupConversation, - conversationTitle = conversationTitle, - senderIcon = senderIcon, - senderName = senderName, groupKey = groupKey, - notificationImage = notificationImage, ) applicationScope.launch { notificationFlow.emit(event) } onNotificationPosted?.invoke(event) @@ -603,6 +526,7 @@ constructor( sbn: StatusBarNotification, extras: Bundle, actionLabels: List = emptyList(), + actionIntents: List = emptyList(), ) { val label = extras.getString("android.title") ?: context.getString(R.string.ax_dynamic_bar_timer) val icon = @@ -612,12 +536,20 @@ constructor( null } - val hasPauseAction = actionLabels.any { it.contains("pause") } - val hasResumeAction = actionLabels.any { - it.contains("resume") || it.contains("play") || - (it.contains("start") && !it.contains("stop")) + val hasPauseIntent = actionIntents.any { it == DeskClockActions.PAUSE_TIMER } + val hasStartIntent = actionIntents.any { it == DeskClockActions.START_TIMER } + val isPaused = when { + hasStartIntent -> true + hasPauseIntent -> false + else -> { + val hasPauseLabel = actionLabels.any { it.contains("pause") } + val hasResumeLabel = actionLabels.any { + it.contains("resume") || it.contains("play") || + (it.contains("start") && !it.contains("stop")) + } + hasResumeLabel || (actionLabels.isNotEmpty() && !hasPauseLabel) + } } - val isPaused = hasResumeAction || (actionLabels.isNotEmpty() && !hasPauseAction) var endTimeMs = sbn.notification.`when` if (endTimeMs <= System.currentTimeMillis()) { @@ -670,6 +602,7 @@ constructor( sbn: StatusBarNotification, extras: Bundle, actionLabels: List = emptyList(), + actionIntents: List = emptyList(), ) { val label = extras.getString("android.title") ?: "" val icon = @@ -679,7 +612,14 @@ constructor( null } - val isRunning = actionLabels.any { it.contains("pause") || it.contains("lap") } + val hasPauseIntent = actionIntents.any { it == DeskClockActions.PAUSE_STOPWATCH } + val hasStartIntent = actionIntents.any { it == DeskClockActions.START_STOPWATCH } + val hasLapIntent = actionIntents.any { it == DeskClockActions.LAP_STOPWATCH } + val isRunning = when { + hasPauseIntent || hasLapIntent -> true + hasStartIntent -> false + else -> actionLabels.any { it.contains("pause") || it.contains("lap") } + } var startTimeMs = System.currentTimeMillis() val chronoBase = extractChronometerBase(sbn) @@ -766,60 +706,61 @@ constructor( "" } - private fun handleCallNotification(sbn: StatusBarNotification, extras: Bundle) { - val callerName = extras.getString("android.title") - val number = extras.getString("android.text") - val callerPhoto = - try { - sbn.notification?.getLargeIcon()?.loadDrawable(context) - } catch (_: Exception) { - null - } - val allActions = sbn.notification?.actions ?: emptyArray() - val actions = - allActions - .filter { a -> a.remoteInputs.isNullOrEmpty() && a.actionIntent != null } - .take(3) - .mapNotNull { a -> - a.title?.let { IslandEvent.NotificationAction(label = it, action = a) } - } + private fun actionIconResName(action: Notification.Action, pkg: String): String? { + val icon = action.getIcon() ?: return null + val resId = try { icon.resId } catch (_: Exception) { 0 } + if (resId == 0) return null + return try { + context.packageManager.getResourcesForApplication(pkg).getResourceEntryName(resId) + } catch (_: Exception) { + null + } + } - val androidCallType = extras.getInt( - Notification.EXTRA_CALL_TYPE, - Notification.CallStyle.CALL_TYPE_UNKNOWN, - ) - val callType = - if (androidCallType == Notification.CallStyle.CALL_TYPE_INCOMING) - "Phone:incoming" - else "Phone:active" + private fun actionIntentString(action: Notification.Action): String? { + val pi = action.actionIntent ?: return null + return try { + pi.intent?.action + } catch (_: Exception) { + null + } + } - val icon = - try { - context.packageManager.getApplicationIcon(sbn.packageName) - } catch (_: Exception) { - null - } + private fun parseRecorderElapsedMs(extras: Bundle): Long? { + val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: return null + val match = Regex("""(\d+):(\d+)(?::(\d+))?""").find(text) ?: return null + val a = match.groupValues[1].toLongOrNull() ?: return null + val b = match.groupValues[2].toLongOrNull() ?: return null + val c = match.groupValues[3].toLongOrNull() + val secs = if (c != null) a * 3600 + b * 60 + c else a * 60 + b + return secs * 1000L + } - val callWhen = sbn.notification?.`when` ?: 0L - val callStart = if (callWhen > 0L) callWhen else System.currentTimeMillis() + private enum class MaterialIconSet(val patterns: List) { + Play(listOf("play_arrow", "media_play", "play_circle", "play")), + Pause(listOf("pause_circle", "media_pause", "pause")), + Stop(listOf("stop_circle", "media_stop", "stop")), + } - val event = - IslandEvent.Notification( - sbn = sbn, - title = callerName, - text = number, - appIcon = icon, - appName = callType, - actions = actions, - senderIcon = callerPhoto, - senderName = callerName, - isConversation = false, - callStartTimeMs = callStart, - ) - applicationScope.launch { notificationFlow.emit(event) } - onNotificationPosted?.invoke(event) + private object DeskClockActions { + private const val PREFIX = "com.android.deskclock.action." + const val START_TIMER = PREFIX + "START_TIMER" + const val PAUSE_TIMER = PREFIX + "PAUSE_TIMER" + const val RESET_TIMER = PREFIX + "RESET_TIMER" + const val ADD_MINUTE_TIMER = PREFIX + "ADD_MINUTE_TIMER" + const val SHOW_TIMER = PREFIX + "SHOW_TIMER" + const val START_STOPWATCH = PREFIX + "START_STOPWATCH" + const val PAUSE_STOPWATCH = PREFIX + "PAUSE_STOPWATCH" + const val RESET_STOPWATCH = PREFIX + "RESET_STOPWATCH" + const val LAP_STOPWATCH = PREFIX + "LAP_STOPWATCH" + const val SHOW_STOPWATCH = PREFIX + "SHOW_STOPWATCH" + const val ALARM_SNOOZE = "com.android.deskclock.ALARM_SNOOZE" + const val ALARM_DISMISS = "com.android.deskclock.ALARM_DISMISS" } + private fun String.matchesMaterial(set: MaterialIconSet): Boolean = + set.patterns.any { this.contains(it) } + private fun isPromotable(sbn: StatusBarNotification, extras: Bundle): Boolean { val notification = sbn.notification ?: return false @@ -1019,26 +960,6 @@ constructor( return true } - private fun extractNotificationImage(extras: Bundle, sbn: StatusBarNotification): Drawable? { - try { - extras.getParcelable(Notification.EXTRA_PICTURE_ICON, Icon::class.java) - ?.loadDrawable(context)?.let { return it } - } catch (_: Exception) {} - - try { - extras.getParcelable(Notification.EXTRA_PICTURE, Bitmap::class.java) - ?.let { return BitmapDrawable(context.resources, it) } - } catch (_: Exception) {} - - if (!sbn.notification.isStyle(Notification.MessagingStyle::class.java)) { - try { - sbn.notification?.getLargeIcon()?.loadDrawable(context)?.let { return it } - } catch (_: Exception) {} - } - - return null - } - private fun loadNotificationIcon(sbn: StatusBarNotification, pkg: String): Drawable? { return try { sbn.notification?.smallIcon?.loadDrawable(context) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt deleted file mode 100644 index 6f7d45da47ad..000000000000 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/PrivacyIslandManager.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.android.systemui.axdynamicbar.data.source - -import android.app.AppOpsManager -import android.content.Context -import com.android.systemui.axdynamicbar.model.IslandEvent -import com.android.systemui.axdynamicbar.model.MicCamApp -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.privacy.PrivacyItem -import com.android.systemui.privacy.PrivacyItemController -import com.android.systemui.privacy.PrivacyType -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -@SysUISingleton -class PrivacyIslandManager -@Inject -constructor( - @Application private val context: Context, - private val privacyItemController: PrivacyItemController, -) { - private val _micCamEvent = MutableStateFlow(null) - val micCamEvent: StateFlow = _micCamEvent.asStateFlow() - - var onCamStarted: ((IslandEvent.MicCamActive) -> Unit)? = null - - private var listening = false - @Volatile private var wasCamActive = false - - @Volatile private var privacyControllerReportedCam = false - - private val appOpsManager: AppOpsManager by lazy { - context.getSystemService(AppOpsManager::class.java) - } - - private val callback = - object : PrivacyItemController.Callback { - override fun onPrivacyItemsChanged(privacyItems: List) { - val isMic = privacyItems.any { it.privacyType == PrivacyType.TYPE_MICROPHONE } - val isCam = privacyItems.any { it.privacyType == PrivacyType.TYPE_CAMERA } - privacyControllerReportedCam = isCam - - if (isMic || isCam) { - val apps: List = - privacyItems - .filter { - it.privacyType == PrivacyType.TYPE_MICROPHONE || - it.privacyType == PrivacyType.TYPE_CAMERA - } - .mapNotNull { item -> - val pkg = item.application.packageName ?: return@mapNotNull null - val name = - try { - context.packageManager - .getApplicationLabel( - context.packageManager.getApplicationInfo(pkg, 0) - ) - .toString() - } catch (_: Exception) { - pkg - } - val icon = - try { - context.packageManager.getApplicationIcon(pkg) - } catch (_: Exception) { - null - } - MicCamApp(packageName = pkg, appName = name, appIcon = icon) - } - .distinctBy { it.packageName } - - emitEvent(isMic = isMic, isCam = isCam, apps = apps) - } else if (!directCamActive) { - wasCamActive = false - _micCamEvent.value = null - } - } - - override fun onFlagAllChanged(flag: Boolean) {} - } - - @Volatile private var directCamActive = false - - private val cameraOpCallback = - AppOpsManager.OnOpActiveChangedListener { op, _, packageName, active -> - if (op != AppOpsManager.OPSTR_CAMERA) return@OnOpActiveChangedListener - directCamActive = active - - if (privacyControllerReportedCam) return@OnOpActiveChangedListener - - if (active) { - val appName = - try { - context.packageManager - .getApplicationLabel( - context.packageManager.getApplicationInfo(packageName, 0) - ) - .toString() - } catch (_: Exception) { - packageName - } - val icon = - try { - context.packageManager.getApplicationIcon(packageName) - } catch (_: Exception) { - null - } - val current = _micCamEvent.value - emitEvent( - isMic = current?.isMic ?: false, - isCam = true, - apps = - listOfNotNull(MicCamApp(packageName, appName, icon)) + - (current?.apps?.filter { it.packageName != packageName } ?: emptyList()), - ) - } else { - val current = _micCamEvent.value ?: return@OnOpActiveChangedListener - if (current.isMic) { - _micCamEvent.value = current.copy(isCam = false) - } else { - wasCamActive = false - _micCamEvent.value = null - } - } - } - - private fun emitEvent(isMic: Boolean, isCam: Boolean, apps: List) { - val appName = apps.firstOrNull()?.appName ?: "" - val event = - IslandEvent.MicCamActive(isMic = isMic, isCam = isCam, appName = appName, apps = apps) - _micCamEvent.value = event - - if (isCam && !wasCamActive) onCamStarted?.invoke(event) - wasCamActive = isCam - } - - fun startListening() { - if (listening) return - listening = true - privacyItemController.addCallback(callback) - try { - appOpsManager.startWatchingActive( - arrayOf(AppOpsManager.OPSTR_CAMERA), - context.mainExecutor, - cameraOpCallback, - ) - } catch (_: Exception) {} - } - - fun stopListening() { - if (!listening) return - listening = false - privacyItemController.removeCallback(callback) - try { - appOpsManager.stopWatchingActive(cameraOpCallback) - } catch (_: Exception) {} - _micCamEvent.value = null - directCamActive = false - privacyControllerReportedCam = false - } -} - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt deleted file mode 100644 index 99a26421f2cb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/ScreenRecordIslandManager.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.android.systemui.axdynamicbar.data.source - -import com.android.systemui.axdynamicbar.model.IslandEvent -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds -import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor -import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -@SysUISingleton -class ScreenRecordIslandManager -@Inject -constructor( - @Application private val applicationScope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val screenRecordChipInteractor: ScreenRecordChipInteractor, -) { - private val _screenRecordEvent = MutableStateFlow(null) - val screenRecordEvent: StateFlow = - _screenRecordEvent.asStateFlow() - - @Volatile - private var notificationStartTimeMs: Long = 0L - - private var listening = false - private var listenerJob: Job? = null - - fun startListening() { - if (listening) return - listening = true - listenerJob?.cancel() - listenerJob = - applicationScope.launch(backgroundDispatcher) { - screenRecordChipInteractor.screenRecordState.collect { state -> - _screenRecordEvent.value = - when (state) { - is ScreenRecordChipModel.Recording -> { - val notifMs = notificationStartTimeMs - val existing = _screenRecordEvent.value - val startMs = when { - notifMs > 0L -> notifMs - existing != null && !existing.isCountdown -> existing.startTimeMs - else -> System.currentTimeMillis() - } - if (existing != null && !existing.isCountdown && existing.startTimeMs == startMs) { - existing - } else { - IslandEvent.ScreenRecording(startTimeMs = startMs) - } - } - is ScreenRecordChipModel.Starting -> - IslandEvent.ScreenRecording( - countdownSeconds = - state.millisUntilStarted.toCountdownSeconds(), - ) - is ScreenRecordChipModel.DoingNothing -> null - } - } - } - } - - fun stopListening() { - if (!listening) return - listening = false - listenerJob?.cancel() - listenerJob = null - _screenRecordEvent.value = null - notificationStartTimeMs = 0L - } - - fun stopRecording() { - applicationScope.launch { screenRecordChipInteractor.stopRecording() } - } - - fun updateNotificationStartTime(timeMs: Long) { - notificationStartTimeMs = timeMs - _screenRecordEvent.update { existing -> - if (existing != null && timeMs > 0L && existing.startTimeMs != timeMs) { - existing.copy(startTimeMs = timeMs) - } else existing - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt index 647d09fc5f2d..c8d1bbd2e152 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarChipsRefiner.kt @@ -20,13 +20,20 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow @SysUISingleton class AxDynamicBarChipsRefiner @Inject constructor( private val settings: AxDynamicBarSettings, ) : OngoingActivityChipsRefiner { + private val _chipsFlow = MutableStateFlow(MultipleOngoingActivityChipsModel()) + val chipsFlow: StateFlow = _chipsFlow.asStateFlow() + override fun transform(input: MultipleOngoingActivityChipsModel): MultipleOngoingActivityChipsModel { + _chipsFlow.value = input if (!settings.isEnabled.value) return input return input.copy( diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt index 7756b1b3d537..1ebab0a5efc5 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarInteractor.kt @@ -1,9 +1,6 @@ package com.android.systemui.axdynamicbar.domain -import android.app.Notification -import android.media.AudioManager import android.net.Uri -import android.provider.Settings.Global import com.android.systemui.axdynamicbar.data.IslandEventRepository import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.model.IslandState @@ -20,8 +17,6 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.statusbar.policy.ZenModeController -import com.android.systemui.util.settings.GlobalSettings import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -33,7 +28,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -50,15 +44,11 @@ constructor( private val indicationController: KeyguardIndicationController, private val shadeInteractor: ShadeInteractor, private val shadeRepository: ShadeRepository, - private val zenModeController: ZenModeController, - private val globalSettings: GlobalSettings, - private val audioManager: AudioManager, ) : IslandActions { private val _uiState = MutableStateFlow(IslandUiState()) val uiState: StateFlow = _uiState.asStateFlow() private val autoDismissJobs = ConcurrentHashMap() - @Volatile private var notifAlertJob: Job? = null private val dismissedEventIds: MutableSet = ConcurrentHashMap.newKeySet() @@ -95,14 +85,6 @@ constructor( companion object { private const val TAG = "AxDynamicBarInteractor" - private const val NOTIF_ALERT_DURATION_MS = 4500L - - private fun IslandEvent.Notification.isActiveCall(): Boolean { - val extras = sbn.notification?.extras ?: return false - return extras.containsKey(Notification.EXTRA_ANSWER_INTENT) || - extras.containsKey(Notification.EXTRA_DECLINE_INTENT) || - extras.containsKey(Notification.EXTRA_HANG_UP_INTENT) - } } fun init() { @@ -115,8 +97,6 @@ constructor( repository.system.onRingerChanged = { scheduleAutoDismiss(it) } repository.system.onClipboardCopied = { scheduleAutoDismiss(it) } - repository.privacy.onCamStarted = { scheduleAutoDismiss(it, 10_000L) } - repository.notification.activeMediaPackageProvider = { repository.media.activeMediaPackage } repository.notification.onAlarmEvent = { scheduleAutoDismiss(it, if (it.isRinging) 30_000L else 5_000L) @@ -137,14 +117,6 @@ constructor( applicationScope.launch { repository.notification.notificationFlow.collect { notification -> repository.notification.coalesceNotification(notification) - showNotificationAlert(notification) - } - } - - applicationScope.launch { - repository.notification.notificationRemovedFlow.collect { key -> - val alert = _uiState.value.notificationAlert ?: return@collect - if (alert.sbn.key == key) dismissNotificationAlert() } } @@ -241,12 +213,6 @@ constructor( } } - applicationScope.launch { - settings.isHeadsUpEnabled.collect { enabled -> - if (!enabled) dismissNotificationAlert() - } - } - applicationScope.launch { combine( repository.events, @@ -334,7 +300,6 @@ constructor( pinnedEventIndex = pinnedIndex, manuallyHidden = if (shouldReset) false else current.manuallyHidden, forceVisible = false, - notificationAlert = current.notificationAlert, ) } } @@ -381,10 +346,7 @@ constructor( ) when (event) { - is IslandEvent.ScreenRecording -> repository.screenRecord.stopListening() - is IslandEvent.MicCamActive -> {} is IslandEvent.AudioRecording -> repository.notification.clearAudioRecording() - is IslandEvent.Casting -> repository.connectivity.clearCasting() is IslandEvent.Sports -> { repository.smartspace.clearSportsEvent(event.key) repository.notification.clearSportsEvent(event.key) @@ -410,6 +372,7 @@ constructor( } is IslandEvent.BiometricUnlock -> repository.biometric.clear() is IslandEvent.KeyguardIndication -> repository.clearIndicationEvent(event.indicationType) + is IslandEvent.AospChip -> {} } } @@ -429,79 +392,6 @@ constructor( scheduleAutoDismiss(event) } - override fun onNotificationAlertInteractionStart() { - notifAlertJob?.cancel() - notifAlertJob = null - } - - override fun onNotificationAlertInteractionEnd() { - val current = _uiState.value - val alert = current.notificationAlert ?: return - if (alert.isActiveCall()) return - notifAlertJob = applicationScope.launch { - delay(NOTIF_ALERT_DURATION_MS) - dismissNotificationAlert() - } - } - - fun dismissNotificationAlert() { - notifAlertJob?.cancel() - notifAlertJob = null - val current = _uiState.value - if (current.notificationAlert != null) { - _uiState.value = current.copy(notificationAlert = null) - } - } - - private fun shouldSuppressForDndOrRinger(notification: IslandEvent.Notification): Boolean { - if (notification.isActiveCall()) return false - if (!settings.isHeadsUpEnabled.value) return true - val category = notification.sbn.notification?.category - if (category == Notification.CATEGORY_CALL || category == Notification.CATEGORY_ALARM) return false - val zenMode = zenModeController.zen - if (zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS || - zenMode == Global.ZEN_MODE_ALARMS) return true - val ringerMode = audioManager.ringerMode - return ringerMode == AudioManager.RINGER_MODE_SILENT - } - - private fun showNotificationAlert( - notification: IslandEvent.Notification, - ) { - if (panelBlocking || statusBlocking || _isOnKeyguard.value) return - if (shouldSuppressForDndOrRinger(notification)) return - val current = _uiState.value - val existingAlert = current.notificationAlert - if (existingAlert != null && - existingAlert.isActiveCall() && - !notification.isActiveCall() - ) return - - val hasProgress = notification.progress >= 0 || notification.isProgressIndeterminate - val isSameKey = existingAlert != null && existingAlert.sbn.key == notification.sbn.key - - if (isSameKey && hasProgress) { - _uiState.value = current.copy(notificationAlert = notification) - return - } - - notifAlertJob?.cancel() - _uiState.value = current.copy(notificationAlert = notification) - - if (hasProgress) return - - val duration = - if (!notification.isActiveCall()) NOTIF_ALERT_DURATION_MS else null - if (duration != null) { - notifAlertJob = applicationScope.launch { - delay(duration) - dismissNotificationAlert() - } - } - } - - override fun stopScreenRecording() = repository.screenRecord.stopRecording() - override fun togglePlayPause() = repository.media.togglePlayPause() override fun skipNext() = repository.media.skipNext() @@ -544,7 +434,6 @@ constructor( fun onPanelExpandedChanged(expanded: Boolean) { panelBlocking = expanded _isPanelExpanded.value = expanded - if (expanded) dismissNotificationAlert() updateChipVisibility() } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt index 1855820b5f73..5fd4a092f189 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/domain/AxDynamicBarSettings.kt @@ -4,12 +4,10 @@ import android.database.ContentObserver import android.os.Handler import android.os.UserHandle import android.provider.Settings -import android.provider.Settings.Global import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.shared.EVENT_TYPE_IDS import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -21,7 +19,6 @@ import org.json.JSONArray class AxDynamicBarSettings @Inject constructor( @Main private val mainHandler: Handler, private val secureSettings: SecureSettings, - private val globalSettings: GlobalSettings, private val context: android.content.Context, ) { companion object { @@ -30,7 +27,6 @@ class AxDynamicBarSettings @Inject constructor( const val KEY_KEYGUARD_ENABLED = "ax_dynamic_bar_keyguard_enabled" const val KEY_KEYGUARD_MUSIC_PILL_ENABLED = "ax_dynamic_bar_keyguard_music_pill" const val KEY_KEYGUARD_BATTERY_CHIP_MODE = "ax_dynamic_bar_keyguard_battery_chip_mode" - const val KEY_COMPACT_NOTIFICATIONS = "ax_dynamic_bar_compact_notifications" } private val contentResolver = context.contentResolver @@ -47,12 +43,6 @@ class AxDynamicBarSettings @Inject constructor( private val _keyguardBatteryChipMode = MutableStateFlow(1) val keyguardBatteryChipMode: StateFlow = _keyguardBatteryChipMode.asStateFlow() - private val _compactNotifications = MutableStateFlow(true) - val compactNotifications: StateFlow = _compactNotifications.asStateFlow() - - private val _isHeadsUpEnabled = MutableStateFlow(true) - val isHeadsUpEnabled: StateFlow = _isHeadsUpEnabled.asStateFlow() - private val _useWaveformSeekBar = MutableStateFlow(false) val useWaveformSeekBar: StateFlow = _useWaveformSeekBar.asStateFlow() @@ -106,17 +96,6 @@ class AxDynamicBarSettings @Inject constructor( settingsObserver, UserHandle.USER_ALL, ) - secureSettings.registerContentObserverForUserSync( - KEY_COMPACT_NOTIFICATIONS, - false, - settingsObserver, - UserHandle.USER_ALL, - ) - globalSettings.registerContentObserverSync( - Global.HEADS_UP_NOTIFICATIONS_ENABLED, - false, - settingsObserver, - ) contentResolver.registerContentObserver( Settings.System.getUriFor(Settings.System.MEDIA_WAVEFORM_SEEKBAR), false, @@ -129,7 +108,6 @@ class AxDynamicBarSettings @Inject constructor( if (!initialized) return initialized = false secureSettings.getContentResolver().unregisterContentObserver(settingsObserver) - globalSettings.getContentResolver().unregisterContentObserver(settingsObserver) contentResolver.unregisterContentObserver(settingsObserver) } @@ -142,10 +120,6 @@ class AxDynamicBarSettings @Inject constructor( secureSettings.getIntForUser(KEY_KEYGUARD_BATTERY_CHIP_MODE, 1, UserHandle.USER_CURRENT) _isKeyguardMusicPillEnabled.value = secureSettings.getIntForUser(KEY_KEYGUARD_MUSIC_PILL_ENABLED, 0, UserHandle.USER_CURRENT) == 1 - _compactNotifications.value = - secureSettings.getIntForUser(KEY_COMPACT_NOTIFICATIONS, 1, UserHandle.USER_CURRENT) == 1 - _isHeadsUpEnabled.value = - globalSettings.getInt(Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1) == 1 _useWaveformSeekBar.value = Settings.System.getIntForUser( contentResolver, Settings.System.MEDIA_WAVEFORM_SEEKBAR, 0, UserHandle.USER_CURRENT,) == 1 @@ -167,7 +141,4 @@ class AxDynamicBarSettings @Inject constructor( val typeId = EVENT_TYPE_IDS[event::class.java] ?: return true return typeId !in _disabledEventTypes.value } - - fun isNotificationEventsActive(): Boolean = - _isEnabled.value && "notification" !in _disabledEventTypes.value } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt index e0e623e0d93b..2f6ee1e9be98 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandEvent.kt @@ -1,10 +1,10 @@ package com.android.systemui.axdynamicbar.model import android.app.Notification -import android.app.RemoteInput import android.graphics.drawable.Drawable import android.net.Uri import android.service.notification.StatusBarNotification +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel enum class RecordingState { RECORDING, @@ -12,8 +12,6 @@ enum class RecordingState { SAVED, } -data class MicCamApp(val packageName: String, val appName: String, val appIcon: Drawable? = null) - const val NOTIFICATION_DECAY_MS = 5000L const val NOTIFICATION_FRESH_PRIORITY = 75 const val NOTIFICATION_STALE_PRIORITY = 15 @@ -33,21 +31,9 @@ sealed class IslandEvent(open val priority: Int, val id: String) : Comparable 0 - } - - data class MicCamActive( - val isMic: Boolean, - val isCam: Boolean, - val appName: String = "", - val apps: List = emptyList(), - ) : IslandEvent(priority = 85, id = "mic_cam") { - override fun withoutDrawables() = copy(apps = apps.map { it.copy(appIcon = null) }) - } + data class AospChip( + val active: OngoingActivityChipModel.Active, + ) : IslandEvent(priority = priorityForAospChipKey(active.key), id = "aosp_${active.key}") data class AudioRecording( val appName: String = "", @@ -60,9 +46,6 @@ sealed class IslandEvent(open val priority: Int, val id: String) : Comparable = emptyList(), - val replyAction: ReplyAction? = null, - val isConversation: Boolean = false, - val isGroupConversation: Boolean = false, - val conversationTitle: String? = null, - val senderIcon: Drawable? = null, - val senderName: String? = null, val groupKey: String? = null, val isGroupSummary: Boolean = false, - val notificationImage: Drawable? = null, - val callStartTimeMs: Long = 0L, val createdAt: Long = System.currentTimeMillis(), ) : IslandEvent(priority = NOTIFICATION_STALE_PRIORITY, id = "notification_${sbn.key}") { override val behavior = EventBehavior(autoDismissMs = null) @@ -317,15 +286,13 @@ sealed class IslandEvent(open val priority: Int, val id: String) : Comparable> = setOf( - ScreenRecording::class.java, - MicCamActive::class.java, AudioRecording::class.java, - Casting::class.java, PromotedOngoing::class.java, Sports::class.java, Media::class.java, Timer::class.java, Stopwatch::class.java, + AospChip::class.java, ) } @@ -340,3 +307,11 @@ enum class IslandState { CHIP, } +internal fun priorityForAospChipKey(key: String): Int = when { + key.startsWith("callChip-") -> 88 + key == "ScreenRecord" -> 90 + key == "ShareToApp" -> 86 + key == "CastToOtherDevice" -> 82 + else -> 70 +} + diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt index debea8326302..66648ba9a4b4 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/model/IslandUiState.kt @@ -6,11 +6,10 @@ data class IslandUiState( val pinnedEventIndex: Int = 0, val manuallyHidden: Boolean = false, val forceVisible: Boolean = false, - val notificationAlert: IslandEvent.Notification? = null, ) { val shouldShow: Boolean - get() = islandState == IslandState.CHIP || notificationAlert != null + get() = islandState == IslandState.CHIP val isChip: Boolean get() = islandState == IslandState.CHIP @@ -21,4 +20,3 @@ data class IslandUiState( val activeEvents: List get() = events.filter { it !is IslandEvent.Notification } } - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt index 2a406d59f4b0..404f2c84d44c 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandActions.kt @@ -5,10 +5,7 @@ import com.android.systemui.axdynamicbar.model.IslandEvent val EVENT_TYPE_IDS: Map, String> = mapOf( - IslandEvent.ScreenRecording::class.java to "screen_recording", - IslandEvent.MicCamActive::class.java to "privacy", IslandEvent.AudioRecording::class.java to "audio_recording", - IslandEvent.Casting::class.java to "casting", IslandEvent.PromotedOngoing::class.java to "promoted_ongoing", IslandEvent.Sports::class.java to "sports", IslandEvent.Media::class.java to "media", @@ -25,6 +22,7 @@ val EVENT_TYPE_IDS: Map, String> = IslandEvent.AppSwitch::class.java to "app_switch", IslandEvent.Torch::class.java to "torch", IslandEvent.BiometricUnlock::class.java to "biometric_unlock", + IslandEvent.AospChip::class.java to "aosp_chip", ) interface IslandActions { @@ -39,7 +37,6 @@ interface IslandActions { fun sendCustomAction(action: String) fun openMediaOutputSwitcher() fun openMediaApp() - fun stopScreenRecording() fun disconnectBluetooth(address: String) fun setRingerMode(mode: Int) fun toggleTorch() @@ -52,7 +49,5 @@ interface IslandActions { fun switchToApp(taskId: Int) fun onNotificationInteraction(eventId: String) fun onNotificationInteractionEnd(eventId: String) - fun onNotificationAlertInteractionStart() - fun onNotificationAlertInteractionEnd() fun launchNotificationDismissingKeyguard(event: IslandEvent.Notification) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt index 9a067eaaf532..35b61670b44b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandContentTokens.kt @@ -10,6 +10,7 @@ import androidx.core.graphics.ColorUtils import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -265,6 +266,10 @@ internal fun chipAccentColorFor(event: IslandEvent): Color { val color = rememberPaletteColor(event.appIcon!!) if (color != null) return ensureContrast(color, isDark) } + if (event is IslandEvent.AospChip) { + val ctx = LocalContext.current + return Color(event.active.colors.background(ctx).defaultColor) + } return accentColorFor(event) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt index 06f9f3dcf795..d7a74ff28188 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/IslandEventMapper.kt @@ -21,21 +21,18 @@ import androidx.compose.material.icons.filled.Alarm import androidx.compose.material.icons.filled.AvTimer import androidx.compose.material.icons.filled.BatteryChargingFull import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.Cast import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.FiberManualRecord import androidx.compose.material.icons.filled.Fingerprint import androidx.compose.material.icons.filled.FlashlightOn import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.NotificationsActive -import androidx.compose.material.icons.filled.NotificationsOff import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Timer import androidx.compose.material.icons.filled.Vibration -import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VolumeOff +import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material.icons.filled.Wifi import android.media.AudioManager @@ -52,11 +49,6 @@ internal data class EventStyle( ) internal fun eventStyleFor(event: IslandEvent): EventStyle = when (event) { - is IslandEvent.ScreenRecording -> EventStyle( - accent = RedAccent, - icon = Icons.Filled.FiberManualRecord, - labelRes = R.string.ax_dynamic_bar_screen_recording, - ) is IslandEvent.AudioRecording -> EventStyle( accent = when (event.state) { RecordingState.RECORDING -> RedAccent @@ -74,23 +66,6 @@ internal fun eventStyleFor(event: IslandEvent): EventStyle = when (event) { RecordingState.SAVED -> R.string.ax_dynamic_bar_saved }, ) - is IslandEvent.MicCamActive -> EventStyle( - accent = PinkAccent, - icon = when { - event.isCam -> Icons.Filled.Videocam - else -> Icons.Filled.Mic - }, - labelRes = when { - event.isCam && event.isMic -> R.string.ax_dynamic_bar_camera_mic - event.isCam -> R.string.ax_dynamic_bar_camera - else -> R.string.ax_dynamic_bar_microphone - }, - ) - is IslandEvent.Casting -> EventStyle( - accent = TealAccent, - icon = Icons.Filled.Cast, - labelRes = R.string.ax_dynamic_bar_casting_to, - ) is IslandEvent.Media -> EventStyle( accent = PurpleAccent, icon = Icons.Filled.MusicNote, @@ -153,9 +128,9 @@ internal fun eventStyleFor(event: IslandEvent): EventStyle = when (event) { else -> BlueAccent }, icon = when (event.mode) { - AudioManager.RINGER_MODE_SILENT -> Icons.Filled.NotificationsOff + AudioManager.RINGER_MODE_SILENT -> Icons.Filled.VolumeOff AudioManager.RINGER_MODE_VIBRATE -> Icons.Filled.Vibration - else -> Icons.Filled.NotificationsActive + else -> Icons.Filled.VolumeUp }, labelRes = when (event.mode) { AudioManager.RINGER_MODE_SILENT -> R.string.ax_dynamic_bar_silent @@ -203,4 +178,9 @@ internal fun eventStyleFor(event: IslandEvent): EventStyle = when (event) { icon = null, labelRes = R.string.ax_dynamic_bar_on, ) + is IslandEvent.AospChip -> EventStyle( + accent = BlueAccent, + icon = Icons.Filled.Notifications, + labelRes = R.string.ax_dynamic_bar_on, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/NotificationActionType.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/NotificationActionType.kt new file mode 100644 index 000000000000..2aa73009847f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/shared/NotificationActionType.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.shared + +import android.app.Notification +import android.content.Context + +enum class NotificationActionType { + STOP, + PAUSE, + RESUME, + LAP, + RESET, + DELETE, + ADD_MINUTE, + SNOOZE, + DISMISS, + OTHER, +} + +fun Notification.Action.classify(context: Context, packageName: String): NotificationActionType { + val intentAction = try { actionIntent?.intent?.action } catch (_: Exception) { null } + when (intentAction) { + "STOP" -> return NotificationActionType.STOP + "PAUSE" -> return NotificationActionType.PAUSE + "RESUME" -> return NotificationActionType.RESUME + "com.android.deskclock.action.PAUSE_TIMER", + "com.android.deskclock.action.PAUSE_STOPWATCH" -> return NotificationActionType.PAUSE + "com.android.deskclock.action.START_TIMER", + "com.android.deskclock.action.START_STOPWATCH" -> return NotificationActionType.RESUME + "com.android.deskclock.action.RESET_TIMER", + "com.android.deskclock.action.RESET_STOPWATCH" -> return NotificationActionType.RESET + "com.android.deskclock.action.LAP_STOPWATCH" -> return NotificationActionType.LAP + "com.android.deskclock.action.ADD_MINUTE_TIMER" -> return NotificationActionType.ADD_MINUTE + "com.android.deskclock.ALARM_SNOOZE" -> return NotificationActionType.SNOOZE + "com.android.deskclock.ALARM_DISMISS" -> return NotificationActionType.DISMISS + } + + val iconResName: String? = run { + val icon = getIcon() ?: return@run null + val resId = try { icon.resId } catch (_: Exception) { 0 } + if (resId == 0) return@run null + try { + context.packageManager.getResourcesForApplication(packageName) + .getResourceEntryName(resId).lowercase() + } catch (_: Exception) { + null + } + } + + when { + iconResName == null -> Unit + iconResName.contains("stop") -> return NotificationActionType.STOP + iconResName.contains("pause") -> return NotificationActionType.PAUSE + iconResName.contains("play") -> return NotificationActionType.RESUME + iconResName.contains("delete") || iconResName.contains("trash") -> return NotificationActionType.DELETE + iconResName.contains("reset") -> return NotificationActionType.RESET + iconResName.contains("lap") -> return NotificationActionType.LAP + } + + val label = title?.toString()?.lowercase() ?: "" + return when { + label.contains("stop") -> NotificationActionType.STOP + label.contains("delete") -> NotificationActionType.DELETE + label.contains("pause") -> NotificationActionType.PAUSE + label.contains("resume") || label.contains("play") -> NotificationActionType.RESUME + label.contains("lap") -> NotificationActionType.LAP + label.contains("reset") -> NotificationActionType.RESET + label.contains("snooze") -> NotificationActionType.SNOOZE + label.contains("dismiss") -> NotificationActionType.DISMISS + else -> NotificationActionType.OTHER + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 0acc123dafc9..182f66981cb0 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -1,7 +1,9 @@ package com.android.systemui.axdynamicbar.ui +import com.android.systemui.animation.Expandable import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor import com.android.systemui.axdynamicbar.model.IslandEvent +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dagger.SysUISingleton @@ -26,16 +28,13 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch data class AxDynamicBarChipState( val event: IslandEvent, val eventCount: Int, val pinnedIndex: Int, val allEvents: List, - val notificationAlert: IslandEvent.Notification? = null, ) data class KeyguardBatteryInfo( @@ -58,6 +57,8 @@ constructor( authController: AuthController, udfpsOverlayInteractor: UdfpsOverlayInteractor, keyguardIndicationController: KeyguardIndicationController, + val keyguardExpansion: AxDynamicBarKeyguardExpansion, + val statusBarExpansion: AxDynamicBarStatusBarExpansion, ) { val isLowUdfps: StateFlow = udfpsOverlayInteractor.udfpsOverlayParams @@ -75,14 +76,12 @@ constructor( interactor.uiState .map { uiState -> if (!uiState.shouldShow) return@map null - val alert = uiState.notificationAlert - val topEvent = uiState.topEvent ?: alert ?: return@map null + val topEvent = uiState.topEvent ?: return@map null AxDynamicBarChipState( event = topEvent, eventCount = uiState.activeEvents.size, pinnedIndex = uiState.pinnedEventIndex, allEvents = uiState.events, - notificationAlert = alert, ) } .stateIn(applicationScope, SharingStarted.Lazily, null) @@ -153,56 +152,9 @@ constructor( _chipCenterXFraction.value = fraction } - private val _isExpanded = MutableStateFlow(false) - val isExpanded: StateFlow = _isExpanded.asStateFlow() - @Volatile private var collapseOnNullJob: Job? = null - - val isKeyguardExpanded: StateFlow = - combine(isExpanded, isOnKeyguard) { exp, kg -> exp && kg } - .stateIn(applicationScope, SharingStarted.Lazily, false) - - init { - - chipState.onEach { state -> - if (state == null) { - collapseOnNullJob?.cancel() - collapseOnNullJob = applicationScope.launch { - delay(200) - _isExpanded.value = false - } - } else { - collapseOnNullJob?.cancel() - collapseOnNullJob = null - } - }.launchIn(applicationScope) - - interactor.isPanelExpanded.onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) - interactor.qsExpansion.map { it > 0f }.distinctUntilChanged().onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) - - isBouncerShowing.onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) - - isOnKeyguard.onEach { if (!it) _isExpanded.value = false }.launchIn(applicationScope) - - combine(interactor.legacyShadeExpansion, isOnKeyguard) { expansion, onKg -> - onKg && expansion < 0.95f - }.onEach { dismissing -> if (dismissing) _isExpanded.value = false }.launchIn(applicationScope) - - interactor.isDozing.drop(1).onEach { _isExpanded.value = false }.launchIn(applicationScope) - - interactor.dozeAmount.map { it > 0f }.distinctUntilChanged().onEach { if (it) _isExpanded.value = false }.launchIn(applicationScope) - } - - fun expandPanel() { - if (chipState.value != null) _isExpanded.value = true - } - - fun collapsePanel() { - _isExpanded.value = false - } + val isExpanded: StateFlow = statusBarExpansion.isExpanded - fun togglePanel() { - if (_isExpanded.value) collapsePanel() else expandPanel() - } + val isKeyguardExpanded: StateFlow = keyguardExpansion.isExpanded fun cycleNext() = interactor.cycleNext() @@ -218,12 +170,29 @@ constructor( fun toggleTorch() = interactor.toggleTorch() - fun stopScreenRecording() = interactor.stopScreenRecording() - fun launchNotificationFromKeyguard(event: IslandEvent.Notification) { interactor.launchNotificationDismissingKeyguard(event) } + fun handleAospChipTap(event: IslandEvent.AospChip, expandable: Expandable): Boolean { + val active = event.active + return when (val behavior = active.clickBehavior) { + is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { + behavior.onClick() + true + } + is OngoingActivityChipModel.ClickBehavior.HideHeadsUpNotification -> { + behavior.onClick() + true + } + is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { + behavior.onClick(expandable) + true + } + is OngoingActivityChipModel.ClickBehavior.None -> false + } + } + companion object { private const val LOW_UDFPS_THRESHOLD = 0.93f private const val BATTERY_STRING_REFRESH_MS = 2_000L diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index dd98c96c7bac..c5daf38657b3 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -14,16 +14,14 @@ import androidx.compose.animation.scaleOut import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.input.pointer.PointerEventPass @@ -47,9 +45,7 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.compose.theme.PlatformTheme import com.android.systemui.shared.recents.utilities.Utilities import com.android.systemui.axdynamicbar.model.IslandEvent -import com.android.systemui.axdynamicbar.shared.ExpandedMaxWidth import com.android.systemui.axdynamicbar.ui.compose.ExpandedIslandContent -import com.android.systemui.axdynamicbar.ui.compose.NotificationAlertCard import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -61,7 +57,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import android.view.WindowInsets @@ -84,16 +79,15 @@ constructor( private var hideOverlayJob: Job? = null fun init() { - viewModel.interactor.onCollapseRequested = { viewModel.collapsePanel() } + viewModel.interactor.onCollapseRequested = { viewModel.statusBarExpansion.collapse() } viewModel.interactor.onFocusableRequested = { focusable -> setOverlayFocusable(focusable) } val needsOverlay = combine( viewModel.isExpanded, - viewModel.interactor.uiState.map { it.notificationAlert }, viewModel.isOnKeyguard, - ) { expanded, alert, onKeyguard -> - !onKeyguard && (expanded || alert != null) + ) { expanded, onKeyguard -> + !onKeyguard && expanded } needsOverlay @@ -242,32 +236,26 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight val isLargeScreen = Utilities.isLargeScreen(LocalContext.current) val largeScreenExtra = if (isLargeScreen) 4.dp else 0.dp - val topPad = if (hasCutout) largeScreenExtra + val baseTopPad = 4.dp + val topPad = baseTopPad + if (hasCutout) largeScreenExtra else with(density) { statusBarHeightPx.toDp() } + largeScreenExtra val chipState by viewModel.chipState.collectAsStateWithLifecycle() val isExpanded by viewModel.isExpanded.collectAsStateWithLifecycle() - val uiState by viewModel.interactor.uiState.collectAsStateWithLifecycle() - val isOnKeyguard by viewModel.isOnKeyguard.collectAsStateWithLifecycle() val chipX by viewModel.chipCenterXFraction.collectAsStateWithLifecycle() - val notifAlert = uiState.notificationAlert - val compactNotifs by viewModel.interactor.settings.compactNotifications.collectAsStateWithLifecycle() - - val lastAlert = remember { mutableStateOf(null) } - if (notifAlert != null) lastAlert.value = notifAlert val expandedVisible = remember { MutableTransitionState(false) } - val showNotif = !isExpanded && notifAlert != null - val notifVisible = remember { MutableTransitionState(false) } LaunchedEffect(isExpanded) { expandedVisible.targetState = isExpanded } - LaunchedEffect(showNotif) { - notifVisible.targetState = showNotif - } val originX = chipX val origin = TransformOrigin(originX, 0f) + + val chipAlignment = BiasAlignment( + horizontalBias = originX * 2f - 1f, + verticalBias = -1f, + ) AnimatedVisibility( visibleState = expandedVisible, @@ -306,7 +294,7 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight val dx = change.position.x - downPos.x val dy = change.position.y - downPos.y if (dx * dx + dy * dy <= slop * slop) { - viewModel.collapsePanel() + viewModel.statusBarExpansion.collapse() } } break @@ -315,56 +303,21 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight } } .padding(top = topPad), - contentAlignment = Alignment.TopCenter, + contentAlignment = chipAlignment, ) { chipState?.let { state -> + val filtered = state.allEvents.filter { it !is IslandEvent.AospChip } + if (filtered.isEmpty()) return@let ExpandedIslandContent( - events = state.allEvents, + events = filtered, interactor = viewModel.interactor, - onCollapse = { viewModel.collapsePanel() }, + onCollapse = { viewModel.statusBarExpansion.collapse() }, pinnedEventId = state.event.id, hapticsViewModelFactory = viewModel.interactor.sliderHapticsViewModelFactory, ) } } } - - val alertOrigin = TransformOrigin(0.5f, 0f) - - AnimatedVisibility( - visibleState = notifVisible, - enter = fadeIn(tween(300)) + scaleIn( - animationSpec = tween(300), - initialScale = 0.4f, - transformOrigin = alertOrigin, - ), - exit = fadeOut(tween(250)) + scaleOut( - animationSpec = tween(250), - targetScale = 0.4f, - transformOrigin = alertOrigin, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = topPad), - contentAlignment = Alignment.TopCenter, - ) { - val alert = lastAlert.value - if (alert != null) { - NotificationAlertCard( - notification = alert, - interactor = viewModel.interactor, - onDismiss = { viewModel.interactor.dismissNotificationAlert() }, - initiallyCompact = compactNotifs, - modifier = - Modifier.widthIn(max = ExpandedMaxWidth) - .fillMaxWidth() - .padding(horizontal = 12.dp), - ) - } - } - } } private class PanelLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner, diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarKeyguardExpansion.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarKeyguardExpansion.kt new file mode 100644 index 000000000000..97edd7efe308 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarKeyguardExpansion.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui + +import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class AxDynamicBarKeyguardExpansion +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val interactor: AxDynamicBarInteractor, +) { + private val _intent = MutableStateFlow(false) + + private val hasChip = + interactor.uiState + .map { it.shouldShow && it.topEvent != null } + .distinctUntilChanged() + + val canShow: StateFlow = run { + val contextAndDoze = + combine( + interactor.isOnKeyguard, + interactor.isDozing, + interactor.dozeAmount.map { it > 0f }.distinctUntilChanged(), + interactor.qsExpansion.map { it > 0f }.distinctUntilChanged(), + interactor.isPanelExpanded, + ) { onKg, dozing, dozeAmt, qs, panelExp -> + onKg && !dozing && !dozeAmt && !qs && !panelExp + } + val shadeAndBouncer = + combine( + interactor.isBouncerShowing, + interactor.legacyShadeExpansion.map { it >= 0.95f }.distinctUntilChanged(), + hasChip, + ) { bouncer, shadeFull, chip -> + !bouncer && shadeFull && chip + } + combine(contextAndDoze, shadeAndBouncer) { a, b -> a && b } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.Eagerly, false) + } + + val isExpanded: StateFlow = + combine(_intent, canShow) { intent, show -> intent && show } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.Lazily, false) + + private val _collapseSettled = MutableSharedFlow(extraBufferCapacity = 1) + val collapseSettled: SharedFlow = _collapseSettled.asSharedFlow() + + fun notifyCollapseSettled() { + _collapseSettled.tryEmit(Unit) + } + + init { + interactor.isOnKeyguard + .onEach { if (!it) collapse() } + .launchIn(applicationScope) + } + + fun expand() { + if (interactor.uiState.value.topEvent == null) return + _intent.value = true + } + + fun collapse() { + _intent.value = false + } + + fun toggle() { + if (_intent.value) collapse() else expand() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt new file mode 100644 index 000000000000..d890fba8fad4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.axdynamicbar.ui + +import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class AxDynamicBarStatusBarExpansion +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val interactor: AxDynamicBarInteractor, +) { + private val _intent = MutableStateFlow(false) + + val isExpanded: StateFlow = + combine(_intent, interactor.isOnKeyguard) { intent, onKg -> intent && !onKg } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.Lazily, false) + + init { + interactor.isOnKeyguard + .onEach { if (it) collapse() } + .launchIn(applicationScope) + + combine( + interactor.qsExpansion.map { it > 0f }.distinctUntilChanged(), + interactor.legacyShadeExpansion.map { it > 0f }.distinctUntilChanged(), + interactor.isPanelExpanded, + ) { qs, shade, panel -> qs || shade || panel } + .distinctUntilChanged() + .onEach { if (it) collapse() } + .launchIn(applicationScope) + } + + fun expand() { + if (interactor.uiState.value.topEvent == null) return + _intent.value = true + } + + fun collapse() { + _intent.value = false + } + + fun toggle() { + if (_intent.value) collapse() else expand() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt index 7f4c9fbd9e56..5b556e40264e 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.dimensionResource +import com.android.compose.animation.Expandable +import com.android.compose.animation.rememberExpandableController import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import android.graphics.drawable.Drawable @@ -102,6 +104,7 @@ fun AxDynamicBarChip( } val touchSlop = LocalViewConfiguration.current.touchSlop + val expandableController = rememberExpandableController(color = Color.Transparent, shape = ChipShape) val motionScheme = MaterialTheme.motionScheme @@ -131,9 +134,16 @@ fun AxDynamicBarChip( if (totalDx > 0) viewModel.cyclePrev() else viewModel.cycleNext() } else if (!decided) { - + change.consume() - viewModel.togglePanel() + val current = state?.event + if (current is IslandEvent.AospChip) { + if (!viewModel.handleAospChipTap(current, expandableController.expandable)) { + viewModel.statusBarExpansion.toggle() + } + } else { + viewModel.statusBarExpansion.toggle() + } } break @@ -168,9 +178,14 @@ fun AxDynamicBarChip( }, ) { state?.let { chipState -> - val displayEvent = chipState.notificationAlert ?: chipState.event - val isAlert = chipState.notificationAlert != null + val displayEvent = chipState.event + val isAlert = false + Expandable( + controller = expandableController, + onClick = null, + defaultMinSize = false, + ) { _ -> AnimatedContent( targetState = ChipDisplay(displayEvent, isAlert), transitionSpec = { @@ -205,6 +220,7 @@ fun AxDynamicBarChip( Row( modifier = Modifier.height(ChipHeight) + .widthIn(max = 100.dp) .clip(ChipShape) .background(accent) .animateContentSize(motionScheme.defaultSpatialSpec()) @@ -322,7 +338,7 @@ fun AxDynamicBarChip( }, contentKey = { textKeyFor(it) }, label = "chip_text", - modifier = Modifier.weight(1f, fill = false), + modifier = Modifier.weight(1f, fill = false).widthIn(max = chipTextMaxWidth), ) { event -> PillEventText( event, @@ -355,6 +371,7 @@ fun AxDynamicBarChip( } } } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index a2a0ed2f7e48..4e6d1c52b60d 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -2,11 +2,13 @@ package com.android.systemui.axdynamicbar.ui.compose +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing import kotlin.math.abs @@ -18,14 +20,10 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme -import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image @@ -122,45 +120,39 @@ fun AxDynamicBarKeyguardChip( Box(modifier = modifier) { + val expandedVisibleState = remember { MutableTransitionState(false) } + expandedVisibleState.targetState = isKeyguardExpanded && state != null + LaunchedEffect(expandedVisibleState.isIdle, expandedVisibleState.currentState) { + if (expandedVisibleState.isIdle && !expandedVisibleState.currentState) { + viewModel.keyguardExpansion.notifyCollapseSettled() + } + } AnimatedVisibility( - visible = isKeyguardExpanded && state != null, - enter = fadeIn(motionScheme.defaultEffectsSpec()) + - slideInVertically( - animationSpec = motionScheme.defaultSpatialSpec(), - initialOffsetY = { it / 3 }, - ) + - expandVertically( - animationSpec = motionScheme.defaultSpatialSpec(), - expandFrom = Alignment.Bottom, - ), - exit = fadeOut(motionScheme.fastEffectsSpec()) + - slideOutVertically( - animationSpec = motionScheme.fastSpatialSpec(), - targetOffsetY = { it / 4 }, - ) + - shrinkVertically( - animationSpec = motionScheme.fastSpatialSpec(), - shrinkTowards = Alignment.Bottom, - ), + visibleState = expandedVisibleState, + enter = fadeIn(motionScheme.defaultEffectsSpec()), + exit = fadeOut(tween(durationMillis = 250)), modifier = Modifier .fillMaxWidth() - .align(Alignment.TopStart) - .padding(bottom = ChipHeight + SpaceLg), + .align(Alignment.Center), ) { state?.let { KeyguardExpandedContent( event = it.event, allEvents = it.allEvents, interactor = viewModel.interactor, - onCollapse = { viewModel.collapsePanel() }, + onCollapse = { viewModel.keyguardExpansion.collapse() }, hapticsViewModelFactory = viewModel.interactor.sliderHapticsViewModelFactory, ) } } AnimatedVisibility( - visible = isOnKeyguard && isEnabled && isKeyguardEnabled, - enter = fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.9f, animationSpec = motionScheme.defaultSpatialSpec()), + visible = isOnKeyguard && isEnabled && isKeyguardEnabled && !isKeyguardExpanded, + enter = fadeIn(tween(durationMillis = 200, delayMillis = 300)) + + scaleIn( + initialScale = 0.9f, + animationSpec = tween(durationMillis = 200, delayMillis = 300), + ), exit = fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.9f, animationSpec = motionScheme.fastSpatialSpec()), modifier = Modifier .align(Alignment.BottomCenter) @@ -195,7 +187,7 @@ fun AxDynamicBarKeyguardChip( ) { val chipState = state if (chipState != null) { - val rawEvent = chipState.notificationAlert ?: chipState.event + val rawEvent = chipState.event val displayEvent: com.android.systemui.axdynamicbar.model.IslandEvent? = when { isKeyguardMusicPillEnabled -> rawEvent @@ -289,7 +281,7 @@ private fun KeyguardChipBody( Row( modifier = Modifier .height(ChipHeight) - .widthIn(max = 260.dp) + .widthIn(max = 178.dp) .clip(ChipShape) .background(accent) .animateContentSize(motionScheme.defaultSpatialSpec()) @@ -313,7 +305,7 @@ private fun KeyguardChipBody( is IslandEvent.KeyguardIndication, is IslandEvent.AppSwitch -> { } - else -> viewModel.togglePanel() + else -> viewModel.keyguardExpansion.toggle() } } .padding(start = SpaceSm, end = SpaceMd), @@ -488,7 +480,8 @@ private fun KeyguardChipBody( } if (event !is IslandEvent.Media) { - val actions = actionsFor(event) + val actionsCtx = LocalContext.current + val actions = actionsFor(event, actionsCtx) if (actions.isNotEmpty()) { Spacer(Modifier.width(SpaceXs)) actions.forEach { action -> @@ -730,9 +723,6 @@ private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = Bat @Composable private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modifier) { when (event) { - is IslandEvent.ScreenRecording -> - if (event.isCountdown) MarqueeText(formatCountdownSeconds(event.countdownSeconds), color, modifier) - else ElapsedTimeText(event.startTimeMs, color, modifier) is IslandEvent.AudioRecording -> when (event.state) { RecordingState.RECORDING -> ElapsedTimeText( event.startTimeMs, color, modifier, event.pausedDurationMs, @@ -757,7 +747,6 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi ) } is IslandEvent.Alarm -> MarqueeText(event.label.ifEmpty { stringResource(R.string.ax_dynamic_bar_alarm) }, color, modifier) - is IslandEvent.Casting -> MarqueeText(event.deviceName.take(12), color, modifier) is IslandEvent.Torch -> MarqueeText( if (event.supportsLevel) "${(event.level.toFloat() / event.maxLevel * 100).toInt()}%" else stringResource(R.string.ax_dynamic_bar_flashlight), @@ -770,16 +759,6 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi ) is IslandEvent.BiometricUnlock -> MarqueeText(stringResource(R.string.ax_dynamic_bar_unlocked), color, modifier) is IslandEvent.AppSwitch -> MarqueeText(stringResource(R.string.ax_dynamic_bar_recents), color, modifier) - is IslandEvent.MicCamActive -> MarqueeText( - event.appName.ifEmpty { - buildString { - if (event.isCam) append(stringResource(R.string.ax_dynamic_bar_cam_short)) - if (event.isMic && event.isCam) append(" · ") - if (event.isMic) append(stringResource(R.string.ax_dynamic_bar_mic_short)) - } - }, - color, modifier, - ) is IslandEvent.PromotedOngoing -> MarqueeText( event.shortText.ifEmpty { event.title.ifEmpty { event.appName } }, color, modifier, ) @@ -790,15 +769,16 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi "${event.songTitle} · ${event.artist}".trimEnd(' ', '·', ' '), color, modifier, ) is IslandEvent.KeyguardIndication -> MarqueeText(event.text, color, modifier) + is IslandEvent.AospChip -> { + val text = (event.active.content as? OngoingActivityChipModel.Content.Text)?.text + if (!text.isNullOrEmpty()) MarqueeText(text, color, modifier) + } } } @Composable private fun secondaryTextFor(event: IslandEvent): String? = when (event) { is IslandEvent.Media -> event.artist.takeIf { it.isNotBlank() } - is IslandEvent.ScreenRecording -> - if (event.isCountdown) formatCountdownSeconds(event.countdownSeconds) - else stringResource(R.string.ax_dynamic_bar_rec_short) is IslandEvent.AudioRecording -> event.appName.takeIf { it.isNotBlank() } is IslandEvent.Timer -> event.label.takeIf { it.isNotBlank() } is IslandEvent.Stopwatch -> event.label.takeIf { it.isNotBlank() } @@ -811,7 +791,6 @@ private fun secondaryTextFor(event: IslandEvent): String? = when (event) { "%d:%02d".format(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)) } else null } - is IslandEvent.Casting -> stringResource(R.string.ax_dynamic_bar_cast_short) is IslandEvent.Vpn -> stringResource(R.string.ax_dynamic_bar_active) is IslandEvent.BiometricUnlock -> event.sourceName is IslandEvent.AppSwitch -> null @@ -858,7 +837,7 @@ private data class ChipAction( val perform: (AxDynamicBarChipViewModel, IslandEvent, Context) -> Unit, ) -private fun actionsFor(event: IslandEvent): List = when (event) { +private fun actionsFor(event: IslandEvent, context: Context): List = when (event) { is IslandEvent.Media -> listOf( ChipAction(ActionIcon.SKIP_PREV) { vm, _, _ -> vm.skipPrev() }, ChipAction(if (event.isPlaying) ActionIcon.PAUSE else ActionIcon.PLAY) { vm, _, _ -> @@ -866,18 +845,10 @@ private fun actionsFor(event: IslandEvent): List = when (event) { }, ChipAction(ActionIcon.SKIP_NEXT) { vm, _, _ -> vm.skipNext() }, ) - is IslandEvent.ScreenRecording -> - if (event.isCountdown) emptyList() - else listOf(ChipAction(ActionIcon.STOP) { vm, _, _ -> vm.stopScreenRecording() }) is IslandEvent.AudioRecording -> { - val pauseResume = event.actions.firstOrNull { a -> - val label = a.label.toString().lowercase() - label.contains("pause") || label.contains("resume") - } - val stop = event.actions.firstOrNull { a -> - val label = a.label.toString().lowercase() - label.contains("stop") || label.contains("delete") - } + val classified = event.actions.map { it to it.action.classify(context, it.action.actionIntent?.creatorPackage ?: context.packageName) } + val pauseResume = classified.firstOrNull { (_, k) -> k == NotificationActionType.PAUSE || k == NotificationActionType.RESUME }?.first + val stop = classified.firstOrNull { (_, k) -> k == NotificationActionType.STOP || k == NotificationActionType.DELETE }?.first listOfNotNull( pauseResume?.let { action -> ChipAction( @@ -914,9 +885,6 @@ private fun actionsFor(event: IslandEvent): List = when (event) { is IslandEvent.Torch -> listOf( ChipAction(ActionIcon.STOP) { vm, _, _ -> vm.toggleTorch() }, ) - is IslandEvent.Casting -> listOf( - ChipAction(ActionIcon.STOP) { vm, e, _ -> vm.dismissEvent(e) }, - ) else -> emptyList() } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt index c700c3ce5e48..bc21b7929ac0 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarNowBar.kt @@ -91,8 +91,8 @@ fun AxDynamicBarNowBar( exit = slideOutVertically(motionScheme.fastSpatialSpec()) { -it } + fadeOut(motionScheme.fastEffectsSpec()), ) { state?.let { chipState -> - val displayEvent = chipState.notificationAlert ?: chipState.event - val isAlert = chipState.notificationAlert != null + val displayEvent = chipState.event + val isAlert = false AnimatedContent( targetState = NowBarDisplay(displayEvent, isAlert), @@ -144,7 +144,7 @@ fun AxDynamicBarNowBar( if (totalDragX > 0f) viewModel.cyclePrev() else viewModel.cycleNext() } else { - viewModel.togglePanel() + viewModel.statusBarExpansion.toggle() } break } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt index dc26ba77697f..d57029687bf5 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedIslandContent.kt @@ -192,10 +192,7 @@ internal fun ExpandedEventContent( hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) { when (event) { - is IslandEvent.ScreenRecording -> ScreenRecordExpanded(event, interactor) - is IslandEvent.MicCamActive -> MicCamExpanded(event) is IslandEvent.AudioRecording -> AudioRecordingExpanded(event, interactor) - is IslandEvent.Casting -> CastingExpanded(event) is IslandEvent.PromotedOngoing -> PromotedOngoingExpanded(event, interactor) is IslandEvent.Sports -> SportsExpanded(event, interactor) is IslandEvent.NowPlaying -> NowPlayingExpanded(event, interactor) @@ -213,7 +210,8 @@ internal fun ExpandedEventContent( is IslandEvent.AppSwitch -> AppHistoryExpanded(event, interactor) is IslandEvent.Torch -> TorchExpanded(event, interactor, hapticsViewModelFactory) is IslandEvent.BiometricUnlock -> BiometricUnlockExpanded(event) - is IslandEvent.KeyguardIndication -> {} + is IslandEvent.KeyguardIndication -> {} + is IslandEvent.AospChip -> {} } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt index 76e5fd88fe0c..62de66a00104 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedNotificationContent.kt @@ -17,10 +17,7 @@ package com.android.systemui.axdynamicbar.ui.compose import android.app.Notification -import android.app.RemoteInput import android.content.Context -import android.content.Intent -import android.os.Bundle import android.service.notification.StatusBarNotification import android.util.Log import android.util.Size @@ -42,14 +39,10 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore @@ -67,9 +60,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -116,44 +106,22 @@ internal fun NotificationExpanded( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs), ) { - if (event.isConversation && event.senderName != null) { - val subtitle = if (event.isGroupConversation && event.conversationTitle != null) - "${event.appName} · ${event.conversationTitle}" - else - "${event.appName} · ${event.senderName}" + if (event.appName.isNotEmpty()) { Text( - subtitle, + event.appName, color = SubtleGray, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Text( - if (event.isGroupConversation) event.senderName - else event.title ?: event.senderName, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } else { - if (event.appName.isNotEmpty()) { - Text( - event.appName, - color = SubtleGray, - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Text( - event.title ?: event.appName.ifEmpty { event.sbn.packageName.substringAfterLast('.') }, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) } + Text( + event.title ?: event.appName.ifEmpty { event.sbn.packageName.substringAfterLast('.') }, + color = OnCardText, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } event.appIcon?.let { @@ -184,18 +152,6 @@ internal fun NotificationExpanded( } } - event.notificationImage?.let { img -> - Image( - bitmap = img.toScaledBitmap(200.dp), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 200.dp) - .clip(ShapeSm), - contentScale = ContentScale.Crop, - ) - } - Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(SpaceMd), @@ -235,31 +191,20 @@ internal fun NotificationExpanded( } } - event.replyAction?.let { reply -> - NotificationReplyField( - reply = reply, - sbn = event.sbn, - interactor = interactor, - eventId = event.id, - ) - } } } @Composable private fun NotifExpandedAvatar(event: IslandEvent.Notification, size: Dp) { - val icon = event.senderIcon ?: event.appIcon - val isRound = event.isConversation && event.senderIcon != null - val hasBadge = event.isConversation && event.senderIcon != null && event.appIcon != null - - when { - hasBadge && icon != null -> BadgedContactIcon(icon, event.appIcon!!, size, SpaceXxl, true) - icon != null -> Image( + val icon = event.appIcon + if (icon != null) { + Image( bitmap = icon.toScaledBitmap(size), contentDescription = null, - modifier = Modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), + modifier = Modifier.size(size).clip(ShapeIconMedium), ) - else -> Box( + } else { + Box( modifier = Modifier.size(size).clip(ShapeIconMedium).background(BlueAccent.copy(alpha = AlphaSubtle)), contentAlignment = Alignment.Center, ) { @@ -377,81 +322,6 @@ private fun NotificationProgressFallback(event: IslandEvent.Notification) { } } -@Composable -private fun NotificationReplyField( - reply: IslandEvent.ReplyAction, - sbn: StatusBarNotification, - interactor: IslandActions, - eventId: String, -) { - var replyText by remember { mutableStateOf("") } - - Row( - modifier = - Modifier.fillMaxWidth() - .height(40.dp) - .clip(ShapeLg) - .background(DarkCard), - verticalAlignment = Alignment.CenterVertically, - ) { - BasicTextField( - value = replyText, - onValueChange = { replyText = it }, - modifier = - Modifier.weight(1f).padding(horizontal = SpaceLg).onFocusChanged { focusState -> - interactor.onFocusableRequested?.invoke(focusState.isFocused) - if (focusState.isFocused) { - interactor.onNotificationInteraction(eventId) - } - }, - textStyle = MaterialTheme.typography.bodySmall.copy(color = OnCardText), - singleLine = true, - cursorBrush = SolidColor(BlueAccent), - decorationBox = { inner -> - if (replyText.isEmpty()) { - Text(reply.label.toString(), color = SubtleGray, style = MaterialTheme.typography.bodySmall) - } - inner() - }, - ) - if (replyText.isNotEmpty()) { - val context = LocalContext.current - Icon( - Icons.AutoMirrored.Filled.Send, - null, - tint = BlueAccent, - modifier = - Modifier.size(32.dp) - .clip(CircleShape) - .clickable { - sendReply(context, reply, replyText, sbn) - replyText = "" - interactor.onFocusableRequested?.invoke(false) - } - .padding(SpaceSm), - ) - } - } -} - -private fun sendReply( - context: Context, - reply: IslandEvent.ReplyAction, - text: String, - sbn: StatusBarNotification, -) { - val intent = - Intent().apply { - val results = Bundle().apply { putCharSequence(reply.remoteInput.resultKey, text) } - RemoteInput.addResultsToIntent(arrayOf(reply.remoteInput), this, results) - } - try { - reply.action.actionIntent?.sendWithBal(context, intent) - } catch (e: Exception) { - Log.w("NotificationReply", "Failed to send reply", e) - } -} - @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun NotificationGroupCard( @@ -490,10 +360,7 @@ internal fun NotificationGroupCard( if (!isExpanded) { val latest = notifications.first() Text( - buildString { - latest.senderName?.let { append("$it: ") } - append(latest.text ?: latest.title ?: "") - }, + latest.text ?: latest.title ?: "", color = SubtleGray, style = MaterialTheme.typography.labelSmall, maxLines = 1, @@ -583,22 +450,18 @@ private fun GroupedNotificationRow( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs), ) { - val displayName = event.senderName - ?: event.title + val displayName = event.title ?: event.appName.ifEmpty { event.sbn.packageName.substringAfterLast('.') } - val nameWithGroup = if (event.isGroupConversation && event.conversationTitle != null && event.senderName != null) - "$displayName · ${event.conversationTitle}" else displayName Text( - nameWithGroup, + displayName, color = OnCardText, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - val body = event.text ?: if (event.senderName != null) event.title else null - body?.let { + event.text?.let { Text( it, color = SubtleGray, @@ -662,16 +525,6 @@ private fun GroupedNotificationRow( } } - if (childExpanded) { - event.replyAction?.let { reply -> - NotificationReplyField( - reply = reply, - sbn = event.sbn, - interactor = interactor, - eventId = event.id, - ) - } - } } } } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt index dc5a42475108..645a4d484d86 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedOngoingContent.kt @@ -151,47 +151,6 @@ private fun hideCollapseButtons(view: View) { } } -@Composable -internal fun CastingExpanded(event: IslandEvent.Casting) { - val style = eventStyleFor(event) - ExpandedCardLayout( - accentColor = style.accent, - icon = { style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(28.dp)) } }, - title = { - Text(stringResource(style.labelRes), color = SubtleGray, style = MaterialTheme.typography.labelMedium) - Text(event.deviceName, color = OnCardText, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - if (!event.description.isNullOrEmpty()) { - Text(event.description, color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - } - }, - trailing = { PulsingDot(color = style.accent, size = SpaceMd) }, - ) -} - -@Composable -internal fun RowScope.CompactCastingRow(event: IslandEvent.Casting) { - val style = eventStyleFor(event) - Box( - modifier = - Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(style.accent.copy(alpha = AlphaIconBg)), - contentAlignment = Alignment.Center, - ) { - style.icon?.let { Icon(it, null, tint = style.accent, modifier = Modifier.size(20.dp)) } - } - Spacer(Modifier.width(SpaceLg)) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { - Text(stringResource(style.labelRes), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - Text( - event.deviceName, - color = OnCardText, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - PulsingDot(color = style.accent, size = 7.dp) -} - @Composable internal fun PromotedOngoingExpanded( event: IslandEvent.PromotedOngoing, diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt index e4c854c73dcc..8f43a67ba00b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedRecordingContent.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop -import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -42,57 +41,6 @@ import androidx.compose.ui.platform.LocalContext import com.android.systemui.axdynamicbar.shared.* import kotlinx.coroutines.delay -@Composable -internal fun ScreenRecordExpanded( - event: IslandEvent.ScreenRecording, - interactor: IslandActions, -) { - if (event.isCountdown) { - ScreenRecordCountdownExpanded(event) - return - } - var elapsedMs by remember(event.startTimeMs) { - mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) - } - LaunchedEffect(event.startTimeMs) { - while (true) { - delay(1000) - elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) - } - } - ExpandedCardLayout( - accentColor = RedAccent, - icon = { PulsingDot(color = RedAccent, size = 14.dp, durationMs = 550, minAlpha = AlphaTrack) }, - title = { - Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text(formatElapsedTime(elapsedMs), color = RedAccent, style = MaterialTheme.typography.headlineMedium) - }, - trailing = { StatusChip(stringResource(R.string.ax_dynamic_bar_recording), RedAccent) }, - actions = { - ActionChip( - label = stringResource(R.string.ax_dynamic_bar_stop_recording), - icon = Icons.Filled.Stop, - color = OnDestructiveText, - bg = DestructiveBg, - modifier = Modifier.fillMaxWidth(), - onClick = { interactor.stopScreenRecording() }, - ) - }, - ) -} - -@Composable -private fun ScreenRecordCountdownExpanded(event: IslandEvent.ScreenRecording) { - ExpandedCardLayout( - accentColor = RedAccent, - icon = { Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(22.dp)) }, - title = { - Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = SubtleGray, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text(formatCountdownSeconds(event.countdownSeconds), color = RedAccent, style = MaterialTheme.typography.headlineMedium) - }, - ) -} - @Composable internal fun AudioRecordingExpanded( event: IslandEvent.AudioRecording, @@ -148,24 +96,25 @@ internal fun AudioRecordingExpanded( horizontalArrangement = Arrangement.spacedBy(SpaceLg), ) { event.actions.forEach { notifAction -> - val lower = notifAction.label.toString().lowercase() + val pkg = notifAction.action.actionIntent?.creatorPackage ?: context.packageName + val kind = notifAction.action.classify(context, pkg) val btnColor = - when { - lower.contains("stop") || lower.contains("delete") -> DestructiveBg - lower.contains("resume") || lower.contains("play") -> GreenAccent + when (kind) { + NotificationActionType.STOP, NotificationActionType.DELETE -> DestructiveBg + NotificationActionType.RESUME -> GreenAccent else -> ActionBg } val textColor = - when { - lower.contains("stop") || lower.contains("delete") -> OnDestructiveText - lower.contains("resume") || lower.contains("play") -> chipContentColorOn(GreenAccent) + when (kind) { + NotificationActionType.STOP, NotificationActionType.DELETE -> OnDestructiveText + NotificationActionType.RESUME -> chipContentColorOn(GreenAccent) else -> OnActionText } val btnIcon = - when { - lower.contains("stop") || lower.contains("delete") -> Icons.Filled.Stop - lower.contains("resume") || lower.contains("play") -> Icons.Filled.PlayArrow - lower.contains("pause") -> Icons.Filled.Pause + when (kind) { + NotificationActionType.STOP, NotificationActionType.DELETE -> Icons.Filled.Stop + NotificationActionType.RESUME -> Icons.Filled.PlayArrow + NotificationActionType.PAUSE -> Icons.Filled.Pause else -> null } ActionChip( @@ -188,90 +137,6 @@ internal fun AudioRecordingExpanded( ) } -@Composable -internal fun MicCamExpanded(event: IslandEvent.MicCamActive) { - val accent = if (event.isCam) RedAccent else OrangeAccent - ExpandedCardLayout( - accentColor = accent, - icon = { - Row(horizontalArrangement = Arrangement.spacedBy(SpaceSm)) { - if (event.isCam) Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(28.dp)) - if (event.isMic) Icon(Icons.Filled.Mic, null, tint = OrangeAccent, modifier = Modifier.size(28.dp)) - } - }, - title = { - Text( - when { - event.isCam && event.isMic -> stringResource(R.string.ax_dynamic_bar_camera_mic) - event.isCam -> stringResource(R.string.ax_dynamic_bar_camera) - else -> stringResource(R.string.ax_dynamic_bar_microphone) - }, - color = OnCardText, - style = MaterialTheme.typography.titleMedium, - ) - if (event.apps.size > 1) { - Row( - horizontalArrangement = Arrangement.spacedBy(SpaceSm), - verticalAlignment = Alignment.CenterVertically, - ) { - event.apps.take(4).forEach { app -> - app.appIcon?.let { - Image( - bitmap = it.toScaledBitmap(28.dp), - contentDescription = app.appName, - modifier = Modifier.size(28.dp).clip(ShapeIconMedium), - ) - } - } - } - StatusChip(stringResource(R.string.ax_dynamic_bar_apps_count, event.apps.size), accent) - } else if (event.appName.isNotEmpty()) { - StatusChip(event.appName, accent) - } - }, - trailing = { PulsingDot(color = accent, size = SpaceMd) }, - ) -} - -@Composable -internal fun RowScope.CompactRecordingRow(event: IslandEvent.ScreenRecording) { - if (event.isCountdown) { - Box( - modifier = - Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(RedAccent.copy(alpha = AlphaIconBg)), - contentAlignment = Alignment.Center, - ) { - Icon(Icons.Filled.Videocam, null, tint = RedAccent, modifier = Modifier.size(16.dp)) - } - Spacer(Modifier.width(SpaceLg)) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { - Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = OnCardText, style = MaterialTheme.typography.bodySmall) - Text(formatCountdownSeconds(event.countdownSeconds), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - } - return - } - var elapsedMs by remember(event.startTimeMs) { - mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) - } - LaunchedEffect(event.startTimeMs) { - while (true) { - delay(1000) - elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) - } - } - Box( - modifier = - Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(RedAccent.copy(alpha = AlphaIconBg)), - contentAlignment = Alignment.Center, - ) { - PulsingDot(color = RedAccent, size = 12.dp, durationMs = 550, minAlpha = AlphaTrack) - } - Spacer(Modifier.width(SpaceLg)) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { - Text(stringResource(R.string.ax_dynamic_bar_screen_recording), color = OnCardText, style = MaterialTheme.typography.bodySmall) - Text(formatElapsedTime(elapsedMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - } -} @Composable internal fun RowScope.CompactAudioRecordingRow(event: IslandEvent.AudioRecording) { @@ -331,42 +196,3 @@ internal fun RowScope.CompactAudioRecordingRow(event: IslandEvent.AudioRecording } } -@Composable -internal fun RowScope.CompactMicCamRow(event: IslandEvent.MicCamActive) { - val color = if (event.isCam) RedAccent else OrangeAccent - Box( - modifier = Modifier.size(SizeCompactIcon).clip(ShapeCompact).background(color.copy(alpha = AlphaIconBg)), - contentAlignment = Alignment.Center, - ) { - Icon( - if (event.isCam) Icons.Filled.Videocam else Icons.Filled.Mic, - null, - tint = color, - modifier = Modifier.size(18.dp), - ) - } - Spacer(Modifier.width(SpaceLg)) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(SpaceXxs)) { - Text( - when { - event.isCam && event.isMic -> stringResource(R.string.ax_dynamic_bar_camera_mic_short) - event.isCam -> stringResource(R.string.ax_dynamic_bar_camera) - else -> stringResource(R.string.ax_dynamic_bar_microphone) - }, - color = OnCardText, - style = MaterialTheme.typography.bodySmall, - ) - if (event.apps.size > 1) { - Text(stringResource(R.string.ax_dynamic_bar_apps_count, event.apps.size), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - } else if (event.appName.isNotEmpty()) { - Text( - event.appName, - color = SubtleGray, - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt index 6c60e0c9c7d6..4ff95f6cf93d 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedSystemContent.kt @@ -22,8 +22,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BatteryChargingFull import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.BluetoothDisabled -import androidx.compose.material.icons.filled.NotificationsActive -import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.VolumeOff +import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.filled.Vibration import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material.icons.filled.Wifi @@ -162,7 +162,7 @@ internal fun RingerModeExpanded(event: IslandEvent.RingerMode, interactor: Islan ) { RingerCard( isSelected = event.mode == AudioManager.RINGER_MODE_NORMAL, - icon = Icons.Filled.NotificationsActive, + icon = Icons.Filled.VolumeUp, label = stringResource(R.string.ax_dynamic_bar_ring), accent = BlueAccent, onClick = { interactor.setRingerMode(AudioManager.RINGER_MODE_NORMAL) }, @@ -178,7 +178,7 @@ internal fun RingerModeExpanded(event: IslandEvent.RingerMode, interactor: Islan ) RingerCard( isSelected = event.mode == AudioManager.RINGER_MODE_SILENT, - icon = Icons.Filled.NotificationsOff, + icon = Icons.Filled.VolumeOff, label = stringResource(R.string.ax_dynamic_bar_silent), accent = RedAccent, onClick = { interactor.setRingerMode(AudioManager.RINGER_MODE_SILENT) }, diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt index b183400d691e..c328f67d07de 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -2,10 +2,8 @@ package com.android.systemui.axdynamicbar.ui.compose -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable -import android.util.TypedValue -import android.widget.SeekBar +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -13,16 +11,20 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.StartOffset import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -45,7 +47,6 @@ import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.AvTimer -import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -61,26 +62,23 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.viewinterop.AndroidView import kotlin.math.cos import kotlin.math.sin import androidx.compose.ui.unit.Dp @@ -91,12 +89,7 @@ import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.model.RecordingState import com.android.systemui.axdynamicbar.shared.* import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel -import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import kotlinx.coroutines.delay -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.systemui.media.controls.ui.view.WaveformSeekBar - -private val KeyguardSeekBarHeight = 28.dp @Composable internal fun KeyguardExpandedContent( @@ -124,7 +117,6 @@ internal fun KeyguardExpandedContent( is IslandEvent.Media -> KeyguardMediaPanel(event, interactor) is IslandEvent.Timer -> KeyguardTimerPanel(event, interactor) is IslandEvent.Stopwatch -> KeyguardStopwatchPanel(event, interactor) - is IslandEvent.ScreenRecording -> KeyguardRecordingPanel(event, interactor) is IslandEvent.AudioRecording -> KeyguardAudioRecordingPanel(event, interactor) else -> KeyguardGenericPanel(event, interactor, hapticsViewModelFactory) } @@ -214,438 +206,317 @@ private fun TonalBanner( @Composable private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActions) { - val useWaveform = if (interactor is com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor) { - interactor.mediaUseWaveform.collectAsStateWithLifecycle().value - } else { - false - } val colors = rememberMediaColors(event) + val motionScheme = MaterialTheme.motionScheme - val eqBars = if (event.isPlaying) { - val eqTransition = rememberInfiniteTransition(label = "media_eq") - (0 until 4).map { i -> - eqTransition.animateFloat( - initialValue = 0.25f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - tween(500 + i * 100, easing = LinearEasing), - RepeatMode.Reverse, - initialStartOffset = StartOffset(i * 130), - ), - label = "meq_$i", - ) - } - } else { - null - } - - KeyguardPanelSurface { Column(modifier = Modifier.fillMaxWidth()) { - event.albumArt?.let { art -> - Image( - bitmap = art.toScaledBitmap(280.dp), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(280.dp) - .padding(SpaceLg) - .clip(ShapeLg), - contentScale = ContentScale.Crop, - ) - } ?: Box( + Column( + modifier = Modifier + .widthIn(max = ExpandedMaxWidth) + .fillMaxSize() + .padding(horizontal = SpaceSection), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .padding(SpaceLg) - .clip(ShapeLg) - .background(colors.tonal), + .weight(1f, fill = true) + .fillMaxWidth(), contentAlignment = Alignment.Center, ) { - Icon(Icons.Filled.MusicNote, null, tint = colors.accent.copy(AlphaDisabled), modifier = Modifier.size(72.dp)) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SpaceSection), - verticalArrangement = Arrangement.spacedBy(SpaceXxl), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(SpaceXs), - ) { - Text( - event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) }, - color = OnCardText, - style = MaterialTheme.typography.titleLarge, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + AnimatedContent( + targetState = event.albumArt, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + contentKey = { it?.hashCode() ?: 0 }, + label = "kg_media_album_art", + ) { art -> + if (art != null) { + Image( + bitmap = art.toScaledBitmap(SizeAlbumLg), + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(ShapeLg), + contentScale = ContentScale.Crop, ) - if (event.artist.isNotEmpty()) { - Text( - event.artist, - color = colors.accent, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - Spacer(Modifier.width(SpaceLg)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceMd), - ) { - Canvas(modifier = Modifier.width(SpaceSection).height(SpaceXxl)) { - val barW = 3.dp.toPx() - val gap = 2.dp.toPx() - val totalW = 4 * barW + 3 * gap - val startX = (size.width - totalW) / 2f - for (i in 0 until 4) { - val h = (eqBars?.get(i)?.value ?: 0.3f) * size.height - drawRoundRect( - color = colors.accent, - topLeft = Offset(startX + i * (barW + gap), size.height - h), - size = Size(barW, h), - cornerRadius = CornerRadius(barW / 2f), - ) - } - } - event.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier.size(SizeIconSm).clip(ShapeXs), - colorFilter = ColorFilter.tint(OnCardText), - ) - } - } - } - - if (event.outputDeviceName.isNotBlank()) { - Surface( - onClick = { - interactor.openMediaOutputSwitcher() - interactor.collapseIsland() - }, - shape = ShapeChip, - color = colors.tonal, - ) { - Row( - modifier = Modifier.padding(horizontal = SpaceXl, vertical = SpaceMd), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceSm), + } else { + Box( + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(ShapeLg) + .background(colors.tonal), + contentAlignment = Alignment.Center, ) { - Icon(Icons.Filled.VolumeUp, null, tint = SubtleGray, modifier = Modifier.size(SpaceXl)) - Text(event.outputDeviceName, color = SubtleGray, style = MaterialTheme.typography.labelSmall, maxLines = 1) + Icon(Icons.Filled.MusicNote, null, tint = colors.accent.copy(AlphaDisabled), modifier = Modifier.size(SpacePanelLarge)) } } } + } + + Spacer(Modifier.height(SpaceLg)) + + val trackText = event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) } + AnimatedContent( + targetState = trackText, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + label = "kg_media_track", + ) { title -> + Text( + title, + color = Color.White, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } - if (event.duration > 0L) { - KeyguardMediaSeekBar(event, interactor, colors.accent, useWaveform) + if (event.artist.isNotEmpty()) { + Spacer(Modifier.height(SpaceMd)) + AnimatedContent( + targetState = event.artist, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + label = "kg_media_artist", + ) { artist -> + Text( + artist, + color = Color.White.copy(alpha = AlphaSecondary), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } + Spacer(Modifier.height(SpaceLg)) + Surface( modifier = Modifier .fillMaxWidth() - .padding(top = SpaceLg), - shape = ShapeLg, - color = colors.tonal, + .border(1.dp, CardBorderBrush, ShapeCard), + shape = ShapeCard, + color = CardBg, ) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = SpaceLg), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, + .padding(horizontal = SpaceXxl, vertical = SpaceLg), + verticalArrangement = Arrangement.Center, ) { - IconButton( - onClick = { - event.customActions.firstOrNull()?.let { - interactor.sendCustomAction(it.action) - } - }, - modifier = Modifier.size(SizeButtonLg), - ) { - val ca = event.customActions.firstOrNull() - if (ca != null) { - CustomActionIcon(ca, tint = SubtleGray, modifier = Modifier.size(SpacePanel)) - } else { - Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) - } - } - - IconButton( - onClick = { interactor.skipPrev() }, - modifier = Modifier.size(SizeButtonLg), - ) { - Icon(Icons.Filled.SkipPrevious, stringResource(R.string.ax_dynamic_bar_previous), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) - } - - Surface( - onClick = { interactor.togglePlayPause() }, - modifier = Modifier.size(SizeButtonXl), - shape = CircleShape, - color = colors.accent, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Icon( - if (event.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, - if (event.isPlaying) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_play), - tint = colors.onAccent, - modifier = Modifier.size(32.dp), + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceMd), + ) { + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(colors.accent), + ) + } ?: Icon( + Icons.Filled.MusicNote, + null, + tint = colors.accent, + modifier = Modifier.size(SizeIconSm), + ) + Text( + event.outputDeviceName.ifBlank { + stringResource(R.string.ax_dynamic_bar_now_playing) + }, + color = OnCardText, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } - } - - IconButton( - onClick = { interactor.skipNext() }, - modifier = Modifier.size(SizeButtonLg), - ) { - Icon(Icons.Filled.SkipNext, stringResource(R.string.ax_dynamic_bar_next), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) - } - - IconButton( - onClick = { - val ca = event.customActions.getOrNull(1) - if (ca != null) interactor.sendCustomAction(ca.action) - else { + Surface( + onClick = { interactor.openMediaOutputSwitcher() interactor.collapseIsland() + }, + shape = ShapeChip, + color = colors.accent.copy(alpha = AlphaSubtle), + ) { + Row( + modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceSm), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Icon( + Icons.Filled.VolumeUp, + stringResource(R.string.ax_dynamic_bar_output), + tint = colors.accent, + modifier = Modifier.size(SizeIconSm), + ) } - }, - modifier = Modifier.size(SizeButtonLg), - ) { - val ca = event.customActions.getOrNull(1) - if (ca != null) { - CustomActionIcon(ca, tint = SubtleGray, modifier = Modifier.size(SpacePanel)) - } else { - Icon(Icons.Filled.VolumeUp, stringResource(R.string.ax_dynamic_bar_output), tint = SubtleGray, modifier = Modifier.size(SpacePanel)) } } - } - } - } -} -} - -@Composable -private fun KeyguardMediaSeekBar( - event: IslandEvent.Media, - interactor: IslandActions, - accent: Color, - useWaveform: Boolean = false, -) { - val mediaProgress = rememberMediaProgress(event) - val isPlaying = event.isPlaying - val durationMs = event.duration - val positionMs = mediaProgress.positionMs - val serverFraction = mediaProgress.progress - - var isScrubbing by remember { mutableStateOf(false) } - var displayFraction by remember { mutableStateOf(serverFraction) } - - val interactorRef = rememberUpdatedState(interactor) - - // Read the dismiss swipe lock provided by MagneticSwipeToDismiss - val swipeLock = LocalDismissSwipeLock.current - - // Smooth frame-interpolated progress - LaunchedEffect(positionMs, durationMs, isPlaying) { - if (isScrubbing) return@LaunchedEffect - - displayFraction = serverFraction - - if (!isPlaying || durationMs <= 0L) return@LaunchedEffect - - val startWallMs = System.currentTimeMillis() - val startProgressMs = positionMs - while (true) { - delay(16L) - if (isScrubbing) break - val elapsed = System.currentTimeMillis() - startWallMs - val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f, 1f) - displayFraction = interpolated - if (interpolated >= 1f) break - } - } - - val displayMs = (displayFraction * durationMs).toLong() - val accentArgb = accent.toArgb() - val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb() - Column(verticalArrangement = Arrangement.spacedBy(SpaceSm)) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(KeyguardSeekBarHeight) - .pointerInput(swipeLock) { - awaitEachGesture { - awaitPointerEvent() - swipeLock.value = true - try { - do { - val event = awaitPointerEvent() - } while (event.changes.any { it.pressed }) - } finally { - swipeLock.value = false - } + if (event.duration > 0L) { + Spacer(Modifier.height(SpaceMd)) + val mediaProgress = rememberMediaProgress(event) + var isSeeking by remember { mutableStateOf(false) } + var seekProgress by remember { mutableFloatStateOf(mediaProgress.progress) } + if (!isSeeking) seekProgress = mediaProgress.progress + val displayMs = + if (isSeeking) (seekProgress * event.duration).toLong() + else mediaProgress.positionMs + Box( + modifier = Modifier + .fillMaxWidth() + .height(SpaceSection) + .pointerInput("tap") { + detectTapGestures { offset -> + val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + seekProgress = f + interactor.seekTo((f * event.duration).toLong()) + } + } + .pointerInput("drag") { + detectHorizontalDragGestures( + onDragStart = { offset -> + isSeeking = true + seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + isSeeking = false + interactor.seekTo((seekProgress * event.duration).toLong()) + }, + onDragCancel = { isSeeking = false }, + onHorizontalDrag = { change, _ -> + seekProgress = (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) + change.consume() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + LinearWavyProgressIndicator( + progress = { seekProgress }, + modifier = Modifier.fillMaxWidth(), + color = colors.accent, + trackColor = colors.accent.copy(alpha = AlphaSubtle), + amplitude = { if (event.isPlaying) 1f else 0f }, + ) } - } - .pointerInput("tap") { - detectTapGestures { offset -> - val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - displayFraction = f - interactorRef.value.seekTo((f * durationMs).toLong()) + Spacer(Modifier.height(SpaceXs)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(formatElapsedTime(displayMs), color = OnCardSecondary, style = MaterialTheme.typography.labelSmall) + Text(formatElapsedTime(event.duration), color = OnCardSecondary, style = MaterialTheme.typography.labelSmall) } } - .pointerInput("drag") { - detectHorizontalDragGestures( - onDragStart = { offset -> - isScrubbing = true - displayFraction = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - }, - onDragEnd = { - interactorRef.value.seekTo((displayFraction * durationMs).toLong()) - isScrubbing = false - }, - onDragCancel = { isScrubbing = false }, - onHorizontalDrag = { change, _ -> - displayFraction = - (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) - change.consume() + + Spacer(Modifier.height(SpaceMd)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { + event.customActions.firstOrNull()?.let { + interactor.sendCustomAction(it.action) + } }, - ) - }, - contentAlignment = Alignment.Center, - ) { - if (useWaveform) { - AndroidView( - factory = { context -> - WaveformSeekBar(context).apply { - max = 10_000 - setWaveformColor(accentArgb) - setThumbColor(accentArgb) - isEnabled = false - } - }, - update = { bar -> - val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) - if (bar.progress != target) bar.progress = target - when { - isPlaying && !bar.isPlaying -> bar.startWaveAnimation() - !isPlaying && bar.isPlaying -> bar.stopWaveAnimation() + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.firstOrNull() + if (ca != null) { + CustomActionIcon(ca, tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) + } else { + Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) } - }, - modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), - ) - } else { - AndroidView( - factory = { context -> - SeekBar(context).apply { - max = 10_000 - splitTrack = false - setPadding(0, 0, 0, 0) - isEnabled = false + } - thumb = createKeyguardSeekBarThumb(context, accentArgb) - thumbOffset = thumb.intrinsicWidth / 2 + Spacer(Modifier.width(SpaceSm)) - val layer = (progressDrawable?.mutate() as? LayerDrawable) - if (layer != null) { - layer.findDrawableByLayerId(android.R.id.background) - ?.mutate()?.setTint(trackAlphaArgb) - layer.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.mutate()?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) - val squiggle = SquigglyProgress().apply { - waveLength = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_wavelength - ).toFloat() - lineAmplitude = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_amplitude - ).toFloat() - phaseSpeed = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_phase - ).toFloat() - strokeWidth = context.resources.getDimensionPixelSize( - R.dimen.qs_media_seekbar_progress_stroke_width - ).toFloat() - setTint(accentArgb) - drawRemainingLine = false - transitionEnabled = false - animate = false - } - layer.setDrawableByLayerId(android.R.id.progress, squiggle) - progressDrawable = layer + IconButton( + onClick = { interactor.skipPrev() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon(Icons.Filled.SkipPrevious, stringResource(R.string.ax_dynamic_bar_previous), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) + } + + Spacer(Modifier.width(SpaceSm)) + + Surface( + onClick = { interactor.togglePlayPause() }, + modifier = Modifier.size(SizeButtonLg), + shape = CircleShape, + color = colors.accent, + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + AnimatedContent( + targetState = event.isPlaying, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) + }, + label = "kg_media_playpause", + ) { playing -> + Icon( + if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, + if (playing) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_play), + tint = colors.onAccent, + modifier = Modifier.size(SizeIconMd), + ) } } - }, - update = { bar -> - val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000) - bar.progress = target - - (bar.thumb as? GradientDrawable)?.setColor(accentArgb) + } - val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt() - bar.thumb?.alpha = alpha + Spacer(Modifier.width(SpaceSm)) - val layer = bar.progressDrawable as? LayerDrawable + IconButton( + onClick = { interactor.skipNext() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon(Icons.Filled.SkipNext, stringResource(R.string.ax_dynamic_bar_next), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) + } - // Re-tint track colors - layer?.findDrawableByLayerId(android.R.id.background) - ?.setTint(trackAlphaArgb) - layer?.findDrawableByLayerId(android.R.id.secondaryProgress) - ?.setTint( - com.android.internal.graphics.ColorUtils - .setAlphaComponent(accentArgb, 60) - ) + Spacer(Modifier.width(SpaceSm)) - val squiggle = layer - ?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress - squiggle?.apply { - setTint(accentArgb) - setAlpha(alpha) - animate = isPlaying && !isScrubbing + IconButton( + onClick = { + event.customActions.getOrNull(1)?.let { + interactor.sendCustomAction(it.action) + } + }, + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.getOrNull(1) + if (ca != null) { + CustomActionIcon(ca, tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) + } else { + Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) } - layer?.alpha = alpha - }, - modifier = Modifier.fillMaxWidth().height(KeyguardSeekBarHeight), - ) + } + } } } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(formatElapsedTime(displayMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - Text(formatElapsedTime(durationMs), color = SubtleGray, style = MaterialTheme.typography.labelSmall) - } - } -} - -private fun createKeyguardSeekBarThumb(context: android.content.Context, tintColor: Int): GradientDrawable { - val wPx = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 4f, context.resources.displayMetrics - ).toInt().coerceAtLeast(1) - val hPx = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics - ).toInt().coerceAtLeast(1) - val radiusPx = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics - ) - return GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - setSize(wPx, hPx) - cornerRadius = radiusPx - setColor(tintColor) } } @@ -828,93 +699,6 @@ private fun KeyguardStopwatchPanel(event: IslandEvent.Stopwatch, interactor: Isl } } -@Composable -private fun KeyguardRecordingPanel(event: IslandEvent.ScreenRecording, interactor: IslandActions) { - val colors = rememberIslandColors(event) - - if (event.isCountdown) { - KeyguardPanelSurface { Column( - modifier = Modifier - .fillMaxWidth() - .padding(SpaceSection), - verticalArrangement = Arrangement.spacedBy(SpaceXxl), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - TonalBanner(colors) { - Icon(Icons.Filled.Videocam, null, tint = colors.accent, modifier = Modifier.size(SizeIconSm)) - Text( - stringResource(R.string.ax_dynamic_bar_screen_recording).uppercase(), - color = colors.accent, - style = MaterialTheme.typography.labelMedium, - ) - } - - Text( - event.countdownSeconds.toString(), - color = OnCardText, - style = MaterialTheme.typography.displayLarge, - ) - - Text( - stringResource(R.string.ax_dynamic_bar_screen_recording), - color = SubtleGray, - style = MaterialTheme.typography.bodyMedium, - ) - } } - return - } - - var elapsedMs by remember(event.startTimeMs) { - mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) - } - LaunchedEffect(event.startTimeMs) { - while (true) { - delay(1000) - elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) - } - } - - KeyguardPanelSurface { Column( - modifier = Modifier - .fillMaxWidth() - .padding(SpaceSection), - verticalArrangement = Arrangement.spacedBy(SpaceXxl), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - TonalBanner(colors) { - PulsingDot(color = colors.accent, size = SpaceMd) - Icon(Icons.Filled.Videocam, null, tint = colors.accent, modifier = Modifier.size(SizeIconSm)) - Text( - stringResource(R.string.ax_dynamic_bar_screen_recording).uppercase(), - color = colors.accent, - style = MaterialTheme.typography.labelMedium, - ) - } - - Text( - formatElapsedTime(elapsedMs), - color = OnCardText, - style = MaterialTheme.typography.displayLarge, - ) - - LinearWavyProgressIndicator( - modifier = Modifier.fillMaxWidth().clip(ShapeChip), - color = colors.accent, - trackColor = colors.accent.copy(alpha = AlphaFaint), - ) - - ExpressivePillButton( - label = stringResource(R.string.ax_dynamic_bar_stop), - icon = Icons.Filled.Stop, - contentColor = colors.onAccent, - backgroundColor = colors.accent, - modifier = Modifier.fillMaxWidth(), - onClick = { interactor.stopScreenRecording() }, - ) - } -} -} - @Composable private fun KeyguardAudioRecordingPanel(event: IslandEvent.AudioRecording, interactor: IslandActions) { val context = LocalContext.current @@ -1012,21 +796,21 @@ private fun KeyguardGenericPanel( hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) { val colors = rememberIslandColors(event) - KeyguardPanelSurface { Column( - modifier = Modifier - .fillMaxWidth() - .padding(SpaceSection), - verticalArrangement = Arrangement.spacedBy(SpaceXxl), - ) { - ExpandedEventContent(event, interactor, hapticsViewModelFactory) + KeyguardPanelSurface { Column( + modifier = Modifier + .fillMaxWidth() + .padding(SpaceSection), + verticalArrangement = Arrangement.spacedBy(SpaceXxl), + ) { + ExpandedEventContent(event, interactor, hapticsViewModelFactory) - ExpressivePillButton( - label = stringResource(R.string.ax_dynamic_bar_dismiss), - contentColor = colors.onAccent, - backgroundColor = colors.accent, - modifier = Modifier.fillMaxWidth(), - onClick = { interactor.dismissEvent(event) }, - ) + ExpressivePillButton( + label = stringResource(R.string.ax_dynamic_bar_dismiss), + contentColor = colors.onAccent, + backgroundColor = colors.accent, + modifier = Modifier.fillMaxWidth(), + onClick = { interactor.dismissEvent(event) }, + ) + } } -} -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt deleted file mode 100644 index 9e2aef4f33c5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/NotificationAlertCard.kt +++ /dev/null @@ -1,848 +0,0 @@ -package com.android.systemui.axdynamicbar.ui.compose - -import android.app.Notification -import android.app.PendingIntent -import android.app.RemoteInput -import android.content.Intent -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.util.Log -import androidx.compose.animation.core.spring -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -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.shape.CircleShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Reply -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.Call -import androidx.compose.material.icons.filled.CallEnd -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.android.compose.animation.scene.ContentScope -import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.SceneKey -import com.android.compose.animation.scene.SceneTransitionLayout -import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState -import com.android.compose.animation.scene.transitions -import com.android.systemui.axdynamicbar.shared.IslandActions -import com.android.systemui.axdynamicbar.model.IslandEvent -import com.android.systemui.axdynamicbar.shared.AlphaIconBg -import com.android.systemui.axdynamicbar.shared.AlphaSubtle -import com.android.systemui.axdynamicbar.shared.CardBg -import com.android.systemui.axdynamicbar.shared.CardBorderBrush -import com.android.systemui.axdynamicbar.shared.DarkCard -import com.android.systemui.axdynamicbar.shared.ExpressivePillButton -import com.android.systemui.axdynamicbar.shared.GreenAccent -import com.android.systemui.axdynamicbar.shared.OnCardText -import com.android.systemui.axdynamicbar.shared.RedAccent -import com.android.systemui.axdynamicbar.shared.ShapeCard -import com.android.systemui.axdynamicbar.shared.ShapeChip -import com.android.systemui.axdynamicbar.shared.ShapeIconMedium -import com.android.systemui.axdynamicbar.shared.ShapeSm -import com.android.systemui.axdynamicbar.shared.SizeCompactIcon -import com.android.systemui.axdynamicbar.shared.SizeIconSm -import com.android.systemui.axdynamicbar.shared.SpaceLg -import com.android.systemui.axdynamicbar.shared.SpaceMd -import com.android.systemui.axdynamicbar.shared.SpaceSm -import com.android.systemui.axdynamicbar.shared.SpaceXs -import com.android.systemui.axdynamicbar.shared.SpaceXxl -import com.android.systemui.axdynamicbar.shared.SubtleGray -import com.android.systemui.axdynamicbar.shared.chipAccentColorFor -import com.android.systemui.axdynamicbar.shared.chipContentColorOn -import com.android.systemui.axdynamicbar.shared.sendWithBal -import com.android.systemui.axdynamicbar.shared.toScaledBitmap - -private const val MAX_EXPANDED_ACTIONS = 3 -private val ThumbnailSize = 44.dp - -private object AlertCardScenes { - val Compact = SceneKey("AlertCompact") - val Collapsed = SceneKey("AlertCollapsed") - val Expanded = SceneKey("AlertExpanded") -} - -private object AlertCardElements { - val AppIcon = ElementKey("AlertAppIcon") - val CompactLabel = ElementKey("AlertCompactLabel") - val CompactExpand = ElementKey("AlertCompactExpand") - val Avatar = ElementKey("AlertAvatar") - val SenderName = ElementKey("AlertSenderName") - val CardContent = ElementKey("AlertCardContent") - val CardExpand = ElementKey("AlertCardExpand") -} - -private const val SPRING_STIFFNESS = 500f -private const val SPRING_DAMPING = 0.9f - -private val alertCardTransitions = transitions { - from(AlertCardScenes.Compact, to = AlertCardScenes.Expanded) { - spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) - sharedElement(AlertCardElements.AppIcon) - fade(AlertCardElements.CompactLabel) - fade(AlertCardElements.CompactExpand) - fade(AlertCardElements.CardContent) - fade(AlertCardElements.CardExpand) - } - from(AlertCardScenes.Compact, to = AlertCardScenes.Collapsed) { - spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) - sharedElement(AlertCardElements.AppIcon) - fade(AlertCardElements.CompactLabel) - fade(AlertCardElements.CompactExpand) - fade(AlertCardElements.CardContent) - fade(AlertCardElements.CardExpand) - } - from(AlertCardScenes.Collapsed, to = AlertCardScenes.Expanded) { - spec = spring(stiffness = SPRING_STIFFNESS, dampingRatio = SPRING_DAMPING) - sharedElement(AlertCardElements.AppIcon) - sharedElement(AlertCardElements.Avatar) - sharedElement(AlertCardElements.SenderName) - sharedElement(AlertCardElements.CardExpand) - } -} - -private suspend fun PointerInputScope.detectInteractionGesture( - onStart: () -> Unit, - onEnd: () -> Unit, -) { - awaitEachGesture { - var ev: PointerEvent - do { - ev = awaitPointerEvent(PointerEventPass.Final) - } while (!ev.changes.any { it.changedToDownIgnoreConsumed() }) - onStart() - do { - ev = awaitPointerEvent(PointerEventPass.Final) - } while (ev.changes.any { it.pressed }) - onEnd() - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun NotificationAlertCard( - notification: IslandEvent.Notification, - interactor: IslandActions, - onDismiss: () -> Unit, - initiallyCompact: Boolean = false, - modifier: Modifier = Modifier, -) { - val extras = notification.sbn.notification?.extras - val isCall = extras?.let { - it.containsKey(Notification.EXTRA_ANSWER_INTENT) || - it.containsKey(Notification.EXTRA_DECLINE_INTENT) || - it.containsKey(Notification.EXTRA_HANG_UP_INTENT) - } ?: false - val title = notification.title - val body = notification.text - val hasTitleAndBody = title != null && title != notification.senderName && !body.isNullOrEmpty() - val hasExtraActions = notification.actions.size > 2 - val hasExpandableContent = !isCall && (hasTitleAndBody || hasExtraActions) - val accent = chipAccentColorFor(notification) - var showReply by remember(notification.sbn.key) { mutableStateOf(false) } - val scope = rememberCoroutineScope() - - val initialScene = if (initiallyCompact && !isCall) AlertCardScenes.Compact - else AlertCardScenes.Expanded - val stlState = rememberMutableSceneTransitionLayoutState( - initialScene = initialScene, - transitions = alertCardTransitions, - ) - - DisposableEffect(Unit) { - onDispose { interactor.onNotificationAlertInteractionEnd() } - } - - MagneticSwipeToDismiss( - onDismiss = { - if (showReply) interactor.onFocusableRequested?.invoke(false) - onDismiss() - }, - modifier = modifier, - allowUpDismiss = true, - ) { - Column( - modifier = Modifier.fillMaxWidth() - .pointerInput(Unit) { - detectInteractionGesture( - onStart = interactor::onNotificationAlertInteractionStart, - onEnd = interactor::onNotificationAlertInteractionEnd, - ) - } - .clip(ShapeCard) - .background(CardBg) - .border(1.dp, CardBorderBrush, ShapeCard), - ) { - SceneTransitionLayout(state = stlState) { - scene(AlertCardScenes.Compact) { - CompactScene( - notification = notification, - accent = accent, - onExpand = { - stlState.setTargetScene(AlertCardScenes.Expanded, scope) - }, - ) - } - scene(AlertCardScenes.Collapsed) { - CardScene( - notification = notification, - accent = accent, - expanded = false, - isCall = isCall, - initiallyCompact = initiallyCompact, - hasExpandableContent = hasExpandableContent, - hasTitleAndBody = hasTitleAndBody, - showReply = showReply, - interactor = interactor, - onDismiss = onDismiss, - onExpandToggle = { - stlState.setTargetScene(AlertCardScenes.Expanded, scope) - }, - onCollapseToCompact = { - stlState.setTargetScene(AlertCardScenes.Compact, scope) - }, - onShowReply = { showReply = true }, - onSentReply = { showReply = false; onDismiss() }, - ) - } - scene(AlertCardScenes.Expanded) { - CardScene( - notification = notification, - accent = accent, - expanded = true, - isCall = isCall, - initiallyCompact = initiallyCompact, - hasExpandableContent = hasExpandableContent, - hasTitleAndBody = hasTitleAndBody, - showReply = showReply, - interactor = interactor, - onDismiss = onDismiss, - onExpandToggle = { - val target = if (initiallyCompact) AlertCardScenes.Compact - else AlertCardScenes.Collapsed - stlState.setTargetScene(target, scope) - }, - onCollapseToCompact = { - stlState.setTargetScene(AlertCardScenes.Compact, scope) - }, - onShowReply = { showReply = true }, - onSentReply = { showReply = false; onDismiss() }, - ) - } - } - } - } -} - -@Composable -private fun ContentScope.CompactScene( - notification: IslandEvent.Notification, - accent: Color, - onExpand: () -> Unit, -) { - Surface( - onClick = onExpand, - color = Color.Transparent, - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = SpaceXxl, vertical = SpaceMd), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceSm), - ) { - notification.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier - .element(AlertCardElements.AppIcon) - .size(SizeIconSm) - .clip(ShapeSm), - ) - } - Row( - modifier = Modifier - .element(AlertCardElements.CompactLabel) - .weight(1f), - horizontalArrangement = Arrangement.spacedBy(SpaceSm), - verticalAlignment = Alignment.CenterVertically, - ) { - val label = notification.senderName - ?: notification.title - ?: notification.appName - Text( - label, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - val preview = notification.text ?: notification.title ?: "" - if (preview.isNotEmpty() && preview != label) { - Text( - "· $preview", - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - } else { - Spacer(Modifier.weight(1f)) - } - } - ExpandChip( - Icons.Filled.ExpandMore, - accent, - onExpand, - Modifier.element(AlertCardElements.CompactExpand), - ) - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun ContentScope.CardScene( - notification: IslandEvent.Notification, - accent: Color, - expanded: Boolean, - isCall: Boolean, - initiallyCompact: Boolean, - hasExpandableContent: Boolean, - hasTitleAndBody: Boolean, - showReply: Boolean, - interactor: IslandActions, - onDismiss: () -> Unit, - onExpandToggle: () -> Unit, - onCollapseToCompact: () -> Unit, - onShowReply: () -> Unit, - onSentReply: () -> Unit, -) { - val context = LocalContext.current - val title = notification.title - val body = notification.text - - Column(modifier = Modifier.element(AlertCardElements.CardContent).fillMaxWidth()) { - Surface( - onClick = { - try { notification.sbn.notification?.contentIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - if (showReply) interactor.onFocusableRequested?.invoke(false) - if (!isCall) onDismiss() - }, - color = Color.Transparent, - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(SpaceXxl), - verticalArrangement = Arrangement.spacedBy(SpaceLg), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceSm), - ) { - notification.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier - .element(AlertCardElements.AppIcon) - .size(SizeIconSm) - .clip(ShapeSm), - ) - } - Text( - if (notification.isGroupConversation && notification.conversationTitle != null) - "${notification.appName} · ${notification.conversationTitle}" - else - notification.appName.ifEmpty { notification.senderName ?: "" }, - color = accent, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - if (isCall) { - CallActions(notification, interactor, onDismiss) - } - if (!showReply && !isCall) { - val expandMod = Modifier.element(AlertCardElements.CardExpand) - if (initiallyCompact) { - ExpandChip(Icons.Filled.ExpandLess, accent, onCollapseToCompact, expandMod) - } else if (hasExpandableContent) { - ExpandChip( - if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, - accent, - onExpandToggle, - expandMod, - ) - } - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(SpaceLg), - ) { - NotifAvatar( - notification, - accent, - SizeCompactIcon, - Modifier.element(AlertCardElements.Avatar), - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(SpaceXs), - ) { - val sender = notification.senderName ?: notification.appName - Text( - sender, - color = OnCardText, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.element(AlertCardElements.SenderName), - ) - - if (expanded) { - if (hasTitleAndBody) { - Text( - title!!, - color = OnCardText, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - body!!, - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 8, - overflow = TextOverflow.Ellipsis, - ) - } else { - val preview = body ?: title ?: "" - if (preview.isNotEmpty()) { - Text( - preview, - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 10, - overflow = TextOverflow.Ellipsis, - ) - } - } - } else { - val preview = body ?: title ?: "" - if (preview.isNotEmpty()) { - Text( - preview, - color = SubtleGray, - style = MaterialTheme.typography.bodySmall, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - - notification.notificationImage?.let { img -> - Image( - bitmap = img.toScaledBitmap(ThumbnailSize), - contentDescription = null, - modifier = Modifier.size(ThumbnailSize).clip(ShapeSm), - contentScale = ContentScale.Crop, - ) - } - } - } - } - - val hasReply = notification.replyAction != null - val hasActions = notification.actions.isNotEmpty() || hasReply - if (!isCall && !showReply && hasActions) { - Box( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = SpaceXxl) - .padding(bottom = SpaceXxl), - ) { - if (expanded) { - ExpandedActions(notification, accent, onDismiss, onShowReply) - } else { - CollapsedActions(notification, accent, onDismiss, onShowReply) - } - } - } - - if (showReply && notification.replyAction != null) { - ReplyField( - reply = notification.replyAction!!, - accent = accent, - interactor = interactor, - onSent = onSentReply, - ) - } - } -} - -@Composable -private fun NotifAvatar( - notification: IslandEvent.Notification, - accent: Color, - size: Dp, - modifier: Modifier = Modifier, -) { - val icon = notification.senderIcon ?: notification.appIcon - val isRound = notification.isConversation && notification.senderIcon != null - - if (notification.isConversation && notification.senderIcon != null && notification.appIcon != null) { - Box(modifier = modifier) { - BadgedContactIcon(icon!!, notification.appIcon!!, size, (size.value * 0.44f).dp, true) - } - } else if (icon != null) { - Box( - modifier = modifier.size(size).clip(if (isRound) CircleShape else ShapeIconMedium), - contentAlignment = Alignment.Center, - ) { - Image( - bitmap = icon.toScaledBitmap(size), - contentDescription = null, - modifier = Modifier.size(size), - contentScale = ContentScale.Crop, - ) - } - } else { - Box( - modifier = modifier.size(size).clip(ShapeIconMedium).background(accent.copy(alpha = AlphaSubtle)), - contentAlignment = Alignment.Center, - ) { - Icon(Icons.Filled.Notifications, null, tint = accent, modifier = Modifier.size(SizeIconSm)) - } - } -} - -@Composable -private fun CollapsedActions( - notification: IslandEvent.Notification, - accent: Color, - onDismiss: () -> Unit, - onReplyClick: () -> Unit, -) { - val context = LocalContext.current - val hasReply = notification.replyAction != null - val visibleActions = notification.actions.take(2) - if (visibleActions.isEmpty() && !hasReply) return - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(SpaceMd), - ) { - if (hasReply) { - ExpressivePillButton( - label = notification.replyAction!!.label.toString(), - icon = Icons.AutoMirrored.Filled.Reply, - contentColor = chipContentColorOn(accent), - backgroundColor = accent, - modifier = Modifier.weight(1f), - onClick = onReplyClick, - ) - } - visibleActions.take(if (hasReply) 1 else 2).forEach { action -> - ExpressivePillButton( - label = action.label.toString(), - contentColor = accent, - backgroundColor = accent.copy(alpha = AlphaIconBg), - modifier = Modifier.weight(1f), - onClick = { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - }, - ) - } - } -} - -@Composable -private fun CallActions( - notification: IslandEvent.Notification, - interactor: IslandActions, - onDismiss: () -> Unit, -) { - val context = LocalContext.current - val extras = notification.sbn.notification?.extras - val isIncoming = extras?.containsKey(Notification.EXTRA_ANSWER_INTENT) == true - val answerPi = extras?.getParcelable(Notification.EXTRA_ANSWER_INTENT, PendingIntent::class.java) - val declinePi = extras?.getParcelable(Notification.EXTRA_DECLINE_INTENT, PendingIntent::class.java) - val hangUpPi = extras?.getParcelable(Notification.EXTRA_HANG_UP_INTENT, PendingIntent::class.java) - - Row(horizontalArrangement = Arrangement.spacedBy(SpaceMd)) { - if (isIncoming) { - val answerAction = notification.actions.firstOrNull { a -> - answerPi != null && a.action.actionIntent == answerPi - } - val declineAction = notification.actions.firstOrNull { a -> - val pi = a.action.actionIntent - (declinePi != null && pi == declinePi) || (hangUpPi != null && pi == hangUpPi) - } - declineAction?.let { action -> - ActionCircle(Icons.Filled.CallEnd, RedAccent, chipContentColorOn(RedAccent)) { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - } - } - answerAction?.let { action -> - ActionCircle(Icons.Filled.Call, GreenAccent, chipContentColorOn(GreenAccent)) { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - } - } - } else { - val endAction = if (hangUpPi != null) { - notification.actions.firstOrNull { it.action.actionIntent == hangUpPi } - } else { - notification.actions.firstOrNull { a -> - val semantic = a.action.semanticAction - semantic != Notification.Action.SEMANTIC_ACTION_MUTE && - semantic != Notification.Action.SEMANTIC_ACTION_UNMUTE - } - } - endAction?.let { action -> - val drawable = action.action.getIcon()?.loadDrawable(context) - if (drawable != null) { - DrawableActionCircle(drawable, RedAccent, chipContentColorOn(RedAccent)) { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - } - } else { - ActionCircle(Icons.Filled.CallEnd, RedAccent, chipContentColorOn(RedAccent)) { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - } - } - } - } - } -} - -@Composable -private fun ActionCircle( - icon: ImageVector, - bg: Color, - tint: Color, - onClick: () -> Unit, -) { - Surface(onClick = onClick, shape = CircleShape, color = bg, modifier = Modifier.size(36.dp)) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { - Icon(icon, null, tint = tint, modifier = Modifier.size(18.dp)) - } - } -} - -@Composable -private fun ExpandChip( - icon: ImageVector, - accent: Color, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Surface( - onClick = onClick, - shape = CircleShape, - color = accent.copy(alpha = AlphaIconBg), - modifier = modifier.size(36.dp), - ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { - Icon(icon, contentDescription = null, tint = accent, modifier = Modifier.size(18.dp)) - } - } -} - -@Composable -private fun DrawableActionCircle( - drawable: Drawable, - bg: Color, - tint: Color, - onClick: () -> Unit, -) { - Surface(onClick = onClick, shape = CircleShape, color = bg, modifier = Modifier.size(36.dp)) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(36.dp)) { - Image( - bitmap = drawable.toScaledBitmap(18.dp), - contentDescription = null, - modifier = Modifier.size(18.dp), - colorFilter = ColorFilter.tint(tint), - ) - } - } -} - -@Composable -private fun ExpandedActions( - notification: IslandEvent.Notification, - accent: Color, - onDismiss: () -> Unit, - onReplyClick: () -> Unit, -) { - val context = LocalContext.current - val hasReply = notification.replyAction != null - val visibleActions = notification.actions.take(MAX_EXPANDED_ACTIONS) - if (visibleActions.isEmpty() && !hasReply) return - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(SpaceMd), - ) { - if (hasReply) { - ExpressivePillButton( - label = notification.replyAction!!.label.toString(), - icon = Icons.AutoMirrored.Filled.Reply, - contentColor = chipContentColorOn(accent), - backgroundColor = accent, - modifier = Modifier.weight(1f), - onClick = onReplyClick, - ) - } - visibleActions.forEach { action -> - ExpressivePillButton( - label = action.label.toString(), - contentColor = accent, - backgroundColor = accent.copy(alpha = AlphaIconBg), - modifier = Modifier.weight(1f), - onClick = { - try { action.action.actionIntent?.sendWithBal(context) } - catch (_: PendingIntent.CanceledException) {} - onDismiss() - }, - ) - } - } -} - -@Composable -private fun ReplyField( - reply: IslandEvent.ReplyAction, - accent: Color, - interactor: IslandActions, - onSent: () -> Unit, -) { - var replyText by remember { mutableStateOf("") } - val context = LocalContext.current - val focusRequester = remember { FocusRequester() } - - LaunchedEffect(Unit) { - interactor.onFocusableRequested?.invoke(true) - focusRequester.requestFocus() - } - - Row( - modifier = Modifier.fillMaxWidth() - .padding(horizontal = SpaceLg, vertical = SpaceMd) - .height(40.dp) - .clip(ShapeChip) - .background(DarkCard), - verticalAlignment = Alignment.CenterVertically, - ) { - BasicTextField( - value = replyText, - onValueChange = { replyText = it }, - modifier = Modifier.weight(1f) - .padding(horizontal = SpaceLg) - .focusRequester(focusRequester) - .onFocusChanged { interactor.onFocusableRequested?.invoke(it.isFocused) }, - textStyle = MaterialTheme.typography.bodySmall.copy(color = OnCardText), - singleLine = true, - cursorBrush = SolidColor(accent), - decorationBox = { inner -> - if (replyText.isEmpty()) { - Text(reply.label.toString(), color = SubtleGray, style = MaterialTheme.typography.bodySmall) - } - inner() - }, - ) - if (replyText.isNotEmpty()) { - Surface( - onClick = { - val intent = Intent().apply { - val results = Bundle().apply { - putCharSequence(reply.remoteInput.resultKey, replyText) - } - RemoteInput.addResultsToIntent(arrayOf(reply.remoteInput), this, results) - } - try { reply.action.actionIntent?.sendWithBal(context, intent) } - catch (e: Exception) { Log.w("NotificationReply", "Failed to send reply", e) } - replyText = "" - interactor.onFocusableRequested?.invoke(false) - onSent() - }, - shape = CircleShape, - color = accent, - modifier = Modifier.size(32.dp).padding(SpaceXs), - ) { - Box(contentAlignment = Alignment.Center) { - Icon(Icons.AutoMirrored.Filled.Send, null, tint = chipContentColorOn(accent), modifier = Modifier.size(14.dp)) - } - } - Spacer(Modifier.width(SpaceXs)) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index 517755e2f942..b5ac90eb1bf0 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -2,6 +2,13 @@ package com.android.systemui.axdynamicbar.ui.compose import android.graphics.drawable.Drawable import android.media.AudioManager +import android.os.SystemClock +import androidx.compose.ui.platform.LocalContext +import java.text.NumberFormat +import com.android.internal.R as InternalR +import com.android.systemui.common.shared.model.Icon as SysUISharedIcon +import com.android.systemui.common.ui.compose.Icon as SysUIIcon +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode @@ -26,8 +33,11 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.FlashlightOn import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MicOff import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -61,6 +71,7 @@ import androidx.compose.ui.res.stringResource import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.model.RecordingState import com.android.systemui.axdynamicbar.shared.* +import androidx.compose.ui.graphics.graphicsLayer import com.android.systemui.res.R import kotlin.math.PI import kotlin.math.cos @@ -71,11 +82,7 @@ import kotlinx.coroutines.delay @Composable internal fun PillEventIcon(event: IslandEvent, tint: Color? = null) { when (event) { - is IslandEvent.ScreenRecording -> BlinkingDotIcon(tint ?: RedAccent, isAnimating = !event.isCountdown) - is IslandEvent.MicCamActive -> PrivacyDotIcon(event, tint) - is IslandEvent.AudioRecording -> - BlinkingDotIcon(tint ?: RedAccent, isAnimating = event.state == RecordingState.RECORDING) - is IslandEvent.Casting -> AnimatedCastIcon(tint ?: TealAccent) + is IslandEvent.AudioRecording -> AudioRecordingPillIcon(event, tint) is IslandEvent.Media -> MediaPillIcon(event) is IslandEvent.PromotedOngoing -> PromotedOngoingPillIcon(event, tint) is IslandEvent.Sports -> SportsPillIcon(event) @@ -95,9 +102,114 @@ internal fun PillEventIcon(event: IslandEvent, tint: Color? = null) { Icon(Icons.Filled.FlashlightOn, null, tint = tint ?: YellowAccent, modifier = Modifier.size(SizeBadge)) is IslandEvent.BiometricUnlock -> BiometricUnlockIcon(tint) is IslandEvent.KeyguardIndication -> KeyguardIndicationIcon(event, tint) + is IslandEvent.AospChip -> AospChipPillIcon(event, tint) } } +@Composable +private fun AospChipPillIcon(event: IslandEvent.AospChip, tint: Color? = null) { + val color = tint ?: aospChipAccent(event.active) + val context = LocalContext.current + val isCountdown = event.active.content is OngoingActivityChipModel.Content.Countdown + val isIconOnly = event.active.content is OngoingActivityChipModel.Content.IconOnly + val isCall = event.active.key.startsWith("callChip-") + val useScreenRecFallback = isCountdown && event.active.key == "ScreenRecord" + + val renderIcon: OngoingActivityChipModel.ChipIcon = when { + isCall -> OngoingActivityChipModel.ChipIcon.SingleColorIcon( + SysUISharedIcon.Resource(InternalR.drawable.ic_phone, null) + ) + event.active.icon != null -> event.active.icon!! + useScreenRecFallback -> OngoingActivityChipModel.ChipIcon.SingleColorIcon( + SysUISharedIcon.Resource(R.drawable.ic_screenrecord, null) + ) + else -> return + } + + val iconContent: @Composable () -> Unit = { + when (renderIcon) { + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + SysUIIcon( + icon = renderIcon.impl, + tint = color, + modifier = Modifier.size(SizeBadge), + ) + } + is OngoingActivityChipModel.ChipIcon.StatusBarView -> { + val drawable = renderIcon.impl.drawable + if (drawable != null) { + Image( + bitmap = drawable.toScaledBitmap(SizeBadge), + contentDescription = null, + modifier = Modifier.size(SizeBadge), + ) + } + } + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> { + val drawable = remember(event.active.managingPackageName) { + event.active.managingPackageName?.let { pkg -> + try { context.packageManager.getApplicationIcon(pkg) } catch (_: Exception) { null } + } + } + if (drawable != null) { + Image( + bitmap = drawable.toScaledBitmap(SizeBadge), + contentDescription = null, + modifier = Modifier.size(SizeBadge), + ) + } + } + } + } + + val isScreenRec = event.active.key == "ScreenRecord" + when { + isCall -> { + val transition = rememberInfiniteTransition(label = "aosp_call_shake") + val shake by transition.animateFloat( + initialValue = -0.8f, + targetValue = 0.8f, + animationSpec = infiniteRepeatable(tween(90), RepeatMode.Reverse), + label = "aosp_call_shake_anim", + ) + Box(modifier = Modifier.size(SizeBadge).offset(x = shake.dp)) { + iconContent() + } + } + isIconOnly -> { + val transition = rememberInfiniteTransition(label = "aosp_icononly") + val pulseAlpha by transition.animateFloat( + initialValue = 1f, + targetValue = AlphaDisabled, + animationSpec = infiniteRepeatable(tween(900, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "aosp_icononly_alpha", + ) + Box(modifier = Modifier.size(SizeBadge).graphicsLayer { this.alpha = pulseAlpha }) { + iconContent() + } + } + isScreenRec -> { + val transition = rememberInfiniteTransition(label = "aosp_screenrec") + val pulseAlpha by transition.animateFloat( + initialValue = 1f, + targetValue = AlphaSubtle, + animationSpec = infiniteRepeatable(tween(600, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "aosp_screenrec_alpha", + ) + Box(modifier = Modifier.size(SizeBadge).graphicsLayer { this.alpha = pulseAlpha }) { + iconContent() + } + } + else -> iconContent() + } +} + +@Composable +private fun aospChipAccent(active: OngoingActivityChipModel.Active): Color { + val context = LocalContext.current + return Color(active.colors.background(context).defaultColor) +} + @Composable private fun BlinkingDotIcon(color: Color, isAnimating: Boolean = true) { if (isAnimating) { @@ -117,40 +229,6 @@ private fun BlinkingDotIcon(color: Color, isAnimating: Boolean = true) { } } -@Composable -private fun PrivacyDotIcon(event: IslandEvent.MicCamActive, tint: Color? = null) { - val transition = rememberInfiniteTransition(label = "privacy_pulse") - val alpha by - transition.animateFloat( - initialValue = 1f, - targetValue = AlphaDisabled, - animationSpec = - infiniteRepeatable(tween(600, easing = FastOutSlowInEasing), RepeatMode.Reverse), - label = "privacy_alpha", - ) - Canvas(modifier = Modifier.size(SizeBadge)) { - val spacing = size.width * 0.35f - if (event.isCam) { - drawCircle( - color = (tint ?: RedAccent).copy(alpha = alpha), - radius = size.minDimension * 0.25f, - center = Offset(size.width / 2 - spacing / 2, size.height / 2), - ) - } - if (event.isMic) { - drawCircle( - color = (tint ?: OrangeAccent).copy(alpha = alpha), - radius = size.minDimension * 0.25f, - center = - Offset( - if (event.isCam) size.width / 2 + spacing / 2 else size.width / 2, - size.height / 2, - ), - ) - } - } -} - @Composable private fun AnimatedTrophyIcon(color: Color) { val transition = rememberInfiniteTransition(label = "trophy") @@ -246,47 +324,6 @@ private fun AnimatedHotspotIcon(color: Color) { } } -@Composable -private fun AnimatedCastIcon(color: Color) { - val transition = rememberInfiniteTransition(label = "cast") - val sweep by - transition.animateFloat( - initialValue = 0f, - targetValue = 3f, - animationSpec = - infiniteRepeatable(tween(2000, easing = LinearEasing), RepeatMode.Restart), - label = "cast_sweep", - ) - Canvas(modifier = Modifier.size(SizeBadge)) { - val sw = SizeStrokeThin.dp.toPx() - val w = size.width - val h = size.height - drawRoundRect( - color = color.copy(alpha = 0.5f), - topLeft = Offset(0f, h * 0.15f), - size = Size(w, h * 0.7f), - cornerRadius = CornerRadius(w * 0.12f), - style = Stroke(sw), - ) - val bx = w * 0.15f - val by = h * 0.85f - for (i in 0 until 3) { - val r = w * (0.1f + i * 0.12f) - val a = if (sweep > i) ((sweep - i).coerceIn(0f, 1f) * 0.7f) else AlphaFaint - drawArc( - color = color.copy(alpha = a), - startAngle = 180f, - sweepAngle = 90f, - useCenter = false, - topLeft = Offset(bx - r, by - r), - size = Size(r * 2, r * 2), - style = Stroke(sw, cap = StrokeCap.Round), - ) - } - drawCircle(color, radius = w * 0.06f, center = Offset(bx, by)) - } -} - @Composable private fun AnimatedNowPlayingIcon(color: Color) { val transition = rememberInfiniteTransition(label = "np") @@ -568,40 +605,54 @@ private fun AnimatedTickIcon(color: Color, isRunning: Boolean) { @Composable private fun RingerIcon(event: IslandEvent.RingerMode, tint: Color? = null) { - val color = tint ?: eventStyleFor(event).accent - val offset = if (event.mode == AudioManager.RINGER_MODE_VIBRATE) { - val transition = rememberInfiniteTransition(label = "ringer") - val anim by transition.animateFloat( - initialValue = -1f, - targetValue = 1f, - animationSpec = infiniteRepeatable(tween(120), RepeatMode.Reverse), - label = "ringer_offset", - ) - anim - } else { - 0f - } - Canvas(modifier = Modifier.size(SizeBadge)) { - val cx = - size.width / 2 + - if (event.mode == AudioManager.RINGER_MODE_VIBRATE) offset * SizeStrokeThin.dp.toPx() else 0f - val cy = size.height / 2 - val r = size.minDimension / 2 * 0.85f - drawRoundRect( - color = color, - topLeft = Offset(cx - r * 0.4f, cy - r * 0.6f), - size = Size(r * 0.8f, r * 1.2f), - cornerRadius = CornerRadius(r * 0.2f), - ) - if (event.mode == AudioManager.RINGER_MODE_SILENT) { - - drawLine( - color, - Offset(cx - r * 0.6f, cy + r * 0.6f), - Offset(cx + r * 0.6f, cy - r * 0.6f), - strokeWidth = SizeStrokeThin.dp.toPx(), + val style = eventStyleFor(event) + val color = tint ?: style.accent + val vector = style.icon ?: return + when (event.mode) { + AudioManager.RINGER_MODE_VIBRATE -> { + val transition = rememberInfiniteTransition(label = "ringer_shake") + val anim by transition.animateFloat( + initialValue = -0.8f, + targetValue = 0.8f, + animationSpec = infiniteRepeatable(tween(90), RepeatMode.Reverse), + label = "ringer_shake_anim", + ) + Icon(vector, null, tint = color, modifier = Modifier.size(SizeBadge).offset(x = anim.dp)) + } + AudioManager.RINGER_MODE_NORMAL -> { + val transition = rememberInfiniteTransition(label = "ringer_normal") + val scale by transition.animateFloat( + initialValue = 1f, + targetValue = 1.12f, + animationSpec = infiniteRepeatable(tween(750, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "ringer_normal_scale", + ) + Icon( + vector, + null, + tint = color, + modifier = Modifier.size(SizeBadge).graphicsLayer { + scaleX = scale + scaleY = scale + }, + ) + } + AudioManager.RINGER_MODE_SILENT -> { + val transition = rememberInfiniteTransition(label = "ringer_silent") + val alpha by transition.animateFloat( + initialValue = 1f, + targetValue = AlphaDisabled, + animationSpec = infiniteRepeatable(tween(1200, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "ringer_silent_alpha", + ) + Icon( + vector, + null, + tint = color, + modifier = Modifier.size(SizeBadge).graphicsLayer { this.alpha = alpha }, ) } + else -> Icon(vector, null, tint = color, modifier = Modifier.size(SizeBadge)) } } @@ -652,28 +703,16 @@ private fun AnimatedRecentsIcon(color: Color) { @Composable private fun NotificationPillIcon(event: IslandEvent.Notification) { - val icon = event.senderIcon ?: event.appIcon - val isRound = event.isConversation && event.senderIcon != null - val hasBadge = event.isConversation && event.senderIcon != null && event.appIcon != null - icon?.let { - if (hasBadge) { - BadgedContactIcon( - mainIcon = it, - badgeIcon = event.appIcon!!, - mainSize = 16.dp, - badgeSize = 9.dp, - isRound = true, - ) - } else { - Image( - bitmap = it.toScaledBitmap(16.dp), - contentDescription = null, - modifier = - Modifier.size(16.dp) - .clip(if (isRound) CircleShape else ShapeXs), - ) - } - } ?: Icon(Icons.Filled.Notifications, null, tint = BlueAccent, modifier = Modifier.size(SizeBadge)) + val icon = event.appIcon + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(ShapeXs), + ) + } else { + Icon(Icons.Filled.Notifications, null, tint = BlueAccent, modifier = Modifier.size(SizeBadge)) + } } private val DOWNLOAD_KEYWORDS = Regex( @@ -746,11 +785,21 @@ private fun AnimatedDownloadIcon(color: Color) { @Composable private fun PromotedOngoingText(event: IslandEvent.PromotedOngoing, modifier: Modifier, overrideColor: Color? = null) { - val label = event.shortText.ifEmpty { - if ((event.progress >= 0f || event.isIndeterminate) && event.text.isNotEmpty()) event.text - else event.title.ifEmpty { event.appName } + val color = overrideColor ?: BlueAccent + val base = when { + event.shortText.isNotEmpty() -> event.shortText + event.title.isNotEmpty() -> event.title + event.appName.isNotEmpty() -> event.appName + else -> "" } - MarqueeLabel(label, overrideColor ?: BlueAccent, modifier) + val percent = if (event.progress in 0f..1f) "${(event.progress * 100).toInt()}%" else null + val label = when { + base.isNotEmpty() && percent != null -> "$base · $percent" + base.isNotEmpty() -> base + percent != null -> percent + else -> return + } + MarqueeLabel(label, color, modifier) } @Composable @@ -975,10 +1024,7 @@ internal fun PillEventText( overrideColor: Color? = null, ) { when (event) { - is IslandEvent.ScreenRecording -> RecordingText(event, modifier, overrideColor ?: RedAccent) - is IslandEvent.MicCamActive -> MicCamText(event, modifier, overrideColor) is IslandEvent.AudioRecording -> AudioRecText(event, modifier, overrideColor) - is IslandEvent.Casting -> MarqueeLabel(event.deviceName.take(12), overrideColor ?: TealAccent, modifier) is IslandEvent.Media -> MediaTitleText(event, modifier, overrideColor) is IslandEvent.PromotedOngoing -> PromotedOngoingText(event, modifier, overrideColor) is IslandEvent.Sports -> SportsText(event, modifier, overrideColor) @@ -1005,17 +1051,11 @@ internal fun PillEventText( is IslandEvent.Clipboard -> MarqueeLabel(event.preview.ifEmpty { stringResource(R.string.ax_dynamic_bar_copied) }, overrideColor ?: IndigoAccent, modifier) is IslandEvent.Notification -> { - if (event.callStartTimeMs > 0L && event.appName.startsWith("Phone:")) { - CallTimerText(event, modifier, overrideColor) + val name = event.title + if (name != null) { + MarqueeLabel(name, overrideColor ?: BlueAccent, modifier) } else { - val name = event.senderName ?: if (event.isConversation) event.title else null - if (name != null) { - val label = if (event.isGroupConversation && event.conversationTitle != null) - "$name · ${event.conversationTitle}" else name - MarqueeLabel(label, overrideColor ?: BlueAccent, modifier) - } else { - NotifBellBadge(modifier, notifCount) - } + NotifBellBadge(modifier, notifCount) } } is IslandEvent.AppSwitch -> @@ -1029,6 +1069,66 @@ internal fun PillEventText( } is IslandEvent.BiometricUnlock -> MarqueeLabel(stringResource(R.string.ax_dynamic_bar_unlocked), overrideColor ?: GreenAccent, modifier) is IslandEvent.KeyguardIndication -> MarqueeLabel(event.text, overrideColor ?: IndigoAccent, modifier) + is IslandEvent.AospChip -> AospChipText(event, modifier, overrideColor) + } +} + +@Composable +private fun AospChipText(event: IslandEvent.AospChip, modifier: Modifier, overrideColor: Color? = null) { + val color = overrideColor ?: aospChipAccent(event.active) + when (val c = event.active.content) { + is OngoingActivityChipModel.Content.Text -> MarqueeLabel(c.text, color, modifier) + is OngoingActivityChipModel.Content.Timer -> AospChipTimerText(c, color, modifier) + is OngoingActivityChipModel.Content.ShortTimeDelta -> AospChipDeltaText(c, color, modifier) + is OngoingActivityChipModel.Content.Countdown -> + Text( + NumberFormat.getIntegerInstance().format(c.secondsUntilStarted), + color = color, + style = PillMono, + modifier = modifier, + ) + is OngoingActivityChipModel.Content.IconOnly -> {} + } +} + +@Composable +private fun AospChipTimerText(content: OngoingActivityChipModel.Content.Timer, color: Color, modifier: Modifier) { + var elapsedMs by remember(content.startTimeMs) { + mutableLongStateOf((SystemClock.elapsedRealtime() - content.startTimeMs).coerceAtLeast(0L)) + } + LaunchedEffect(content.startTimeMs, content.isEventInFuture) { + while (true) { + elapsedMs = if (content.isEventInFuture) { + (content.startTimeMs - SystemClock.elapsedRealtime()).coerceAtLeast(0L) + } else { + (SystemClock.elapsedRealtime() - content.startTimeMs).coerceAtLeast(0L) + } + delay(1000) + } + } + Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) +} + +@Composable +private fun AospChipDeltaText(content: OngoingActivityChipModel.Content.ShortTimeDelta, color: Color, modifier: Modifier) { + var text by remember(content.time) { mutableStateOf(formatShortDelta(System.currentTimeMillis() - content.time)) } + LaunchedEffect(content.time) { + while (true) { + text = formatShortDelta(System.currentTimeMillis() - content.time) + delay(30_000) + } + } + MarqueeLabel(text, color, modifier) +} + +private fun formatShortDelta(deltaMs: Long): String { + val absMs = kotlin.math.abs(deltaMs) + val mins = absMs / 60_000L + return when { + mins < 1L -> "now" + mins < 60L -> "${mins}m" + mins < 1440L -> "${mins / 60L}h" + else -> "${mins / 1440L}d" } } @@ -1045,34 +1145,34 @@ private fun MarqueeLabel(text: String, color: Color, modifier: Modifier = Modifi } @Composable -private fun RecordingText(event: IslandEvent.ScreenRecording, modifier: Modifier, color: Color) { - if (event.isCountdown) { - Text(formatCountdownSeconds(event.countdownSeconds), color = color, style = PillMono, modifier = modifier) - return +private fun AudioRecordingPillIcon(event: IslandEvent.AudioRecording, tint: Color? = null) { + val color = tint ?: when (event.state) { + RecordingState.RECORDING -> RedAccent + RecordingState.PAUSED -> SubtleGray + RecordingState.SAVED -> GreenAccent } - var elapsedMs by remember(event.startTimeMs) { - mutableLongStateOf((System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L)) + val vector = when (event.state) { + RecordingState.RECORDING -> Icons.Filled.Mic + RecordingState.PAUSED -> Icons.Filled.MicOff + RecordingState.SAVED -> Icons.Filled.CheckCircle } - LaunchedEffect(event.startTimeMs) { - while (true) { - delay(1000) - elapsedMs = (System.currentTimeMillis() - event.startTimeMs).coerceAtLeast(0L) - } - } - Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) -} - -@Composable -private fun MicCamText(event: IslandEvent.MicCamActive, modifier: Modifier, overrideColor: Color? = null) { - val cam = stringResource(R.string.ax_dynamic_bar_cam_short) - val mic = stringResource(R.string.ax_dynamic_bar_mic_short) - val label = buildString { - if (event.isCam) append(cam) - if (event.isMic && event.isCam) append(" · ") - if (event.isMic) append(mic) + if (event.state == RecordingState.RECORDING) { + val transition = rememberInfiniteTransition(label = "audio_rec") + val pulseAlpha by transition.animateFloat( + initialValue = 1f, + targetValue = AlphaSubtle, + animationSpec = infiniteRepeatable(tween(700, easing = FastOutSlowInEasing), RepeatMode.Reverse), + label = "audio_rec_alpha", + ) + Icon( + vector, + null, + tint = color, + modifier = Modifier.size(SizeBadge).graphicsLayer { this.alpha = pulseAlpha }, + ) + } else { + Icon(vector, null, tint = color, modifier = Modifier.size(SizeBadge)) } - val color = overrideColor ?: if (event.isCam) RedAccent else OrangeAccent - MarqueeLabel(event.appName.ifEmpty { label }, color, modifier) } @Composable @@ -1185,26 +1285,6 @@ private fun StopwatchText(event: IslandEvent.Stopwatch, modifier: Modifier, over } } -@Composable -private fun CallTimerText(event: IslandEvent.Notification, modifier: Modifier, overrideColor: Color? = null) { - val isActive = event.appName == "Phone:active" - if (isActive) { - var elapsedMs by remember(event.callStartTimeMs) { - mutableLongStateOf((System.currentTimeMillis() - event.callStartTimeMs).coerceAtLeast(0L)) - } - LaunchedEffect(event.callStartTimeMs) { - while (true) { - delay(1000) - elapsedMs = (System.currentTimeMillis() - event.callStartTimeMs).coerceAtLeast(0L) - } - } - val color = overrideColor ?: GreenAccent - Text(formatElapsedTime(elapsedMs), color = color, style = PillMono, modifier = modifier) - } else { - MarqueeLabel(event.senderName ?: event.title ?: stringResource(R.string.ax_dynamic_bar_incoming_call), overrideColor ?: BlueAccent, modifier) - } -} - @Composable private fun NotifBellBadge(modifier: Modifier, count: Int) { Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -1231,32 +1311,6 @@ private fun NotifBellBadge(modifier: Modifier, count: Int) { } } -@Composable -fun BadgedContactIcon( - mainIcon: Drawable, - badgeIcon: Drawable, - mainSize: Dp, - badgeSize: Dp, - isRound: Boolean = true, -) { - Box(modifier = Modifier.size(mainSize)) { - Image( - bitmap = mainIcon.toScaledBitmap(mainSize), - contentDescription = null, - modifier = - Modifier.size(mainSize) - .clip(if (isRound) CircleShape else ShapeXs), - contentScale = ContentScale.Crop, - ) - Image( - bitmap = badgeIcon.toScaledBitmap(badgeSize), - contentDescription = null, - modifier = - Modifier.size(badgeSize).align(Alignment.BottomEnd).clip(RoundedCornerShape(3.dp)), - contentScale = ContentScale.Crop, - ) - } -} @Composable fun PulsingDot(color: Color, size: Dp = 8.dp, durationMs: Int = 600, minAlpha: Float = AlphaDisabled) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 5f3f068c5a34..0047c76f448b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -1,6 +1,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.transition.TransitionManager import android.view.View import android.view.ViewGroup import com.android.axion.compose.host.AxComposeView @@ -11,16 +12,32 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel import com.android.systemui.axdynamicbar.ui.compose.AxDynamicBarKeyguardChip +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.util.ScrimUtils -import com.android.systemui.util.Utils import javax.inject.Inject import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +private const val CHIP_ABOVE_LOCK_MARGIN_DP = 12f +private const val EXPANDED_BOTTOM_PROTECTION_DP = 16f +private const val UNSET = -1 + +private val HIDDEN_VIEW_IDS = listOf( + R.id.shared_notification_container, + R.id.notificationShelf, +) + +private fun Float.dpToPx(context: Context): Int = + (this * context.resources.displayMetrics.density + 0.5f).toInt() class AxDynamicBarKeyguardChipSection @Inject @@ -28,6 +45,7 @@ constructor( @ShadeDisplayAware private val context: Context, private val viewModel: AxDynamicBarChipViewModel, private val indicationController: KeyguardIndicationController, + private val clockInteractor: KeyguardClockInteractor, ) : KeyguardSection() { private val chipViewId = R.id.ax_dynamic_bar_keyguard_chip @@ -35,8 +53,6 @@ constructor( private var expansionHandle: DisposableHandle? = null private var enforceAction: Runnable? = null - private var isCurrentlyHiding = false - override fun addViews(constraintLayout: ConstraintLayout) { val composeView = AxComposeView(context).apply { id = chipViewId } constraintLayout.addView(composeView) @@ -57,113 +73,163 @@ constructor( bindHandle = composeView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { - combine(viewModel.isOnKeyguard, viewModel.isEnabled, viewModel.isKeyguardEnabled) { onKeyguard, enabled, kgEnabled -> - onKeyguard && enabled && kgEnabled - }.collect { suppress -> - indicationController.setSuppressIndication(suppress) - } + combine(viewModel.isOnKeyguard, viewModel.isEnabled, viewModel.isKeyguardEnabled) { onKg, enabled, kgEnabled -> + onKg && enabled && kgEnabled + }.collect { indicationController.setSuppressIndication(it) } } } expansionHandle = composeView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { + val scope = this + scope.launch { + viewModel.keyguardExpansion.collapseSettled.collect { + if (!viewModel.keyguardExpansion.isExpanded.value) { + applyCollapsedLp(composeView, viewModel.isLowUdfps.value) + setHiddenViewsVisibility(constraintLayout, View.VISIBLE) + } + } + } + scope.launch { + viewModel.isKeyguardExpanded.collectLatest { expanded -> + if (expanded) { + clockInteractor.clockSize.collect { size -> + if (size != ClockSize.SMALL) { + clockInteractor.setClockSize(ClockSize.SMALL) + } + } + } + } + } combine(viewModel.isKeyguardExpanded, viewModel.isLowUdfps) { expanded, lowUdfps -> expanded to lowUdfps }.collect { (expanded, lowUdfps) -> - isCurrentlyHiding = expanded - enforceHiddenViews(constraintLayout, expanded) - - enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } - enforceAction = if (expanded) { - Runnable { enforceHiddenViews(constraintLayout, true) }.also { - ScrimUtils.get().addKeyguardPreDrawAction(it) - } - } else null - - val lp = composeView.layoutParams as ConstraintLayout.LayoutParams - if (expanded) { - lp.width = ConstraintLayout.LayoutParams.MATCH_PARENT - lp.height = 0 - lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID - lp.topMargin = Utils.getStatusBarHeaderHeightKeyguard(context) - lp.bottomToTop = R.id.start_button - lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID - lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID - lp.bottomMargin = 0 - - lp.topToBottom = -1 - lp.bottomToBottom = -1 - lp.startToEnd = -1 - lp.endToStart = -1 - } else if (lowUdfps) { - lp.width = ViewGroup.LayoutParams.WRAP_CONTENT - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT - lp.topMargin = 0 - - lp.bottomToTop = R.id.device_entry_icon_view - lp.bottomMargin = (CHIP_ABOVE_LOCK_MARGIN_DP * context.resources.displayMetrics.density + 0.5f).toInt() - lp.bottomToBottom = -1 - lp.topToTop = -1 - - lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID - lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID - lp.startToEnd = -1 - lp.endToStart = -1 - - lp.topToBottom = -1 - } else { - lp.width = ViewGroup.LayoutParams.WRAP_CONTENT - lp.height = ViewGroup.LayoutParams.WRAP_CONTENT - lp.topMargin = 0 - - lp.bottomToBottom = R.id.start_button - lp.bottomMargin = 0 - lp.bottomToTop = -1 - lp.topToTop = -1 - - lp.startToEnd = R.id.start_button - lp.endToStart = R.id.end_button - lp.startToStart = -1 - lp.endToEnd = -1 - - lp.topToBottom = -1 - } - composeView.layoutParams = lp + onExpandedStateChanged(constraintLayout, composeView, expanded, lowUdfps) } } } } + private fun onExpandedStateChanged( + constraintLayout: ConstraintLayout, + composeView: View, + expanded: Boolean, + lowUdfps: Boolean, + ) { + rebindPreDrawAction(constraintLayout, expanded) + TransitionManager.endTransitions(constraintLayout) + if (expanded) { + setHiddenViewsVisibility(constraintLayout, View.INVISIBLE) + applyExpandedLp(composeView) + } + } + + private fun rebindPreDrawAction(constraintLayout: ConstraintLayout, expanded: Boolean) { + enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } + enforceAction = if (expanded) { + Runnable { enforceHidden(constraintLayout) }.also { + ScrimUtils.get().addKeyguardPreDrawAction(it) + } + } else null + } + + private fun hiddenTargets(constraintLayout: ConstraintLayout): List = + HIDDEN_VIEW_IDS.mapNotNull { constraintLayout.rootView.findViewById(it) } + + private fun setHiddenViewsVisibility(constraintLayout: ConstraintLayout, visibility: Int) { + hiddenTargets(constraintLayout).forEach { v -> + v.alpha = 1f + if (v.visibility != visibility) v.visibility = visibility + } + } + + private fun enforceHidden(constraintLayout: ConstraintLayout) { + hiddenTargets(constraintLayout).forEach { v -> + if (v.visibility != View.INVISIBLE) v.visibility = View.INVISIBLE + } + } + override fun applyConstraints(constraintSet: ConstraintSet) { val expanded = viewModel.isKeyguardExpanded.value val lowUdfps = viewModel.isLowUdfps.value + val bottomProtectionPx = EXPANDED_BOTTOM_PROTECTION_DP.dpToPx(context) + val chipAboveLockPx = CHIP_ABOVE_LOCK_MARGIN_DP.dpToPx(context) + val wrap = ViewGroup.LayoutParams.WRAP_CONTENT constraintSet.apply { - if (expanded) { - constrainWidth(chipViewId, ConstraintSet.MATCH_CONSTRAINT) - constrainHeight(chipViewId, ConstraintSet.MATCH_CONSTRAINT) - connect(chipViewId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, - Utils.getStatusBarHeaderHeightKeyguard(context)) - connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.TOP) - connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) - connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) - } else if (lowUdfps) { - constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - val margin = (CHIP_ABOVE_LOCK_MARGIN_DP * context.resources.displayMetrics.density + 0.5f).toInt() - connect(chipViewId, ConstraintSet.BOTTOM, R.id.device_entry_icon_view, ConstraintSet.TOP, margin) - connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) - connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) - } else { - constrainWidth(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - constrainHeight(chipViewId, ViewGroup.LayoutParams.WRAP_CONTENT) - connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.BOTTOM) - connect(chipViewId, ConstraintSet.START, R.id.start_button, ConstraintSet.END) - connect(chipViewId, ConstraintSet.END, R.id.end_button, ConstraintSet.START) + when { + expanded -> { + constrainWidth(chipViewId, ConstraintSet.MATCH_CONSTRAINT) + constrainHeight(chipViewId, ConstraintSet.MATCH_CONSTRAINT) + connect(chipViewId, ConstraintSet.TOP, ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, ConstraintSet.BOTTOM) + connect(chipViewId, ConstraintSet.BOTTOM, R.id.device_entry_icon_view, ConstraintSet.TOP, bottomProtectionPx) + connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + } + lowUdfps -> { + constrainWidth(chipViewId, wrap) + constrainHeight(chipViewId, wrap) + connect(chipViewId, ConstraintSet.BOTTOM, R.id.device_entry_icon_view, ConstraintSet.TOP, chipAboveLockPx) + connect(chipViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(chipViewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + } + else -> { + constrainWidth(chipViewId, wrap) + constrainHeight(chipViewId, wrap) + connect(chipViewId, ConstraintSet.BOTTOM, R.id.start_button, ConstraintSet.BOTTOM) + connect(chipViewId, ConstraintSet.START, R.id.start_button, ConstraintSet.END) + connect(chipViewId, ConstraintSet.END, R.id.end_button, ConstraintSet.START) + } } } } + private fun applyExpandedLp(composeView: View) { + val lp = composeView.layoutParams as ConstraintLayout.LayoutParams + val bottomProtectionPx = EXPANDED_BOTTOM_PROTECTION_DP.dpToPx(context) + lp.width = ConstraintLayout.LayoutParams.MATCH_PARENT + lp.height = 0 + lp.topToBottom = ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL + lp.bottomToTop = R.id.device_entry_icon_view + lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID + lp.topMargin = 0 + lp.bottomMargin = bottomProtectionPx + lp.topToTop = UNSET + lp.bottomToBottom = UNSET + lp.startToEnd = UNSET + lp.endToStart = UNSET + composeView.layoutParams = lp + } + + private fun applyCollapsedLp(composeView: View, lowUdfps: Boolean) { + val lp = composeView.layoutParams as ConstraintLayout.LayoutParams + lp.width = ViewGroup.LayoutParams.WRAP_CONTENT + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT + lp.topMargin = 0 + lp.topToTop = UNSET + lp.topToBottom = UNSET + if (lowUdfps) { + lp.bottomToTop = R.id.device_entry_icon_view + lp.bottomToBottom = UNSET + lp.bottomMargin = CHIP_ABOVE_LOCK_MARGIN_DP.dpToPx(context) + lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID + lp.startToEnd = UNSET + lp.endToStart = UNSET + } else { + lp.bottomToBottom = R.id.start_button + lp.bottomToTop = UNSET + lp.bottomMargin = 0 + lp.startToEnd = R.id.start_button + lp.endToStart = R.id.end_button + lp.startToStart = UNSET + lp.endToEnd = UNSET + } + composeView.layoutParams = lp + } + override fun removeViews(constraintLayout: ConstraintLayout) { + TransitionManager.endTransitions(constraintLayout) enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } enforceAction = null expansionHandle?.dispose() @@ -173,29 +239,4 @@ constructor( indicationController.setSuppressIndication(false) constraintLayout.removeView(chipViewId) } - - companion object { - private const val CHIP_ABOVE_LOCK_MARGIN_DP = 12f - } - - private val preserveOnExpandIds = setOf( - chipViewId, - R.id.start_button, - R.id.end_button, - ) - - private fun enforceHiddenViews(constraintLayout: ConstraintLayout, hide: Boolean) { - for (i in 0 until constraintLayout.childCount) { - val child = constraintLayout.getChildAt(i) - if (child.id in preserveOnExpandIds) continue - val target = if (hide) View.INVISIBLE else View.VISIBLE - if (child.visibility != target) child.visibility = target - } - constraintLayout.rootView - .findViewById(R.id.shared_notification_container) - ?.let { v -> - val target = if (hide) View.INVISIBLE else View.VISIBLE - if (v.visibility != target) v.visibility = target - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index dca5667fe09e..7ebdc16d1478 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -47,7 +47,6 @@ import android.service.notification.Flags import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.internal.messages.nano.SystemMessageProto.SystemMessage -import com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker @@ -116,15 +115,6 @@ class PeekDisabledSuppressor( } } -class PeekAxDynamicBarSuppressor( - private val settings: AxDynamicBarSettings, -) : VisualInterruptionCondition( - types = setOf(PEEK), - reason = "suppressed by AxDynamicBar" -) { - override fun shouldSuppress(): Boolean = settings.isNotificationEventsActive() -} - class PulseDisabledSuppressor( private val ambientDisplayConfiguration: AmbientDisplayConfiguration, private val userTracker: UserTracker, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 82de28a78ac9..bd7681b2adf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -39,7 +39,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -88,7 +87,6 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private final GlobalSettings mGlobalSettings; private final EventLog mEventLog; private final Optional mBubbles; - private final AxDynamicBarSettings mAxDynamicBarSettings; @VisibleForTesting protected boolean mUseHeadsUp = false; @@ -139,9 +137,7 @@ public NotificationInterruptStateProviderImpl( SystemClock systemClock, GlobalSettings globalSettings, EventLog eventLog, - Optional bubbles, - AxDynamicBarSettings axDynamicBarSettings) { - mAxDynamicBarSettings = axDynamicBarSettings; + Optional bubbles) { mPowerManager = powerManager; mBatteryController = batteryController; mAmbientDisplayConfiguration = ambientDisplayConfiguration; @@ -435,11 +431,6 @@ private boolean shouldHeadsUpWhenAwake(NotificationEntry entry, boolean log) { return false; } - if (mAxDynamicBarSettings.isNotificationEventsActive()) { - if (log) mLogger.logNoHeadsUpFeatureDisabled(); - return false; - } - if (!canAlertCommon(entry, log)) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index c0de0488b400..13f11aad33bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -77,7 +77,6 @@ constructor( private val notificationManager: NotificationManager, private val settingsInteractor: NotificationSettingsInteractor, private val deviceProvisioningInteractor: DeviceProvisioningInteractor, - private val axDynamicBarSettings: com.android.systemui.axdynamicbar.domain.AxDynamicBarSettings, ) : VisualInterruptionDecisionProvider { init { @@ -167,7 +166,6 @@ constructor( check(!started) addCondition(PeekDisabledSuppressor(globalSettings, headsUpManager, logger, mainHandler)) - addCondition(PeekAxDynamicBarSuppressor(axDynamicBarSettings)) addCondition(PulseDisabledSuppressor(ambientDisplayConfiguration, userTracker)) addCondition(PulseBatterySaverSuppressor(batteryController)) addFilter(PeekPackageSnoozedSuppressor(headsUpManager)) From f6d9cdc3e217bd24668c815783990a9c3d08e2df Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 26 Apr 2026 12:09:50 +0000 Subject: [PATCH 1132/1315] SystemUI: DynamicBar: Redesign lockscreen media panel Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/KeyguardExpandedContent.kt | 584 +++++++++++------- 1 file changed, 346 insertions(+), 238 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt index c328f67d07de..1a73e97738f4 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/KeyguardExpandedContent.kt @@ -8,8 +8,6 @@ import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.StartOffset import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -65,6 +63,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput @@ -75,6 +74,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.StrokeCap @@ -209,309 +209,417 @@ private fun KeyguardMediaPanel(event: IslandEvent.Media, interactor: IslandActio val colors = rememberMediaColors(event) val motionScheme = MaterialTheme.motionScheme - Column( + Box( modifier = Modifier .widthIn(max = ExpandedMaxWidth) .fillMaxSize() - .padding(horizontal = SpaceSection), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(horizontal = SpaceSection) + .clip(ShapeXl) + .background(CardBg) + .border(1.dp, CardBorderBrush, ShapeXl) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {}, + ), ) { - Box( - modifier = Modifier - .weight(1f, fill = true) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - AnimatedContent( - targetState = event.albumArt, - transitionSpec = { - fadeIn(motionScheme.defaultEffectsSpec()) togetherWith - fadeOut(motionScheme.fastEffectsSpec()) using - SizeTransform(clip = false) - }, - contentKey = { it?.hashCode() ?: 0 }, - label = "kg_media_album_art", - ) { art -> + AnimatedContent( + targetState = event.albumArt, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + contentKey = { it?.hashCode() ?: 0 }, + label = "kg_media_backdrop", + ) { art -> + Box(modifier = Modifier.fillMaxSize()) { if (art != null) { Image( - bitmap = art.toScaledBitmap(SizeAlbumLg), + bitmap = art.toScaledBitmap(350.dp), contentDescription = null, modifier = Modifier - .fillMaxHeight() - .aspectRatio(1f) - .clip(ShapeLg), + .matchParentSize() + .graphicsLayer { + scaleX = 1.15f + scaleY = 1.15f + } + .blur(32.dp), contentScale = ContentScale.Crop, ) } else { Box( modifier = Modifier - .fillMaxHeight() - .aspectRatio(1f) - .clip(ShapeLg) - .background(colors.tonal), - contentAlignment = Alignment.Center, - ) { - Icon(Icons.Filled.MusicNote, null, tint = colors.accent.copy(AlphaDisabled), modifier = Modifier.size(SpacePanelLarge)) - } + .matchParentSize() + .background(Brush.verticalGradient(listOf(colors.tonal, CardBg))) + ) } + + Box( + modifier = Modifier + .matchParentSize() + .background( + Brush.verticalGradient( + 0f to Color.Black.copy(alpha = 0.30f), + 0.45f to Color.Black.copy(alpha = 0.55f), + 1f to Color.Black.copy(alpha = 0.88f), + ) + ) + ) + + Box( + modifier = Modifier + .matchParentSize() + .background(colors.accent.copy(alpha = 0.08f)) + ) } } - Spacer(Modifier.height(SpaceLg)) + Column( + modifier = Modifier + .fillMaxSize() + .padding(SpaceSection), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .weight(1f, fill = true) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = event.albumArt, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + contentKey = { it?.hashCode() ?: 0 }, + label = "kg_media_album_art", + ) { art -> + if (art != null) { + Image( + bitmap = art.toScaledBitmap(SizeAlbumLg), + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(ShapeLg), + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(ShapeLg) + .background(colors.tonal), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Filled.MusicNote, + null, + tint = colors.accent.copy(alpha = AlphaDisabled), + modifier = Modifier.size(SpacePanelLarge), + ) + } + } + } + } - val trackText = event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) } - AnimatedContent( - targetState = trackText, - transitionSpec = { - fadeIn(motionScheme.defaultEffectsSpec()) togetherWith - fadeOut(motionScheme.fastEffectsSpec()) using - SizeTransform(clip = false) - }, - label = "kg_media_track", - ) { title -> - Text( - title, - color = Color.White, - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } + Spacer(Modifier.height(SpaceLg)) - if (event.artist.isNotEmpty()) { - Spacer(Modifier.height(SpaceMd)) + val trackText = event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) } AnimatedContent( - targetState = event.artist, + targetState = trackText, transitionSpec = { fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec()) using SizeTransform(clip = false) }, - label = "kg_media_artist", - ) { artist -> + label = "kg_media_track", + ) { title -> Text( - artist, - color = Color.White.copy(alpha = AlphaSecondary), - style = MaterialTheme.typography.bodyMedium, + title, + color = Color.White, + style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) } - } - Spacer(Modifier.height(SpaceLg)) + if (event.artist.isNotEmpty()) { + Spacer(Modifier.height(SpaceMd)) + AnimatedContent( + targetState = event.artist, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) using + SizeTransform(clip = false) + }, + label = "kg_media_artist", + ) { artist -> + Text( + artist, + color = Color.White.copy(alpha = AlphaSecondary), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Spacer(Modifier.height(SpaceLg)) - Surface( - modifier = Modifier - .fillMaxWidth() - .border(1.dp, CardBorderBrush, ShapeCard), - shape = ShapeCard, - color = CardBg, - ) { - Column( + Surface( modifier = Modifier .fillMaxWidth() - .padding(horizontal = SpaceXxl, vertical = SpaceLg), - verticalArrangement = Arrangement.Center, + .border(1.dp, CardBorderBrush, ShapeCard), + shape = ShapeCard, + color = Color.Black.copy(alpha = 0.22f), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SpaceXxl, vertical = SpaceLg), + verticalArrangement = Arrangement.Center, ) { Row( - modifier = Modifier.weight(1f), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceMd), - ) { - event.appIcon?.let { icon -> - Image( - bitmap = icon.toScaledBitmap(SizeIconSm), - contentDescription = null, - modifier = Modifier.size(SizeIconSm).clip(ShapeXs), - colorFilter = ColorFilter.tint(colors.accent), - ) - } ?: Icon( - Icons.Filled.MusicNote, - null, - tint = colors.accent, - modifier = Modifier.size(SizeIconSm), - ) - Text( - event.outputDeviceName.ifBlank { - stringResource(R.string.ax_dynamic_bar_now_playing) - }, - color = OnCardText, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Surface( - onClick = { - interactor.openMediaOutputSwitcher() - interactor.collapseIsland() - }, - shape = ShapeChip, - color = colors.accent.copy(alpha = AlphaSubtle), + horizontalArrangement = Arrangement.SpaceBetween, ) { Row( - modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceSm), + modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SpaceXs), + horizontalArrangement = Arrangement.spacedBy(SpaceMd), ) { - Icon( - Icons.Filled.VolumeUp, - stringResource(R.string.ax_dynamic_bar_output), + event.appIcon?.let { icon -> + Image( + bitmap = icon.toScaledBitmap(SizeIconSm), + contentDescription = null, + modifier = Modifier.size(SizeIconSm).clip(ShapeXs), + colorFilter = ColorFilter.tint(colors.accent), + ) + } ?: Icon( + Icons.Filled.MusicNote, + null, tint = colors.accent, modifier = Modifier.size(SizeIconSm), ) + Text( + event.outputDeviceName.ifBlank { + stringResource(R.string.ax_dynamic_bar_now_playing) + }, + color = Color.White, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } - } - } - if (event.duration > 0L) { - Spacer(Modifier.height(SpaceMd)) - val mediaProgress = rememberMediaProgress(event) - var isSeeking by remember { mutableStateOf(false) } - var seekProgress by remember { mutableFloatStateOf(mediaProgress.progress) } - if (!isSeeking) seekProgress = mediaProgress.progress - val displayMs = - if (isSeeking) (seekProgress * event.duration).toLong() - else mediaProgress.positionMs - Box( - modifier = Modifier - .fillMaxWidth() - .height(SpaceSection) - .pointerInput("tap") { - detectTapGestures { offset -> - val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - seekProgress = f - interactor.seekTo((f * event.duration).toLong()) - } - } - .pointerInput("drag") { - detectHorizontalDragGestures( - onDragStart = { offset -> - isSeeking = true - seekProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) - }, - onDragEnd = { - isSeeking = false - interactor.seekTo((seekProgress * event.duration).toLong()) - }, - onDragCancel = { isSeeking = false }, - onHorizontalDrag = { change, _ -> - seekProgress = (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) - change.consume() - }, - ) + Surface( + onClick = { + interactor.openMediaOutputSwitcher() + interactor.collapseIsland() }, - contentAlignment = Alignment.Center, - ) { - LinearWavyProgressIndicator( - progress = { seekProgress }, - modifier = Modifier.fillMaxWidth(), - color = colors.accent, - trackColor = colors.accent.copy(alpha = AlphaSubtle), - amplitude = { if (event.isPlaying) 1f else 0f }, - ) - } - Spacer(Modifier.height(SpaceXs)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(formatElapsedTime(displayMs), color = OnCardSecondary, style = MaterialTheme.typography.labelSmall) - Text(formatElapsedTime(event.duration), color = OnCardSecondary, style = MaterialTheme.typography.labelSmall) - } - } - - Spacer(Modifier.height(SpaceMd)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - onClick = { - event.customActions.firstOrNull()?.let { - interactor.sendCustomAction(it.action) + shape = ShapeChip, + color = colors.accent.copy(alpha = AlphaSubtle), + ) { + Row( + modifier = Modifier.padding(horizontal = SpaceLg, vertical = SpaceSm), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Icon( + Icons.Filled.VolumeUp, + stringResource(R.string.ax_dynamic_bar_output), + tint = colors.accent, + modifier = Modifier.size(SizeIconSm), + ) } - }, - modifier = Modifier.size(SizeButtonLg), - ) { - val ca = event.customActions.firstOrNull() - if (ca != null) { - CustomActionIcon(ca, tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) - } else { - Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) } } - Spacer(Modifier.width(SpaceSm)) + if (event.duration > 0L) { + Spacer(Modifier.height(SpaceMd)) + val mediaProgress = rememberMediaProgress(event) + var isSeeking by remember { mutableStateOf(false) } + var seekProgress by remember { mutableFloatStateOf(mediaProgress.progress) } + if (!isSeeking) seekProgress = mediaProgress.progress + val displayMs = + if (isSeeking) (seekProgress * event.duration).toLong() + else mediaProgress.positionMs + + Box( + modifier = Modifier + .fillMaxWidth() + .height(SpaceSection) + .pointerInput("tap") { + detectTapGestures { offset -> + val f = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + seekProgress = f + interactor.seekTo((f * event.duration).toLong()) + } + } + .pointerInput("drag") { + detectHorizontalDragGestures( + onDragStart = { offset -> + isSeeking = true + seekProgress = + (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + isSeeking = false + interactor.seekTo((seekProgress * event.duration).toLong()) + }, + onDragCancel = { isSeeking = false }, + onHorizontalDrag = { change, _ -> + seekProgress = + (change.position.x / size.width.toFloat()).coerceIn(0f, 1f) + change.consume() + }, + ) + }, + contentAlignment = Alignment.Center, + ) { + LinearWavyProgressIndicator( + progress = { seekProgress }, + modifier = Modifier.fillMaxWidth(), + color = colors.accent, + trackColor = colors.accent.copy(alpha = AlphaSubtle), + amplitude = { if (event.isPlaying) 1f else 0f }, + ) + } - IconButton( - onClick = { interactor.skipPrev() }, - modifier = Modifier.size(SizeButtonLg), - ) { - Icon(Icons.Filled.SkipPrevious, stringResource(R.string.ax_dynamic_bar_previous), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) + Spacer(Modifier.height(SpaceXs)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + formatElapsedTime(displayMs), + color = Color.White.copy(alpha = AlphaSecondary), + style = MaterialTheme.typography.labelSmall, + ) + Text( + formatElapsedTime(event.duration), + color = Color.White.copy(alpha = AlphaSecondary), + style = MaterialTheme.typography.labelSmall, + ) + } } - Spacer(Modifier.width(SpaceSm)) + Spacer(Modifier.height(SpaceMd)) - Surface( - onClick = { interactor.togglePlayPause() }, - modifier = Modifier.size(SizeButtonLg), - shape = CircleShape, - color = colors.accent, + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - AnimatedContent( - targetState = event.isPlaying, - transitionSpec = { - fadeIn(motionScheme.defaultEffectsSpec()) togetherWith - fadeOut(motionScheme.fastEffectsSpec()) - }, - label = "kg_media_playpause", - ) { playing -> + IconButton( + onClick = { + event.customActions.firstOrNull()?.let { + interactor.sendCustomAction(it.action) + } + }, + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.firstOrNull() + if (ca != null) { + CustomActionIcon(ca, tint = Color.White.copy(alpha = AlphaSecondary), modifier = Modifier.size(SizeIconMd)) + } else { Icon( - if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, - if (playing) stringResource(R.string.ax_dynamic_bar_pause) else stringResource(R.string.ax_dynamic_bar_play), - tint = colors.onAccent, + Icons.Filled.Shuffle, + stringResource(R.string.ax_dynamic_bar_shuffle), + tint = Color.White.copy(alpha = AlphaSecondary), modifier = Modifier.size(SizeIconMd), ) } } - } - Spacer(Modifier.width(SpaceSm)) + Spacer(Modifier.width(SpaceSm)) - IconButton( - onClick = { interactor.skipNext() }, - modifier = Modifier.size(SizeButtonLg), - ) { - Icon(Icons.Filled.SkipNext, stringResource(R.string.ax_dynamic_bar_next), tint = OnCardText, modifier = Modifier.size(SizeIconMd)) - } + IconButton( + onClick = { interactor.skipPrev() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon( + Icons.Filled.SkipPrevious, + stringResource(R.string.ax_dynamic_bar_previous), + tint = Color.White, + modifier = Modifier.size(SizeIconMd), + ) + } + + Spacer(Modifier.width(SpaceSm)) - Spacer(Modifier.width(SpaceSm)) + Surface( + onClick = { interactor.togglePlayPause() }, + modifier = Modifier.size(SizeButtonLg), + shape = CircleShape, + color = colors.accent, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + AnimatedContent( + targetState = event.isPlaying, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec()) + }, + label = "kg_media_playpause", + ) { playing -> + Icon( + if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, + if (playing) stringResource(R.string.ax_dynamic_bar_pause) + else stringResource(R.string.ax_dynamic_bar_play), + tint = colors.onAccent, + modifier = Modifier.size(SizeIconMd), + ) + } + } + } + + Spacer(Modifier.width(SpaceSm)) + + IconButton( + onClick = { interactor.skipNext() }, + modifier = Modifier.size(SizeButtonLg), + ) { + Icon( + Icons.Filled.SkipNext, + stringResource(R.string.ax_dynamic_bar_next), + tint = Color.White, + modifier = Modifier.size(SizeIconMd), + ) + } + + Spacer(Modifier.width(SpaceSm)) - IconButton( - onClick = { - event.customActions.getOrNull(1)?.let { - interactor.sendCustomAction(it.action) + IconButton( + onClick = { + event.customActions.getOrNull(1)?.let { + interactor.sendCustomAction(it.action) + } + }, + modifier = Modifier.size(SizeButtonLg), + ) { + val ca = event.customActions.getOrNull(1) + if (ca != null) { + CustomActionIcon(ca, tint = Color.White.copy(alpha = AlphaSecondary), modifier = Modifier.size(SizeIconMd)) + } else { + Icon( + Icons.Filled.Shuffle, + stringResource(R.string.ax_dynamic_bar_shuffle), + tint = Color.White.copy(alpha = AlphaSecondary), + modifier = Modifier.size(SizeIconMd), + ) } - }, - modifier = Modifier.size(SizeButtonLg), - ) { - val ca = event.customActions.getOrNull(1) - if (ca != null) { - CustomActionIcon(ca, tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) - } else { - Icon(Icons.Filled.Shuffle, stringResource(R.string.ax_dynamic_bar_shuffle), tint = OnCardSecondary, modifier = Modifier.size(SizeIconMd)) } } } From 00edb3c5a0b03b385e963a0bf38cf834e404c464 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 26 Apr 2026 13:18:22 +0000 Subject: [PATCH 1133/1315] SystemUI: DynamicBar: Hide custom clock for expanded panel Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../view/layout/sections/AxDynamicBarKeyguardChipSection.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 0047c76f448b..beeb77655238 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -32,6 +32,12 @@ private const val EXPANDED_BOTTOM_PROTECTION_DP = 16f private const val UNSET = -1 private val HIDDEN_VIEW_IDS = listOf( + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, R.id.shared_notification_container, R.id.notificationShelf, ) From 7a16e718c1524a5af9457fde0f934f74c22f8cea Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:21:12 +0800 Subject: [PATCH 1134/1315] SystemUI: DynamicBar: Fixing sb expanded content issue Change-Id: Ib1277ea39a90ac3890ced30ed8802a30b9376e3b Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/AxDynamicBarExpandedPanel.kt | 12 +++++++++++- .../ui/AxDynamicBarStatusBarExpansion.kt | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index c5daf38657b3..7a931885f236 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -156,7 +156,8 @@ constructor( WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or if (isCurrentlyExpanded) 0 - else WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + else (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) val params = WindowManager.LayoutParams( @@ -202,10 +203,12 @@ constructor( val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread if (expanded) { params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() params.height = WindowManager.LayoutParams.MATCH_PARENT windowManager.updateViewLayout(view, params) } else { params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE windowManager.updateViewLayout(view, params) val runnable = Runnable { val v = overlayView ?: return@Runnable @@ -249,6 +252,13 @@ private fun OverlayContent(viewModel: AxDynamicBarChipViewModel, statusBarHeight expandedVisible.targetState = isExpanded } + LaunchedEffect(chipState) { + val filtered = chipState?.allEvents?.filter { it !is IslandEvent.AospChip } + if (filtered.isNullOrEmpty() && isExpanded) { + viewModel.statusBarExpansion.collapse() + } + } + val originX = chipX val origin = TransformOrigin(originX, 0f) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt index d890fba8fad4..bdbf456be553 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarStatusBarExpansion.kt @@ -17,6 +17,7 @@ package com.android.systemui.axdynamicbar.ui import com.android.systemui.axdynamicbar.domain.AxDynamicBarInteractor +import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject @@ -50,6 +51,16 @@ constructor( .onEach { if (it) collapse() } .launchIn(applicationScope) + interactor.uiState + .map { state -> + state.events.isEmpty() || state.events.all { it is IslandEvent.AospChip } + } + .distinctUntilChanged() + .onEach { shouldCollapse -> + if (shouldCollapse) collapse() + } + .launchIn(applicationScope) + combine( interactor.qsExpansion.map { it > 0f }.distinctUntilChanged(), interactor.legacyShadeExpansion.map { it > 0f }.distinctUntilChanged(), @@ -61,7 +72,8 @@ constructor( } fun expand() { - if (interactor.uiState.value.topEvent == null) return + val state = interactor.uiState.value + if (state.events.isEmpty() || state.events.all { it is IslandEvent.AospChip }) return _intent.value = true } From 84d0005f528919b75da93e9a75246fab067fdfc5 Mon Sep 17 00:00:00 2001 From: bijoyv9 Date: Sun, 26 Apr 2026 08:15:53 +0530 Subject: [PATCH 1135/1315] SystemUI: DynamicBar: Stabilize and clean up charging info formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor charging info construction to use a list-based model with TextUtils.join(" · ", …), replacing manual string concatenation. This improves readability and makes future metric additions easier. Introduce formatChargingString() to normalize emitted text while preserving existing state semantics. Unlike the previous approach, this avoids emitting empty values when not charging and instead retains the current indication string, preventing UI resets on the lockscreen during transient state changes. Additionally, fix formatting correctness by: - Using Locale.US for deterministic numeric output - Ensuring floating-point division for accurate metric values This keeps the data flow behavior introduced earlier intact while making the formatting logic more robust and maintainable. Change-Id: I255d6658c8f488b58e277184e6551a644a2ac3d8 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/AxDynamicBarChipViewModel.kt | 9 ++++- .../KeyguardIndicationController.java | 37 +++++++++++-------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 182f66981cb0..178f9c03ed5e 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -121,12 +121,12 @@ constructor( if (charging) { flow { while (true) { - emit(keyguardIndicationController.powerChargingString) + emit(formatChargingString(keyguardIndicationController.powerChargingString)) delay(BATTERY_STRING_REFRESH_MS) } } } else { - flowOf(keyguardIndicationController.powerChargingString) + flowOf(formatChargingString(keyguardIndicationController.powerChargingString)) } } .distinctUntilChanged() @@ -197,4 +197,9 @@ constructor( private const val LOW_UDFPS_THRESHOLD = 0.93f private const val BATTERY_STRING_REFRESH_MS = 2_000L } + + private fun formatChargingString(text: String?): String { + val cleaned = text?.trim() + return if (cleaned.isNullOrEmpty()) "" else cleaned + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 62c65601155d..89c7667a407a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -136,6 +136,9 @@ import java.io.PrintWriter; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Consumer; @@ -1457,27 +1460,31 @@ protected String computePowerChargingStringIndication() { } String batteryInfo = ""; - boolean showBatteryInfo = Settings.System.getIntForUser(mContext.getContentResolver(), + boolean showbatteryInfo = Settings.System.getIntForUser(mContext.getContentResolver(), Settings.System.LOCKSCREEN_BATTERY_INFO, 1, UserHandle.USER_CURRENT) == 1; - if (showBatteryInfo) { - if (mCurrentDivider != 0 && mChargingCurrent >= mCurrentDivider * 1000) { - float chargingCurrentInAmps = (float) (mChargingCurrent / (mCurrentDivider * 1000)); - batteryInfo = String.format("%.1fA", chargingCurrentInAmps); - } else if (mCurrentDivider != 0 && mChargingCurrent > 0) { - float chargingCurrentInMilliamps = (float) (mChargingCurrent / mCurrentDivider); - batteryInfo = String.format("%.0f mA", chargingCurrentInMilliamps); + if (showbatteryInfo) { + List chargingDetails = new ArrayList<>(); + if (mChargingCurrent >= mCurrentDivider * 1000) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingCurrent / (float) mCurrentDivider / 1000f)) + "A"); + } else if (mChargingCurrent > 0) { + chargingDetails.add(String.format(Locale.US, "%.0f", + (mChargingCurrent / (float) mCurrentDivider)) + "mA"); } - if (mCurrentDivider != 0 && mChargingWattage > 0) { - float chargingWattageInWatts = (float) (mChargingWattage / (mCurrentDivider * 1000)); - batteryInfo += " · " + String.format("%.1fW", chargingWattageInWatts); + if (mChargingWattage > 0) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingWattage / (float) mCurrentDivider / 1000f)) + "W"); } if (mChargingVoltage > 0) { - float chargingVoltageInVolts = (float) (mChargingVoltage / 1000000); - batteryInfo += " · " + String.format("%.1fV", chargingVoltageInVolts); + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingVoltage / 1000000f)) + "V"); } if (mTemperature > 0) { - float temperatureInCelsius = (float) (mTemperature / 10); - batteryInfo += " · " + String.format("%.1f°C", temperatureInCelsius); + chargingDetails.add(String.format(Locale.US, "%.1f", + (mTemperature / 10f)) + "°C"); + } + if (!chargingDetails.isEmpty()) { + batteryInfo = "\n" + TextUtils.join(" · ", chargingDetails); } } From 9aa179ce799922c2a7c75231d50611c35a44fefe Mon Sep 17 00:00:00 2001 From: bijoyv9 Date: Sun, 26 Apr 2026 07:50:40 +0000 Subject: [PATCH 1136/1315] SystemUI: DynamicBar: Refine charging chip layout and parsing logic Refactor and enhance charging information handling in the Dynamic Bar keyguard chip to improve readability, stability, and UI behavior. Key changes: - Split extended charging metrics into a structured 2-line layout: primary (charging status) and secondary (current, wattage, voltage, temperature). - Replace single-line text rendering with a Column-based layout to properly support multi-line content and visual hierarchy. - Dynamically adjust chip height and use flexible width constraints with animateContentSize for smoother transitions. - Normalize and sanitize charging strings using a centralized helper (rememberChargingParts), ensuring consistent parsing across all call sites. - Harden parsing logic with trimming, filtering, and bounded splitting (limit = 3) to prevent malformed or unexpected upstream strings from breaking UI layout. - Improve fallback handling to avoid empty or invalid text states. This results in a cleaner, more readable charging UI while maintaining backward compatibility and stable behavior under edge cases. Change-Id: I7453d8658c8f488b58e277184e6551a644a2ac3d8 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/AxDynamicBarKeyguardChip.kt | 97 ++++++++++++++++--- 1 file changed, 81 insertions(+), 16 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index 4e6d1c52b60d..38eb64409c78 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -34,6 +34,8 @@ import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -80,6 +82,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.android.systemui.axdynamicbar.model.IslandEvent import com.android.systemui.axdynamicbar.model.RecordingState import com.android.systemui.axdynamicbar.shared.* @@ -100,6 +103,13 @@ private val ActionIconSize = SizeBadge private val BatteryIconSize = ChipHeight - SpaceXxl private val CountBadgeHeight = ChipHeight / 2 +@Composable +private fun rememberChargingParts(batteryString: String): List { + return remember(batteryString) { + batteryString.split("\n", limit = 3).map { it.trim() }.filter { it.isNotEmpty() } + } +} + @Composable fun AxDynamicBarKeyguardChip( viewModel: AxDynamicBarChipViewModel, @@ -251,6 +261,7 @@ fun AxDynamicBarKeyguardChip( progress = progress, eventCount = chipState.eventCount, viewModel = viewModel, + batteryString = batteryString, ) } } else { @@ -273,15 +284,20 @@ private fun KeyguardChipBody( progress: Float?, eventCount: Int, viewModel: AxDynamicBarChipViewModel, + batteryString: String = "", ) { val context = LocalContext.current val motionScheme = MaterialTheme.motionScheme + val parts = rememberChargingParts(batteryString) + val isMultiLineCharging = event is IslandEvent.Charging && parts.size >= 2 + val dynamicHeight = if (isMultiLineCharging) 48.dp else ChipHeight + Box(contentAlignment = Alignment.Center) { Row( modifier = Modifier - .height(ChipHeight) - .widthIn(max = 178.dp) + .height(dynamicHeight) + .widthIn(min = 48.dp, max = 260.dp) .clip(ChipShape) .background(accent) .animateContentSize(motionScheme.defaultSpatialSpec()) @@ -458,7 +474,7 @@ private fun KeyguardChipBody( modifier = Modifier.weight(1f, fill = false), ) { ev -> Row(verticalAlignment = Alignment.CenterVertically) { - KeyguardPrimaryText(ev, contentColor, Modifier.weight(1f, fill = false)) + KeyguardPrimaryText(ev, contentColor, Modifier.weight(1f, fill = false), batteryString) val secondary = secondaryTextFor(ev) if (secondary != null) { Text( @@ -538,14 +554,19 @@ private fun KeyguardBatteryChip( } val contentColor = chipContentColorOn(accent) + val parts = rememberChargingParts(batteryString) + val isMultiLine = info.isCharging && parts.size >= 2 + val dynamicHeight = if (isMultiLine) 48.dp else ChipHeight + Box(contentAlignment = Alignment.Center) { Row( modifier = modifier - .height(ChipHeight) + .height(dynamicHeight) .clip(ChipShape) .background(accent) - .widthIn(max = 260.dp) - .padding(horizontal = SpaceMd), + .widthIn(min = 48.dp, max = 260.dp) + .padding(horizontal = SpaceMd) + .animateContentSize(MaterialTheme.motionScheme.defaultSpatialSpec()), verticalAlignment = Alignment.CenterVertically, ) { @@ -556,14 +577,36 @@ private fun KeyguardBatteryChip( } Spacer(Modifier.width(SpaceXs)) if (info.isCharging) { - Text( - batteryString, - style = PillPrimary, - color = contentColor.copy(alpha = AlphaSecondary), - maxLines = 2, - overflow = TextOverflow.Clip, - modifier = modifier.basicMarquee(), - ) + if (isMultiLine) { + Column( + modifier = Modifier.weight(1f, fill = false), + verticalArrangement = Arrangement.Center + ) { + Text( + parts[0], + style = PillPrimary, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + parts[1], + style = PillPrimary.copy(fontSize = 10.sp), + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } else { + Text( + if (parts.isNotEmpty()) parts[0] else batteryString, + style = PillPrimary, + color = contentColor.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = Modifier.basicMarquee(), + ) + } return } Text( @@ -721,7 +764,7 @@ private fun AnimatedBatteryFillIcon(level: Int, color: Color, iconSize: Dp = Bat } @Composable -private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modifier) { +private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modifier, batteryString: String = "") { when (event) { is IslandEvent.AudioRecording -> when (event.state) { RecordingState.RECORDING -> ElapsedTimeText( @@ -737,7 +780,29 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi } is IslandEvent.Stopwatch -> StopwatchTimeText(event, color, modifier) is IslandEvent.Notification -> MarqueeText(event.title ?: event.appName, color, modifier) - is IslandEvent.Charging -> MarqueeText("${event.level}%", color, modifier) + is IslandEvent.Charging -> { + val parts = rememberChargingParts(batteryString) + if (parts.size >= 2) { + Column(modifier = modifier, verticalArrangement = Arrangement.Center) { + Text( + parts[0], + style = PillPrimary, + color = color, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + parts[1], + style = PillPrimary.copy(fontSize = 10.sp), + color = color.copy(alpha = AlphaSecondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } else { + MarqueeText(if (parts.isNotEmpty()) parts[0] else "${event.level}%", color, modifier) + } + } is IslandEvent.Bluetooth -> MarqueeText(event.deviceName.take(12), color, modifier) is IslandEvent.Hotspot -> { val hotspotLabel = stringResource(R.string.ax_dynamic_bar_hotspot) From 985696e705040a826f342a58243dda16393518b2 Mon Sep 17 00:00:00 2001 From: bijoyv9 Date: Sun, 26 Apr 2026 09:42:02 +0000 Subject: [PATCH 1137/1315] SystemUI: Fix charging info toggle behavior and unify settings namespace Move lockscreen charging info preferences entirely to Settings.Secure to match Settings app storage and avoid namespace mismatch. Fix detailed charging info toggle by gating individual metrics instead of the entire charging info block. This ensures: - toggle state is respected correctly - no default fallback overrides user preference - consistent behavior across lockscreen and AOD Change-Id: I417a1d1a39b638801e71fe5caa41978c9dda1a0c Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 12 +++++ .../KeyguardIndicationController.java | 46 +++++++++++-------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1e47f38fee98..1e6c466e4a47 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9810,6 +9810,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Readable public static final String LOCK_SCREEN_LOCK_AFTER_TIMEOUT = "lock_screen_lock_after_timeout"; + /** + * Whether to show the charging info on the lockscreen while charging + * @hide + */ + public static final String LOCKSCREEN_BATTERY_INFO = "lockscreen_charging_info"; + + /** + * Whether to show the detailed charging info on the lockscreen while charging + * @hide + */ + public static final String LOCKSCREEN_CHARGING_INFO_DETAILS = "lockscreen_charging_info_details"; + /** * This preference contains the string that shows for owner info on LockScreen. * @hide diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 89c7667a407a..5b0ddfcc9af9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1460,28 +1460,34 @@ protected String computePowerChargingStringIndication() { } String batteryInfo = ""; - boolean showbatteryInfo = Settings.System.getIntForUser(mContext.getContentResolver(), - Settings.System.LOCKSCREEN_BATTERY_INFO, 1, UserHandle.USER_CURRENT) == 1; + boolean showbatteryInfo = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCKSCREEN_BATTERY_INFO, 1, UserHandle.USER_CURRENT) == 1; + if (showbatteryInfo) { + boolean showDetails = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCKSCREEN_CHARGING_INFO_DETAILS, 1, UserHandle.USER_CURRENT) == 1; + List chargingDetails = new ArrayList<>(); - if (mChargingCurrent >= mCurrentDivider * 1000) { - chargingDetails.add(String.format(Locale.US, "%.1f", - (mChargingCurrent / (float) mCurrentDivider / 1000f)) + "A"); - } else if (mChargingCurrent > 0) { - chargingDetails.add(String.format(Locale.US, "%.0f", - (mChargingCurrent / (float) mCurrentDivider)) + "mA"); - } - if (mChargingWattage > 0) { - chargingDetails.add(String.format(Locale.US, "%.1f", - (mChargingWattage / (float) mCurrentDivider / 1000f)) + "W"); - } - if (mChargingVoltage > 0) { - chargingDetails.add(String.format(Locale.US, "%.1f", - (mChargingVoltage / 1000000f)) + "V"); - } - if (mTemperature > 0) { - chargingDetails.add(String.format(Locale.US, "%.1f", - (mTemperature / 10f)) + "°C"); + if (showDetails) { + if (mChargingCurrent >= mCurrentDivider * 1000) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingCurrent / (float) mCurrentDivider / 1000f)) + "A"); + } else if (mChargingCurrent > 0) { + chargingDetails.add(String.format(Locale.US, "%.0f", + (mChargingCurrent / (float) mCurrentDivider)) + "mA"); + } + if (mChargingWattage > 0) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingWattage / (float) mCurrentDivider / 1000f)) + "W"); + } + if (mChargingVoltage > 0) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mChargingVoltage / 1000000f)) + "V"); + } + if (mTemperature > 0) { + chargingDetails.add(String.format(Locale.US, "%.1f", + (mTemperature / 10f)) + "°C"); + } } if (!chargingDetails.isEmpty()) { batteryInfo = "\n" + TextUtils.join(" · ", chargingDetails); From b15c91f15abefd8f4cfbd52bbcd52222afddc868 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 29 Apr 2026 09:23:03 +0000 Subject: [PATCH 1138/1315] SystemUI: DynamicBar: Align media seekbar timestamps inline with progress bar Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/ExpandedMediaContent.kt | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index 9b6dc28eaf4b..a4b3580fcb6c 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -362,22 +362,16 @@ private fun MediaSeekBar( var displayFraction by remember { mutableStateOf(serverFraction) } val interactorRef = rememberUpdatedState(interactor) - - // Read the dismiss swipe lock provided by MagneticSwipeToDismiss val swipeLock = LocalDismissSwipeLock.current - // Smooth frame-interpolated progress when playing, snaps when paused or scrubbing LaunchedEffect(positionMs, durationMs, isPlaying) { if (isScrubbing) return@LaunchedEffect - displayFraction = serverFraction - if (!isPlaying || durationMs <= 0L) return@LaunchedEffect - val startWallMs = System.currentTimeMillis() val startProgressMs = positionMs while (true) { - delay(16L) // ~60 fps + delay(16L) if (isScrubbing) break val elapsed = System.currentTimeMillis() - startWallMs val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f, 1f) @@ -390,30 +384,24 @@ private fun MediaSeekBar( val accentArgb = accent.toArgb() val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb() - Column(verticalArrangement = Arrangement.spacedBy(SpaceXs)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - formatElapsedTime(displayMs), - color = SubtleGray, - style = MaterialTheme.typography.labelSmall, - ) - Text( - formatElapsedTime(durationMs), - color = SubtleGray, - style = MaterialTheme.typography.labelSmall, - ) - } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SpaceXs), + ) { + Text( + formatElapsedTime(displayMs), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) Box( modifier = Modifier - .fillMaxWidth() + .weight(1f) .height(SeekBarHeight) .pointerInput(swipeLock) { awaitEachGesture { - awaitPointerEvent() // DOWN + awaitPointerEvent() swipeLock.value = true try { do { @@ -526,7 +514,6 @@ private fun MediaSeekBar( val layer = bar.progressDrawable as? LayerDrawable - // Re-tint track colors layer?.findDrawableByLayerId(android.R.id.background) ?.setTint(trackAlphaArgb) layer?.findDrawableByLayerId(android.R.id.secondaryProgress) @@ -548,6 +535,12 @@ private fun MediaSeekBar( ) } } + + Text( + formatElapsedTime(durationMs), + color = SubtleGray, + style = MaterialTheme.typography.labelSmall, + ) } } From 496cda868d985a6ab552233a0d0e5cb1da679f07 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 29 Apr 2026 09:24:21 +0000 Subject: [PATCH 1139/1315] SystemUI: DynamicBar: Increase extended media blur value Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../axdynamicbar/ui/compose/ExpandedMediaContent.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt index a4b3580fcb6c..7728a4e77d1b 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/ExpandedMediaContent.kt @@ -92,11 +92,11 @@ internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { event.albumArt?.let { art -> Image( - bitmap = art.toScaledBitmap(350.dp), + bitmap = art.toScaledBitmap(380.dp), contentDescription = null, modifier = Modifier .matchParentSize() - .blur(24.dp), + .blur(32.dp), contentScale = ContentScale.Crop, ) } ?: Box( @@ -110,8 +110,8 @@ internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) { .matchParentSize() .background( Brush.verticalGradient( - 0.0f to Color(0xFF000000).copy(alpha = 0.45f), - 0.45f to Color(0xFF000000).copy(alpha = 0.7f), + 0.0f to Color(0xFF000000).copy(alpha = 0.4f), + 0.45f to Color(0xFF000000).copy(alpha = 0.65f), 1.0f to Color(0xFF000000).copy(alpha = 1.0f), ) ) From 73b7296d1b6d4944f2bba6b29bb0325aaf3492aa Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 10 Mar 2026 20:21:59 +0000 Subject: [PATCH 1140/1315] SystemUI: Introduce Depth Wallpaper feature [1/2] [Adapted by Infinity X] Tejas - Adapt it for A16 QPR2 * like ios wallpaper depth, taken from pixel xpert Co-authored-by: tejas101k Co-authored-by: Siavash Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/WallpaperManager.java | 15 +- .../systemui/media/MediaViewController.kt | 2 + .../CentralSurfacesDependenciesModule.java | 10 + .../statusbar/phone/CentralSurfacesImpl.java | 33 ++ .../statusbar/phone/ScrimController.java | 6 + .../com/android/systemui/util/ScrimUtils.kt | 118 ++++++- .../systemui/util/WallpaperDepthUtils.java | 317 ++++++++++++++++++ 7 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index ba04c78adf31..49e16dbefb17 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -3284,17 +3284,26 @@ public void sendWallpaperCommand(IBinder windowToken, String action, @Keep @TestApi public void setWallpaperZoomOut(@NonNull IBinder windowToken, float zoom) { - if (zoom < 0 || zoom > 1f) { - throw new IllegalArgumentException("zoom must be between 0 and 1: " + zoom); + final float mZoom = isDepthWallpaperEnabled() ? 1 : zoom; + if (mZoom < 0 || mZoom > 1f) { + throw new IllegalArgumentException("zoom must be between 0 and 1: " + mZoom); } if (windowToken == null) { throw new IllegalArgumentException("windowToken must not be null"); } try { - WindowManagerGlobal.getWindowSession().setWallpaperZoomOut(windowToken, zoom); + WindowManagerGlobal.getWindowSession().setWallpaperZoomOut(windowToken, mZoom); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + + } + private boolean isDepthWallpaperEnabled() { + boolean depthWallpaperEnabled = android.provider.Settings.System.getInt(mContext.getContentResolver(), + "depth_wallpaper_enabled", 0) == 1; + String depthWallpaperUri = android.provider.Settings.System.getString(mContext.getContentResolver(), + "depth_wallpaper_subject_image_uri"); + return depthWallpaperEnabled && depthWallpaperUri != null && !depthWallpaperUri.isEmpty(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 0ed68820f495..bb6ff111f153 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -104,6 +104,8 @@ class MediaViewController @Inject constructor( private val grayscaleMatrix = ColorMatrix().apply { setSaturation(0f) } init { + INSTANCE = this + context.contentResolver.registerContentObserver( Settings.System.getUriFor(Settings.System.LS_MEDIA_ART_ENABLED), false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index a7929ecbd801..505c8668bbe1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -56,11 +56,13 @@ import com.android.systemui.statusbar.phone.CentralSurfacesImpl; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; +import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.ui.StatusBarIconList; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.WallpaperDepthUtils; import com.android.wm.shell.shared.ShellTransitions; import dagger.Binds; @@ -248,4 +250,12 @@ public boolean isShowingAlternateAuthOnUnlock() { }; return new DialogTransitionAnimator(mainExecutor, callback, interactionJankMonitor); } + /** Depth Wallpaper */ + @Provides + @SysUISingleton + static WallpaperDepthUtils provideWallpaperDepthUtils( + Context context, + Lazy scrimControllerLazy) { + return WallpaperDepthUtils.getInstance(context, scrimControllerLazy.get()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index a8b30dbdb0eb..fdece7a0cf5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -51,10 +51,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.database.ContentObserver; import android.graphics.Point; import android.hardware.devicestate.DeviceStateManager; +import android.hardware.display.DisplayManager; import android.metrics.LogMaker; import android.net.Uri; +import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -203,6 +206,7 @@ import com.android.systemui.statusbar.LiftReveal; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.LockscreenShadeTransitionController; +import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -244,7 +248,9 @@ import com.android.systemui.topui.TopUiController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.DumpUtilsKt; +import com.android.systemui.util.ScrimUtils; import com.android.systemui.util.WallpaperController; +import com.android.systemui.util.WallpaperDepthUtils; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.MessageRouter; import com.android.systemui.util.kotlin.JavaAdapter; @@ -487,6 +493,8 @@ public QSPanelController getQSPanelController() { private final EdgeLightViewController mEdgeLightViewController; private final NowPlayingViewController mNowPlayingViewController; + private WallpaperDepthUtils mWallpaperDepthUtils; + private final DisplayMetrics mDisplayMetrics; private static final long GC_INTERVAL_MS = 10 * 60 * 1000L; // 10 minutes @@ -603,6 +611,10 @@ public int getId() { // Trigger an update for the scrim state when we enter or exit glanceable hub, so that we // can transition to/from ScrimState.GLANCEABLE_HUB if needed. updateScrimController(); + + if (mWallpaperDepthUtils != null) { + mWallpaperDepthUtils.onGlanceableHubShowingChanged(idleOnCommunal); + } }; private boolean mNoAnimationOnNextBarModeChange; @@ -756,6 +768,7 @@ public CentralSurfacesImpl( PulseViewController pulseViewController, EdgeLightViewController edgeLightViewController, NowPlayingViewController nowPlayingViewController, + WallpaperDepthUtils wallpaperDepthUtils, BurnInProtectionController burnInProtectionController ) { mContext = context; @@ -907,6 +920,7 @@ public CentralSurfacesImpl( mPulseViewController = pulseViewController; mEdgeLightViewController = edgeLightViewController; mNowPlayingViewController = nowPlayingViewController; + mWallpaperDepthUtils = wallpaperDepthUtils; } private void initBubbles(Bubbles bubbles) { @@ -1391,6 +1405,23 @@ public void hide() { mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront); } + // Setup depth wallpaper view - attach directly to NotificationShadeWindowView root + // This is CRITICAL for iOS-style depth effect: subject must be in root container, + ViewGroup root = getNotificationShadeWindowView(); + View depthWallpaperView = mWallpaperDepthUtils.getDepthWallpaperView(); + if (depthWallpaperView.getParent() == null) { + root.setClipChildren(false); + root.setClipToPadding(false); + root.addView(depthWallpaperView); + depthWallpaperView.bringToFront(); + } + ScrimUtils.get(mContext).setWallpaperDepthUtils(mWallpaperDepthUtils); + mWallpaperDepthUtils.updateDepthWallpaper(); + mWallpaperDepthUtils.updateDepthWallpaperVisibility(); + depthWallpaperView.postDelayed(() -> { + mWallpaperDepthUtils.updateDepthWallpaperVisibility(); + }, 500); + mLightRevealScrim.setScrimOpaqueChangedListener((opaque) -> { Runnable updateOpaqueness = () -> { mNotificationShadeWindowController.setLightRevealScrimOpaque( @@ -2863,12 +2894,14 @@ public void onScreenTurnedOn() { } mScrimController.onScreenTurnedOn(); + ScrimUtils.get(mContext).onScreenStateChange(); } @Override public void onScreenTurnedOff() { Trace.beginSection("CentralSurfaces#onScreenTurnedOff"); mFalsingCollector.onScreenOff(); + ScrimUtils.get(mContext).onScreenStateChange(); if (!SceneContainerFlag.isEnabled()) { mScrimController.onScreenTurnedOff(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 459bc62147e3..326469af3857 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -86,6 +86,7 @@ import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.ScrimUtils; import com.android.systemui.util.wakelock.DelayedWakeLock; import com.android.systemui.util.wakelock.WakeLock; import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor; @@ -776,6 +777,10 @@ public float getBackScaling() { return mNotificationsScrim.getScaleY(); } + public float getScrimBehindAlpha() { + return mScrimBehindAlphaKeyguard; + } + public void onTrackingStarted() { mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { @@ -1236,6 +1241,7 @@ private void applyAndDispatchState() { if (mScrimBehind != null) { dispatchBackScrimState(mScrimBehind.getViewAlpha()); } + ScrimUtils.get(mScrimBehind.getContext()).onScrimDispatched(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt index b7bfc0e5c6ae..d94f9fdc29d0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt @@ -16,17 +16,20 @@ package com.android.systemui.util +import android.content.Context import android.os.Handler import android.os.Looper import android.service.notification.StatusBarNotification import android.view.View import android.view.ViewTreeObserver +import com.android.systemui.Dependency import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED +import com.android.systemui.statusbar.phone.ScrimController import java.util.concurrent.atomic.AtomicBoolean /* Scrim - aka testing utils */ -class ScrimUtils private constructor() { +class ScrimUtils private constructor(context: Context?) { interface ScrimEventListener { fun onKeyguardShowingChanged(showing: Boolean) {} @@ -54,6 +57,24 @@ class ScrimUtils private constructor() { private val mPulsing = AtomicBoolean() private val mFadingAwayDuration = 500L + private val mContext: Context by lazy { + context ?: throw IllegalStateException("ScrimUtils requires a valid Context") + } + + private val mScrimController: ScrimController? by lazy { + try { + Dependency.get(ScrimController::class.java) + } catch (e: Exception) { + null + } + } + + private var mWallpaperDepthUtils: WallpaperDepthUtils? = null + + fun setWallpaperDepthUtils(utils: WallpaperDepthUtils) { + mWallpaperDepthUtils = utils + } + @Volatile private var mIsDozing: Boolean? = null @Volatile private var mKeyguardShowing: Boolean? = null @Volatile private var mExpandedFraction: Float? = null @@ -72,7 +93,13 @@ class ScrimUtils private constructor() { @JvmStatic fun get(): ScrimUtils = instance ?: synchronized(this) { - instance ?: ScrimUtils().also { instance = it } + instance ?: ScrimUtils(null).also { instance = it } + } + + @JvmStatic + fun get(context: Context): ScrimUtils = + instance ?: synchronized(this) { + instance ?: ScrimUtils(context).also { instance = it } } } @@ -83,22 +110,35 @@ class ScrimUtils private constructor() { if (mKeyguardShowing == null || mKeyguardShowing != showing) { mKeyguardShowing = showing listeners.notifyOnMain { it.onKeyguardShowingChanged(showing) } + if (showing) { + mainHandler.postDelayed({ + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + mWallpaperDepthUtils?.updateDepthWallpaper() + }, 120) + } } } fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { listeners.notifyOnMain { it.onKeyguardFadingAwayChanged(fadingAway) } postKeyguardRetry() + if (fadingAway) { + mWallpaperDepthUtils?.hideDepthWallpaper() + } } fun onKeyguardGoingAwayChanged(goingAway: Boolean) { listeners.notifyOnMain { it.onKeyguardGoingAwayChanged(goingAway) } postKeyguardRetry() + if (goingAway) { + mWallpaperDepthUtils?.hideDepthWallpaper() + } } fun onPrimaryBouncerShowingChanged(showing: Boolean) { listeners.notifyOnMain { it.onPrimaryBouncerShowingChanged(showing) } postKeyguardRetry() + mWallpaperDepthUtils?.onBouncerShowingChanged(showing) } private fun postKeyguardRetry() { @@ -121,6 +161,13 @@ class ScrimUtils private constructor() { if (mIsDozing == null || mIsDozing != dozing) { mIsDozing = dozing listeners.notify { it.onDozingChanged(dozing) } + mWallpaperDepthUtils?.onDozingChanged(dozing) + // Additional refresh when exiting doze to ensure depth wallpaper appears + if (!dozing && mStateIsKeyguard) { + mainHandler.postDelayed({ + mWallpaperDepthUtils?.updateDepthWallpaper() + }, 200) + } } } @@ -139,18 +186,36 @@ class ScrimUtils private constructor() { fun setQsVisible(visible: Boolean) { if (mQsVisible.getAndSet(visible) != visible) { listeners.notifyOnMain { it.onQsVisibilityChanged(visible) } + if (!visible && mStateIsKeyguard) { + mainHandler.postDelayed({ + mWallpaperDepthUtils?.updateDepthWallpaper() + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + }, 100) + } } } fun setPulsing(pulsing: Boolean) { if (mPulsing.getAndSet(pulsing) != pulsing) { listeners.notify { it.setPulsing(pulsing) } + if (!pulsing && mStateIsKeyguard) { + mainHandler.postDelayed({ + mWallpaperDepthUtils?.updateDepthWallpaper() + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + }, 120) + } } } fun onStartedWakingUp() { mAwake = true listeners.notify { it.onStartedWakingUp() } + if (mStateIsKeyguard) { + mainHandler.postDelayed({ + mWallpaperDepthUtils?.updateDepthWallpaper() + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + }, 150) + } } fun onScreenTurnedOff() { @@ -231,4 +296,53 @@ class ScrimUtils private constructor() { } else { (mExpandedFraction ?: 0.0f) <= 0.0f } + + // Depth Wallpaper control methods + fun setViewAlpha(subjectAlpha: Float) { + mWallpaperDepthUtils?.setSubjectAlpha(subjectAlpha) + } + + fun setQsExpansion(expansion: Float) { + val fullyCollapsed = expansion <= 0f + if (fullyCollapsed) { + if (mStateIsKeyguard) { + mWallpaperDepthUtils?.updateDepthWallpaper() + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + } + } else { + mWallpaperDepthUtils?.hideDepthWallpaper() + } + } + + fun onScreenStateChange() { + updateDepthWallpaperElements() + mainHandler.postDelayed({ + updateDepthWallpaperElements() + }, 250) + } + + private fun updateDepthWallpaperElements() { + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + } + + fun onScrimDispatched() { + mWallpaperDepthUtils?.updateDepthWallpaper() + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + } + + fun updateDepthWallpaper() { + mWallpaperDepthUtils?.updateDepthWallpaper() + } + + fun updateDepthWallpaperVisibility() { + mWallpaperDepthUtils?.updateDepthWallpaperVisibility() + } + + fun hideDepthWallpaper() { + mWallpaperDepthUtils?.hideDepthWallpaper() + } + + fun getScrimBehindAlphaKeyguard(): Float { + return mScrimController?.getScrimBehindAlpha() ?: 0f + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java new file mode 100644 index 000000000000..e04359675bf1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2023-2024 The risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util; + +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.Rect; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.systemui.Dependency; +import com.android.systemui.media.MediaViewController; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.phone.ScrimState; +import com.android.systemui.tuner.TunerService; + +public class WallpaperDepthUtils { + + private static final String WALLPAPER_DEPTH_KEY = "system:depth_wallpaper_subject_image_uri"; + private static final String WALLPAPER_DEPTH_ENABLED_KEY = "system:depth_wallpaper_enabled"; + private static final String WALLPAPER_DEPTH_OPACITY_KEY = "system:depth_wallpaper_opacity"; + private static final String WALLPAPER_DEPTH_OFFSET_X_KEY = "system:depth_wallpaper_offset_x"; + private static final String WALLPAPER_DEPTH_OFFSET_Y_KEY = "system:depth_wallpaper_offset_y"; + + private static WallpaperDepthUtils instance; + private FrameLayout mLockScreenSubject; + private Drawable mDimmingOverlay; + + private final Context mContext; + private final ScrimController mScrimController; + private final TunerService mTunerService; + + private boolean mDWallpaperEnabled; + private int mDWallOpacity = 255; + private String mWallpaperSubjectPath; + private boolean mDozing; + private boolean mBouncerShowing; + private boolean mGlanceableHubShowing; + private boolean mWallpaperLoaded = false; + private String mPreviousWallpaperPath; + private Bitmap mWallpaperBitmap; + private int mOffsetX; + private int mOffsetY; + + private WallpaperDepthUtils(Context context, ScrimController scrimController) { + mContext = context.getApplicationContext(); + mScrimController = scrimController; + mTunerService = Dependency.get(TunerService.class); + mTunerService.addTunable(mTunable, WALLPAPER_DEPTH_KEY, + WALLPAPER_DEPTH_ENABLED_KEY, WALLPAPER_DEPTH_OPACITY_KEY, + WALLPAPER_DEPTH_OFFSET_X_KEY, WALLPAPER_DEPTH_OFFSET_Y_KEY); + mLockScreenSubject = new FrameLayout(mContext) { + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + WallpaperDepthUtils.this.onDetachedFromWindow(); + } + }; + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, -1); + mLockScreenSubject.setLayoutParams(lp); + // Note: View is attached to NotificationShadeWindowView root for iOS-style depth + // Elevation helps but primary layering comes from view hierarchy position + mLockScreenSubject.setElevation(100f); + mLockScreenSubject.setTranslationZ(100f); + // Make view non-interactive so touches pass through to elements below + mLockScreenSubject.setClickable(false); + mLockScreenSubject.setFocusable(false); + } + + public static WallpaperDepthUtils getInstance(Context context, ScrimController scrimController) { + if (instance == null) { + instance = new WallpaperDepthUtils(context, scrimController); + } + return instance; + } + + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + if (mDozing) { + hideDepthWallpaper(); + } else { + updateDepthWallpaperVisibility(); + } + } + + public void onBouncerShowingChanged(boolean showing) { + if (mBouncerShowing == showing) { + return; + } + mBouncerShowing = showing; + if (mBouncerShowing) { + hideDepthWallpaper(); + } else { + updateDepthWallpaperVisibility(); + } + } + + public void onGlanceableHubShowingChanged(boolean showing) { + if (mGlanceableHubShowing == showing) { + return; + } + mGlanceableHubShowing = showing; + if (mGlanceableHubShowing) { + hideDepthWallpaper(); + } else { + updateDepthWallpaperVisibility(); + } + } + + private final TunerService.Tunable mTunable = new TunerService.Tunable() { + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case WALLPAPER_DEPTH_ENABLED_KEY: + mDWallpaperEnabled = TunerService.parseIntegerSwitch(newValue, false); + updateDepthWallpaper(true); + break; + case WALLPAPER_DEPTH_KEY: + mPreviousWallpaperPath = mWallpaperSubjectPath; + mWallpaperSubjectPath = newValue; + updateDepthWallpaper(true); + break; + case WALLPAPER_DEPTH_OPACITY_KEY: + int opacity = TunerService.parseInteger(newValue, 100); + mDWallOpacity = Math.round(opacity * 2.55f); + updateDepthWallpaper(true); + break; + case WALLPAPER_DEPTH_OFFSET_X_KEY: + mOffsetX = TunerService.parseInteger(newValue, 0); + updateDepthWallpaper(true); + break; + case WALLPAPER_DEPTH_OFFSET_Y_KEY: + mOffsetY = TunerService.parseInteger(newValue, 0); + updateDepthWallpaper(true); + break; + default: + break; + } + } + }; + + public void setSubjectAlpha(float subjectAlpha) { + if (mLockScreenSubject == null) return; + mLockScreenSubject.post(() -> mLockScreenSubject.setAlpha(subjectAlpha)); + } + + public void updateDepthWallpaper() { + updateDepthWallpaper(false); + } + + public FrameLayout getDepthWallpaperView() { + return mLockScreenSubject; + } + + private boolean isDWallpaperEnabled() { + return mDWallpaperEnabled && mWallpaperSubjectPath != null + && !mWallpaperSubjectPath.isEmpty(); + } + + private boolean canShowDepthWallpaper() { + ScrimState currentState = mScrimController.getState(); + // Only show on KEYGUARD state when bouncer is NOT showing + return mLockScreenSubject != null + && isDWallpaperEnabled() + && !mDozing + && !mBouncerShowing + && !mGlanceableHubShowing + && currentState == ScrimState.KEYGUARD + && mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE + && !MediaViewController.get(mContext).albumArtVisible(); + } + + public void updateDepthWallpaperVisibility() { + if (mLockScreenSubject == null || !isDWallpaperEnabled()) return; + int subjectVisibility = canShowDepthWallpaper() ? View.VISIBLE : View.GONE; + if (mLockScreenSubject.getVisibility() == subjectVisibility) return; + mLockScreenSubject.post(() -> { + mLockScreenSubject.setVisibility(subjectVisibility); + if (subjectVisibility == View.VISIBLE) { + mLockScreenSubject.bringToFront(); + mLockScreenSubject.invalidate(); + } + }); + } + + public void hideDepthWallpaper() { + if (mLockScreenSubject.getVisibility() == View.GONE) return; + mLockScreenSubject.post(() -> mLockScreenSubject.setVisibility(View.GONE)); + } + + public Bitmap getResizedBitmap(Bitmap wallpaperBitmap, float xOffsetDp, float yOffsetDp) { + Rect displayBounds = mContext.getSystemService(WindowManager.class) + .getCurrentWindowMetrics() + .getBounds(); + DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + float xOffsetPx = xOffsetDp * displayMetrics.density; + float yOffsetPx = yOffsetDp * displayMetrics.density; + float ratioW = displayBounds.width() / (float) wallpaperBitmap.getWidth(); + float ratioH = displayBounds.height() / (float) wallpaperBitmap.getHeight(); + int desiredHeight = Math.round(Math.max(ratioH, ratioW) * wallpaperBitmap.getHeight()); + int desiredWidth = Math.round(Math.max(ratioH, ratioW) * wallpaperBitmap.getWidth()); + desiredHeight = Math.max(desiredHeight, 0); + desiredWidth = Math.max(desiredWidth, 0); + Bitmap scaledWallpaperBitmap = Bitmap.createScaledBitmap(wallpaperBitmap, desiredWidth, desiredHeight, true); + int xPixelShift = Math.max((desiredWidth - displayBounds.width()) / 2, 0) - Math.round(xOffsetPx); + int yPixelShift = Math.max((desiredHeight - displayBounds.height()) / 2, 0) - Math.round(yOffsetPx); + int cropWidth = Math.min(displayBounds.width(), scaledWallpaperBitmap.getWidth() - xPixelShift); + int cropHeight = Math.min(displayBounds.height(), scaledWallpaperBitmap.getHeight() - yPixelShift); + scaledWallpaperBitmap = Bitmap.createBitmap(scaledWallpaperBitmap, Math.max(xPixelShift, 0), Math.max(yPixelShift, 0), cropWidth, cropHeight); + return scaledWallpaperBitmap; + } + + public void updateDepthWallpaper(boolean forced) { + if (mLockScreenSubject == null || !isDWallpaperEnabled()) return; + boolean pathChanged = (mPreviousWallpaperPath != null && !mPreviousWallpaperPath.equals(mWallpaperSubjectPath)); + if (!mWallpaperLoaded || pathChanged || forced) { + Log.d("WallpaperDepthUtils", "updateDepthWallpaper: " + (mWallpaperLoaded || forced ? "update required" : "first load")); + new LoadWallpaperTask().execute(); + mWallpaperLoaded = true; + mPreviousWallpaperPath = mWallpaperSubjectPath; + } + updateDepthWallpaperVisibility(); + } + + private class LoadWallpaperTask extends AsyncTask { + @Override + protected Drawable doInBackground(Void... voids) { + try { + Log.d("LoadWallpaperTask", "Wallpaper path: " + mWallpaperSubjectPath); + Bitmap bitmap = BitmapFactory.decodeFile(mWallpaperSubjectPath); + if (bitmap == null) { + Log.d("LoadWallpaperTask", "Failed to decode bitmap from file"); + return null; + } + Bitmap resizedBitmap = getResizedBitmap(bitmap, mOffsetX, mOffsetY); + if (resizedBitmap == null) { + Log.d("LoadWallpaperTask", "Failed to decode resized bitmap from file"); + return null; + } + if (mWallpaperBitmap != null) { + mWallpaperBitmap = null; + } + mWallpaperBitmap = resizedBitmap; + Drawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), mWallpaperBitmap); + bitmapDrawable.setAlpha(255); + mDimmingOverlay = bitmapDrawable.getConstantState().newDrawable().mutate(); + mDimmingOverlay.setTint(Color.BLACK); + return new LayerDrawable(new Drawable[]{bitmapDrawable, mDimmingOverlay}); + } catch (OutOfMemoryError e) { + Log.e("LoadWallpaperTask", "Out of memory error", e); + return null; + } catch (Exception e) { + Log.e("LoadWallpaperTask", "Error loading wallpaper", e); + return null; + } + } + + @Override + protected void onPostExecute(Drawable drawable) { + if (drawable == null || mWallpaperBitmap == null) { + Log.d("LoadWallpaperTask", "decodeFile returned nothing, skipping application of subject as background"); + mWallpaperLoaded = false; + return; + } + if (drawable != null) { + mLockScreenSubject.setBackground(drawable); + mLockScreenSubject.getBackground().setAlpha(mDWallOpacity); + mDimmingOverlay.setAlpha(Math.round(mScrimController.getScrimBehindAlpha() * 240)); + Log.d("LoadWallpaperTask", "Subject Loaded!"); + } else { + updateDepthWallpaperVisibility(); + } + } + + @Override + protected void onCancelled() { + super.onCancelled(); + mWallpaperBitmap = null; + } + } + + public void onDetachedFromWindow() { + mTunerService.removeTunable(mTunable); + mWallpaperBitmap = null; + } +} From 3e7804c0b3332c2a74a4da2e96e53123f7c482fe Mon Sep 17 00:00:00 2001 From: Zabuka_zuzu Date: Fri, 13 Mar 2026 20:32:48 +0000 Subject: [PATCH 1141/1315] SystemUI: Fixup Notification overlap on Depth Wallpaper Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/phone/CentralSurfacesImpl.java | 16 +++++++++++----- .../systemui/util/WallpaperDepthUtils.java | 8 ++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index fdece7a0cf5e..0f0e22227d35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1188,13 +1188,17 @@ private ViewGroup getScrimOverlayContainer() { private void attachCustomOverlays() { ViewGroup overlay = getScrimOverlayContainer(); + ViewGroup root = (ViewGroup) getNotificationShadeWindowView(); detachFromParent(mMediaViewController.getMediaArtScrim()); detachFromParent(mPulseViewController.getPulseView()); detachFromParent(mEdgeLightViewController.getEdgeLightView()); detachFromParent(mNowPlayingViewController.getNowPlayingView()); - overlay.addView(mMediaViewController.getMediaArtScrim(), + // Place Media Art in the true background (behind the lock screen scrim and clock) + View scrimBehind = root.findViewById(R.id.scrim_behind); + int scrimBehindIndex = Math.max(root.indexOfChild(scrimBehind), 0); + root.addView(mMediaViewController.getMediaArtScrim(), scrimBehindIndex, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -1405,15 +1409,17 @@ public void hide() { mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront); } - // Setup depth wallpaper view - attach directly to NotificationShadeWindowView root - // This is CRITICAL for iOS-style depth effect: subject must be in root container, + // Setup depth wallpaper view - insert between clock and notifications + // Z-order: wallpaper -> clock -> depth image -> notifications ViewGroup root = getNotificationShadeWindowView(); View depthWallpaperView = mWallpaperDepthUtils.getDepthWallpaperView(); if (depthWallpaperView.getParent() == null) { root.setClipChildren(false); root.setClipToPadding(false); - root.addView(depthWallpaperView); - depthWallpaperView.bringToFront(); + // Insert after KeyguardRootView (clock) but before SharedNotificationContainer + View keyguardRootView = root.findViewById(R.id.keyguard_root_view); + int insertIndex = root.indexOfChild(keyguardRootView) + 1; + root.addView(depthWallpaperView, insertIndex); } ScrimUtils.get(mContext).setWallpaperDepthUtils(mWallpaperDepthUtils); mWallpaperDepthUtils.updateDepthWallpaper(); diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java index e04359675bf1..08408dff1315 100644 --- a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java +++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java @@ -86,13 +86,10 @@ protected void onDetachedFromWindow() { }; FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, -1); mLockScreenSubject.setLayoutParams(lp); - // Note: View is attached to NotificationShadeWindowView root for iOS-style depth - // Elevation helps but primary layering comes from view hierarchy position - mLockScreenSubject.setElevation(100f); - mLockScreenSubject.setTranslationZ(100f); - // Make view non-interactive so touches pass through to elements below + // Make view non-interactive so touches pass through to notifications mLockScreenSubject.setClickable(false); mLockScreenSubject.setFocusable(false); + mLockScreenSubject.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); } public static WallpaperDepthUtils getInstance(Context context, ScrimController scrimController) { @@ -208,7 +205,6 @@ public void updateDepthWallpaperVisibility() { mLockScreenSubject.post(() -> { mLockScreenSubject.setVisibility(subjectVisibility); if (subjectVisibility == View.VISIBLE) { - mLockScreenSubject.bringToFront(); mLockScreenSubject.invalidate(); } }); From e7c2f60b7e3e4a6ceef238dcd37b4538c5b2948f Mon Sep 17 00:00:00 2001 From: Zabuka_zuzu Date: Sat, 21 Mar 2026 22:34:38 +0000 Subject: [PATCH 1142/1315] SystemUI: Fixup QS Expandable overlap on Depth Wallpaper Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/shade/QuickSettingsControllerImpl.java | 1 + .../SystemUI/src/com/android/systemui/util/ScrimUtils.kt | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index a4c102bef2db..2b985c8545f1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1133,6 +1133,7 @@ void updateExpansion() { mDepthController.setQsPanelExpansion(qsExpansionFraction); mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction); mShadeRepository.setQsExpansion(qsExpansionFraction); + com.android.systemui.util.ScrimUtils.get().setQsExpansion(qsExpansionFraction); // TODO (b/265193930): remove dependency on NPVC float shadeExpandedFraction = mBarState == KEYGUARD diff --git a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt index d94f9fdc29d0..c2f99910802f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt @@ -186,7 +186,10 @@ class ScrimUtils private constructor(context: Context?) { fun setQsVisible(visible: Boolean) { if (mQsVisible.getAndSet(visible) != visible) { listeners.notifyOnMain { it.onQsVisibilityChanged(visible) } - if (!visible && mStateIsKeyguard) { + if (visible) { + mWallpaperDepthUtils?.getDepthWallpaperView()?.translationZ = -100f + } else if (mStateIsKeyguard) { + mWallpaperDepthUtils?.getDepthWallpaperView()?.translationZ = 0f mainHandler.postDelayed({ mWallpaperDepthUtils?.updateDepthWallpaper() mWallpaperDepthUtils?.updateDepthWallpaperVisibility() @@ -305,12 +308,13 @@ class ScrimUtils private constructor(context: Context?) { fun setQsExpansion(expansion: Float) { val fullyCollapsed = expansion <= 0f if (fullyCollapsed) { + mWallpaperDepthUtils?.getDepthWallpaperView()?.translationZ = 0f if (mStateIsKeyguard) { mWallpaperDepthUtils?.updateDepthWallpaper() mWallpaperDepthUtils?.updateDepthWallpaperVisibility() } } else { - mWallpaperDepthUtils?.hideDepthWallpaper() + mWallpaperDepthUtils?.getDepthWallpaperView()?.translationZ = -100f } } From 5d66d937bc0b5c6bbbb4aa1e1d4089d720c008bb Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 30 Apr 2026 12:43:10 +0000 Subject: [PATCH 1143/1315] SystemUI: DynamicBar: Hide depth wallpaper when keyguard panel visible Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../sections/AxDynamicBarKeyguardChipSection.kt | 8 ++++++++ .../systemui/util/WallpaperDepthUtils.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index beeb77655238..d20721615d40 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -21,6 +21,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.util.ScrimUtils +import com.android.systemui.util.WallpaperDepthUtils import javax.inject.Inject import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.collectLatest @@ -122,6 +123,7 @@ constructor( expanded: Boolean, lowUdfps: Boolean, ) { + WallpaperDepthUtils.get()?.setDynamicBarExpanded(expanded) rebindPreDrawAction(constraintLayout, expanded) TransitionManager.endTransitions(constraintLayout) if (expanded) { @@ -147,12 +149,17 @@ constructor( v.alpha = 1f if (v.visibility != visibility) v.visibility = visibility } + WallpaperDepthUtils.get()?.let { + if (visibility == View.INVISIBLE) it.hideDepthWallpaper() + else it.updateDepthWallpaperVisibility() + } } private fun enforceHidden(constraintLayout: ConstraintLayout) { hiddenTargets(constraintLayout).forEach { v -> if (v.visibility != View.INVISIBLE) v.visibility = View.INVISIBLE } + WallpaperDepthUtils.get()?.hideDepthWallpaper() } override fun applyConstraints(constraintSet: ConstraintSet) { @@ -238,6 +245,7 @@ constructor( TransitionManager.endTransitions(constraintLayout) enforceAction?.let { ScrimUtils.get().removeKeyguardPreDrawAction(it) } enforceAction = null + WallpaperDepthUtils.get()?.setDynamicBarExpanded(false) expansionHandle?.dispose() expansionHandle = null bindHandle?.dispose() diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java index 08408dff1315..52e05f9938aa 100644 --- a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java +++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java @@ -64,6 +64,7 @@ public class WallpaperDepthUtils { private boolean mDozing; private boolean mBouncerShowing; private boolean mGlanceableHubShowing; + private boolean mDynamicBarExpanded; private boolean mWallpaperLoaded = false; private String mPreviousWallpaperPath; private Bitmap mWallpaperBitmap; @@ -98,6 +99,10 @@ public static WallpaperDepthUtils getInstance(Context context, ScrimController s } return instance; } + + public static WallpaperDepthUtils get() { + return instance; + } public void onDozingChanged(boolean dozing) { if (mDozing == dozing) { @@ -123,6 +128,16 @@ public void onBouncerShowingChanged(boolean showing) { } } + public void setDynamicBarExpanded(boolean expanded) { + if (mDynamicBarExpanded == expanded) return; + mDynamicBarExpanded = expanded; + if (expanded) { + hideDepthWallpaper(); + } else { + updateDepthWallpaperVisibility(); + } + } + public void onGlanceableHubShowingChanged(boolean showing) { if (mGlanceableHubShowing == showing) { return; @@ -193,6 +208,7 @@ && isDWallpaperEnabled() && !mDozing && !mBouncerShowing && !mGlanceableHubShowing + && !mDynamicBarExpanded && currentState == ScrimState.KEYGUARD && mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE && !MediaViewController.get(mContext).albumArtVisible(); From 15964f79f6726f708707e72f2e9c7ce4ae949f6f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 28 Apr 2026 07:02:32 +0530 Subject: [PATCH 1144/1315] SystemUI: DynamicBar: Hide weather and smartspace on expand Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../view/layout/sections/AxDynamicBarKeyguardChipSection.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index d20721615d40..91195b5b8718 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -41,6 +41,11 @@ private val HIDDEN_VIEW_IDS = listOf( R.id.keyguard_widgets, R.id.shared_notification_container, R.id.notificationShelf, + R.id.bc_smartspace_view, + R.id.smartspace_card_pager, + R.id.smartspace_page_indicator, + R.id.keyguard_slice_view, + R.id.keyguard_weather_area, ) private fun Float.dpToPx(context: Context): Int = From fc63e6c4baae217134d5abcbb6ea35d1c5004067 Mon Sep 17 00:00:00 2001 From: cjh1249131356 Date: Sat, 6 Jan 2024 21:48:51 +0800 Subject: [PATCH 1145/1315] SystemUI: Implement RefreshRateManager [1/2] Includes: - Per-app refresh rate config - Extreme refresh rate (Force all apps to run in maximum refresh rate) - Temp refresh rate allowed (Used in MEMC) - Refresh rate QS tile Signed-off-by: cjh1249131356 [nurkeinneid: move to fwb and drop sdk extensions/dependencies] Co-authored-by: NurKeinNeid Signed-off-by: NurKeinNeid Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../FullscreenTaskStackChangeListener.kt | 84 ++++ .../android/app/SystemServiceRegistry.java | 12 + core/java/android/content/Context.java | 11 + core/java/android/provider/Settings.java | 12 + .../matrixx/DisplayRefreshRateHelper.java | 108 +++++ .../FullscreenTaskStackChangeListener.java | 136 ++++++ .../android/internal/util/matrixx/Utils.java | 16 + .../display/IRefreshRateListener.aidl | 14 + .../display/IRefreshRateManagerService.aidl | 42 ++ .../derpfest/display/RefreshRateManager.java | 173 +++++++ .../SystemUI/res/values/matrixx_strings.xml | 1 + .../systemui/qs/tiles/RefreshRateTile.java | 256 +++++++++++ .../systemui/qs/tiles/RefreshRateTile.kt | 255 ----------- services/api/current.txt | 2 +- .../server/DerpFestSystemExService.java | 250 +++++++++++ .../server/DisplayRefreshRateController.java | 422 ++++++++++++++++++ .../com/android/server/SystemService.java | 2 +- .../server/wm/TopActivityRecorder.java | 134 ++++++ .../java/com/android/server/SystemServer.java | 6 + services/proguard.flags | 3 + 20 files changed, 1682 insertions(+), 257 deletions(-) create mode 100644 SystemUI/shared/src/com/android/systemui/shared/system/FullscreenTaskStackChangeListener.kt create mode 100644 core/java/com/android/internal/util/matrixx/DisplayRefreshRateHelper.java create mode 100644 core/java/com/android/internal/util/matrixx/FullscreenTaskStackChangeListener.java create mode 100644 core/java/org/derpfest/display/IRefreshRateListener.aidl create mode 100644 core/java/org/derpfest/display/IRefreshRateManagerService.aidl create mode 100644 core/java/org/derpfest/display/RefreshRateManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt create mode 100644 services/core/java/com/android/server/DerpFestSystemExService.java create mode 100644 services/core/java/com/android/server/DisplayRefreshRateController.java create mode 100644 services/core/java/com/android/server/wm/TopActivityRecorder.java diff --git a/SystemUI/shared/src/com/android/systemui/shared/system/FullscreenTaskStackChangeListener.kt b/SystemUI/shared/src/com/android/systemui/shared/system/FullscreenTaskStackChangeListener.kt new file mode 100644 index 000000000000..081d051770b9 --- /dev/null +++ b/SystemUI/shared/src/com/android/systemui/shared/system/FullscreenTaskStackChangeListener.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.shared.system + +import android.app.ActivityTaskManager +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.IActivityTaskManager +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.os.RemoteException +import android.util.Log + +/** + * TaskStackChangeListener that ignores freeform / mini-window focuses change +*/ +open class FullscreenTaskStackChangeListener( + private val context: Context, + private val observeActivityChange: Boolean = false +) : TaskStackChangeListener { + + var debug = false + var debugTag = DEFAULT_TAG + + private val iActivityTaskManager by lazy { ActivityTaskManager.getService() } + + var topPackageName = String() + private set + var topActivityName = String() + private set + var topTaskId = INVALID_TASK_ID + private set + + private fun handleChange(newPackageName: String, newActivityName: String, newTaskId: Int) { + if (topPackageName == newPackageName && topActivityName == newActivityName) { + return + } + topActivityName = newActivityName + if (!observeActivityChange && topPackageName == newPackageName) { + return + } + topPackageName = newPackageName + topTaskId = newTaskId + if (debug) { + Log.d(debugTag, "Change: mTopPackage=$topPackageName" + + ", mTopActivity=$topActivityName" + + ", mTopTaskId=$topTaskId") + } + onFullscreenTaskChanged(topPackageName, topActivityName, topTaskId) + } + + override fun onTaskStackChanged() { + forceCheck() + } + + override fun onTaskFocusChanged(taskId: Int, focused: Boolean) { + if (focused) { + forceCheck() + } + } + + fun forceCheck() { + try { + iActivityTaskManager.focusedRootTaskInfo?.let { info -> + info.windowingMode.let { + if (it == WINDOWING_MODE_FREEFORM) { + return + } + } + info.topActivity?.let { + handleChange(it.packageName, it.className, info.taskId) + } + } + } catch (e: RemoteException) {} + } + + open fun onFullscreenTaskChanged(packageName: String, activityName: String, taskId: Int) {} + + companion object { + private const val DEFAULT_TAG = "FullscreenTaskStackChangeListener" + } +} diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 139beec3a703..65fdf25d6c58 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -309,6 +309,9 @@ import com.android.internal.util.Preconditions; import com.android.modules.utils.ravenwood.RavenwoodHelper; +import org.derpfest.display.IRefreshRateManagerService; +import org.derpfest.display.RefreshRateManager; + import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -1953,6 +1956,15 @@ public IntrusionDetectionManager createService(ContextImpl ctx) } }); + registerService(Context.REFRESH_RATE_MANAGER_SERVICE, RefreshRateManager.class, + new CachedServiceFetcher() { + @Override + public RefreshRateManager createService(ContextImpl ctx) { + IBinder binder = ServiceManager.getService(Context.REFRESH_RATE_MANAGER_SERVICE); + IRefreshRateManagerService service = IRefreshRateManagerService.Stub.asInterface(binder); + return new RefreshRateManager(ctx.getOuterContext(), service); + }}); + if (interactiveChooser()) { registerService( Context.CHOOSER_SERVICE, diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c020adb0a63c..c9bf1b8935a4 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4668,6 +4668,7 @@ public abstract boolean startInstrumentation(@NonNull ComponentName className, MEDIA_QUALITY_SERVICE, ADVANCED_PROTECTION_SERVICE, ANOMALY_DETECTOR_SERVICE, + REFRESH_RATE_MANAGER_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -7219,6 +7220,16 @@ public final T getSystemService(@NonNull Class serviceClass) { @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) public static final String MEDIA_QUALITY_SERVICE = "media_quality"; + /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link org.derpfest.display.RefreshRateManager} for managing display refresh rate. + * + * @hide + * @see #getSystemService + * @see org.derpfest.display.RefreshRateManager + */ + public static final String REFRESH_RATE_MANAGER_SERVICE = "refresh_rate_ext"; + /** * Service to perform operations needed for dynamic instrumentation. * @hide diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1e6c466e4a47..b988d116342e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7601,6 +7601,18 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SHOW_APP_VOLUME = "show_app_volume"; + /** + * Per-app refresh rate config + * @hide + */ + public static final String REFRESH_RATE_CONFIG_CUSTOM = "refresh_rate_config_custom"; + + /** + * Force highest refresh rate in all apps + * @hide + */ + public static final String EXTREME_REFRESH_RATE = "extreme_refresh_rate"; + /** * Whether to show volume percentage in volume panel * @hide diff --git a/core/java/com/android/internal/util/matrixx/DisplayRefreshRateHelper.java b/core/java/com/android/internal/util/matrixx/DisplayRefreshRateHelper.java new file mode 100644 index 000000000000..07a92ef33f89 --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/DisplayRefreshRateHelper.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.util.matrixx; + +import static android.provider.Settings.System.MIN_REFRESH_RATE; +import static android.provider.Settings.System.PEAK_REFRESH_RATE; + +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; +import android.view.Display; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Comparator; + +/** @hide */ +public class DisplayRefreshRateHelper { + + private static final float DEFAULT_REFRESH_RATE = 60f; + + private static DisplayRefreshRateHelper sInstance = null; + + private final ArrayList mRefreshRateList = new ArrayList<>(); + private final Context mContext; + + public static DisplayRefreshRateHelper getInstance(Context context) { + if (sInstance == null) { + sInstance = new DisplayRefreshRateHelper(context); + } + return sInstance; + } + + private DisplayRefreshRateHelper(Context context) { + mContext = context; + initialize(); + } + + private void initialize() { + final Display.Mode mode = mContext.getDisplay().getMode(); + final Display.Mode[] modes = mContext.getDisplay().getSupportedModes(); + for (Display.Mode m : modes) { + if (m.getPhysicalWidth() == mode.getPhysicalWidth() && + m.getPhysicalHeight() == mode.getPhysicalHeight()) { + mRefreshRateList.add((int) m.getRefreshRate()); + } + } + mRefreshRateList.sort(Comparator.naturalOrder()); + } + + public ArrayList getSupportedRefreshRateList() { + return mRefreshRateList; + } + + public int getMinimumRefreshRate() { + final int refreshRate = mContext.getResources().getInteger( + R.integer.config_defaultRefreshRate); + final float defaultRefreshRate = refreshRate != 0 ? (float) refreshRate : DEFAULT_REFRESH_RATE; + final int ret = (int) Settings.System.getFloatForUser(mContext.getContentResolver(), + MIN_REFRESH_RATE, defaultRefreshRate, UserHandle.USER_SYSTEM); + if (mRefreshRateList.size() != 0 && !mRefreshRateList.contains(ret)) { + return mRefreshRateList.get(mRefreshRateList.size() - 1); + } + return ret; + } + + public int getPeakRefreshRate() { + final int refreshRate = mContext.getResources().getInteger( + R.integer.config_defaultPeakRefreshRate); + final float defaultPeakRefreshRate = refreshRate != 0 ? (float) refreshRate : DEFAULT_REFRESH_RATE; + final int ret = (int) Settings.System.getFloatForUser(mContext.getContentResolver(), + PEAK_REFRESH_RATE, defaultPeakRefreshRate, UserHandle.USER_SYSTEM); + if (mRefreshRateList.size() != 0 && !mRefreshRateList.contains(ret)) { + return mRefreshRateList.get(mRefreshRateList.size() - 1); + } + return ret; + } + + public ArrayList getRefreshRate() { + final ArrayList ret = new ArrayList<>(); + ret.add(getMinimumRefreshRate()); + ret.add(getPeakRefreshRate()); + return ret; + } + + public void setMinimumRefreshRate(int refreshRate) { + Settings.System.putFloatForUser(mContext.getContentResolver(), + MIN_REFRESH_RATE, (float) refreshRate, UserHandle.USER_SYSTEM); + } + + public void setPeakRefreshRate(int refreshRate) { + Settings.System.putFloatForUser(mContext.getContentResolver(), + PEAK_REFRESH_RATE, (float) refreshRate, UserHandle.USER_SYSTEM); + } + + public void setRefreshRate(int minRefreshRate, int peakRefreshRate) { + setMinimumRefreshRate(minRefreshRate); + setPeakRefreshRate(peakRefreshRate); + } + + public boolean isRefreshRateValid(int refreshRate) { + return mRefreshRateList.contains(refreshRate); + } +} diff --git a/core/java/com/android/internal/util/matrixx/FullscreenTaskStackChangeListener.java b/core/java/com/android/internal/util/matrixx/FullscreenTaskStackChangeListener.java new file mode 100644 index 000000000000..45de911a89d9 --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/FullscreenTaskStackChangeListener.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.util.matrixx; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.TaskStackListener; +import android.content.ComponentName; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +/** + * TaskStackListener that ignores freeform / mini-window focuses change + * + * @hide + */ +public abstract class FullscreenTaskStackChangeListener extends TaskStackListener { + + private static final String DEFAULT_TAG = "FullscreenTaskStackChangeListener"; + + private final ActivityManager mActivityManager; + private final ActivityTaskManager mActivityTaskManager; + private final IActivityTaskManager mIActivityTaskManager; + + private final boolean mObserveActivityChange; + + private boolean mDebug = false; + private String mDebugTag = DEFAULT_TAG; + + private boolean mListening = false; + + private String mTopPackage = ""; + private String mTopActivity = ""; + private int mTopTaskId = INVALID_TASK_ID; + + public FullscreenTaskStackChangeListener(Context context) { + this(context, false); + } + + public FullscreenTaskStackChangeListener(Context context, boolean observeActivity) { + mActivityManager = context.getSystemService(ActivityManager.class); + mActivityTaskManager = ActivityTaskManager.getInstance(); + mIActivityTaskManager = ActivityTaskManager.getService(); + mObserveActivityChange = observeActivity; + } + + public void setDebug(boolean debug) { + mDebug = debug; + } + + public void setDebugTag(String tag) { + mDebugTag = tag; + } + + public void setListening(boolean listening) { + if (mListening != listening) { + if (listening) { + mActivityTaskManager.registerTaskStackListener(this); + mListening = true; + } else { + mActivityTaskManager.unregisterTaskStackListener(this); + mListening = false; + } + } + } + + private void handleChange(String newPackageName, String newActivityName, int newTaskId) { + if (mTopPackage.equals(newPackageName) && mTopActivity.equals(newActivityName)) { + return; + } + mTopActivity = newActivityName; + if (!mObserveActivityChange && mTopPackage.equals(newPackageName)) { + return; + } + mTopPackage = newPackageName; + mTopTaskId = newTaskId; + if (mDebug) { + Log.d(mDebugTag, "Change: mTopPackage=" + mTopPackage + + ", mTopActivity=" + mTopActivity + + ", mTopTaskId=" + mTopTaskId); + } + onFullscreenTaskChanged(mTopPackage, mTopActivity, mTopTaskId); + } + + @Override + public void onTaskStackChanged() { + forceCheck(); + } + + @Override + public void onTaskFocusChanged(int taskId, boolean focused) { + if (focused) { + forceCheck(); + } + } + + public void forceCheck() { + try { + final ActivityTaskManager.RootTaskInfo info = mIActivityTaskManager.getFocusedRootTaskInfo(); + if (info == null) { + return; + } + if (info.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + return; + } + final ComponentName topActivity = info.topActivity; + if (topActivity == null) { + return; + } + handleChange(topActivity.getPackageName(), topActivity.getClassName(), info.taskId); + } catch (RemoteException e) {} + } + + public String getTopPackageName() { + return mTopPackage; + } + + public String getTopActivityName() { + return mTopActivity; + } + + public int getTopTaskId() { + return mTopTaskId; + } + + public abstract void onFullscreenTaskChanged( + String packageName, String activityName, int taskId); +} diff --git a/core/java/com/android/internal/util/matrixx/Utils.java b/core/java/com/android/internal/util/matrixx/Utils.java index a4263c1f4cc3..a8a71c935411 100644 --- a/core/java/com/android/internal/util/matrixx/Utils.java +++ b/core/java/com/android/internal/util/matrixx/Utils.java @@ -22,6 +22,7 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.role.RoleManager; import android.bluetooth.BluetoothAdapter; import android.content.ContentResolver; import android.content.Context; @@ -63,6 +64,7 @@ import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; import android.util.Log; @@ -210,6 +212,20 @@ private static OverlayIdentifier getOverlayID(OverlayManager overlayManager, Str return null; } + public static String getDefaultLauncher(Context context) { + final RoleManager roleManager = context.getSystemService(RoleManager.class); + final String packageName = CollectionUtils.firstOrNull( + roleManager.getRoleHolders(RoleManager.ROLE_HOME)); + return packageName != null ? packageName : ""; + } + + public static void forceStopDefaultLauncher(Context context) { + final ActivityManager activityManager = context.getSystemService(ActivityManager.class); + try { + activityManager.forceStopPackageAsUser(getDefaultLauncher(context), UserHandle.USER_CURRENT); + } catch (Exception ignored) {} + } + public static class SleepModeController { private final Resources mResources; private final Context mUiContext; diff --git a/core/java/org/derpfest/display/IRefreshRateListener.aidl b/core/java/org/derpfest/display/IRefreshRateListener.aidl new file mode 100644 index 000000000000..4a14c48d9d8d --- /dev/null +++ b/core/java/org/derpfest/display/IRefreshRateListener.aidl @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2023-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.derpfest.display; + +/** @hide */ +oneway interface IRefreshRateListener { + + void onRequestedRefreshRate(int refreshRate); + + void onRequestedMemcRefreshRate(int refreshRate); +} diff --git a/core/java/org/derpfest/display/IRefreshRateManagerService.aidl b/core/java/org/derpfest/display/IRefreshRateManagerService.aidl new file mode 100644 index 000000000000..39a94d7e0413 --- /dev/null +++ b/core/java/org/derpfest/display/IRefreshRateManagerService.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.derpfest.display; + +import org.derpfest.display.IRefreshRateListener; + +/** @hide */ +interface IRefreshRateManagerService { + + /* Request MEMC required refresh rate in video apps */ + void requestMemcRefreshRate(in int refreshRate); + + /* Restore refresh rate after exiting MEMC mode */ + void clearRequestedMemcRefreshRate(); + + /* Get user preferred refresh rate for specific package, return -1 if not set */ + int getRefreshRateForPackage(in String packageName); + + /* Request user preferred refresh rate for specific package */ + void setRefreshRateForPackage(in String packageName, in int refreshRate); + + /* Reset user preferred refresh rate for specific package (Follow system) */ + void unsetRefreshRateForPackage(in String packageName); + + /* Force highest refresh rate in every apps, unless MEMC is running */ + void setExtremeRefreshRateEnabled(in boolean enabled); + + /* Get user preferred refresh in current app, return -1 if not set */ + int getRequestedRefreshRate(); + + /* Get current MEMC requested refresh rate, return -1 if not set */ + int getRequestedMemcRefreshRate(); + + /* Register listener to listen requested refresh rate change */ + boolean registerRefreshRateListener(in IRefreshRateListener listener); + + /* Unregister listener to listen requested refresh rate change */ + boolean unregisterRefreshRateListener(in IRefreshRateListener listener); +} diff --git a/core/java/org/derpfest/display/RefreshRateManager.java b/core/java/org/derpfest/display/RefreshRateManager.java new file mode 100644 index 000000000000..38e26f9b85c8 --- /dev/null +++ b/core/java/org/derpfest/display/RefreshRateManager.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.derpfest.display; + +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.util.matrixx.Utils; +import com.android.internal.util.matrixx.DisplayRefreshRateHelper; + +/** @hide */ +@SystemService(Context.REFRESH_RATE_MANAGER_SERVICE) +public class RefreshRateManager { + + private static final String TAG = "RefreshRateManager"; + + private final Context mContext; + private final DisplayRefreshRateHelper mDisplayRefreshRateHelper; + private final IRefreshRateManagerService mService; + + public RefreshRateManager(Context context, IRefreshRateManagerService service) { + mContext = context; + mService = service; + mDisplayRefreshRateHelper = DisplayRefreshRateHelper.getInstance(context); + } + + public void requestMemcRefreshRate(int refreshRate) { + if (mService == null) { + Slog.e(TAG, "Failed to request memc refresh rate. Service is null"); + return; + } + if (refreshRate > 0f && + !mDisplayRefreshRateHelper.isRefreshRateValid(refreshRate)) { + Slog.e(TAG, "Failed to request memc refresh rate. Invalid refresh rate"); + return; + } + try { + mService.requestMemcRefreshRate(refreshRate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void clearRequestedMemcRefreshRate() { + if (mService == null) { + Slog.e(TAG, "Failed to clear memc refresh rate. Service is null"); + return; + } + try { + mService.clearRequestedMemcRefreshRate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public int getRefreshRateForPackage(String packageName) { + if (mService == null) { + Slog.e(TAG, "Failed to get refresh rate for package. Service is null"); + return -1; + } + if (!Utils.isPackageInstalled(mContext, packageName)) { + Slog.e(TAG, "Failed to get refresh rate for package. Package " + packageName + " is unavailable"); + return -1; + } + try { + return mService.getRefreshRateForPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void setRefreshRateForPackage(String packageName, int refreshRate) { + if (mService == null) { + Slog.e(TAG, "Failed to set refresh rate for package. Service is null"); + return; + } + if (!Utils.isPackageInstalled(mContext, packageName)) { + Slog.e(TAG, "Failed to set refresh rate for package. Package " + packageName + " is unavailable"); + return; + } + if (refreshRate > 0f && + !mDisplayRefreshRateHelper.isRefreshRateValid(refreshRate)) { + Slog.e(TAG, "Failed to set refresh rate for package. Invalid refresh rate"); + return; + } + try { + mService.setRefreshRateForPackage(packageName, refreshRate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void unsetRefreshRateForPackage(String packageName) { + if (mService == null) { + Slog.e(TAG, "Failed to unset refresh rate for package. Service is null"); + return; + } + if (!Utils.isPackageInstalled(mContext, packageName)) { + Slog.e(TAG, "Failed to unset refresh rate for package. Package " + packageName + " is unavailable"); + return; + } + try { + mService.unsetRefreshRateForPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void setExtremeRefreshRateEnabled(boolean enabled) { + if (mService == null) { + Slog.e(TAG, "Failed to set extreme refresh rate. Service is null"); + return; + } + try { + mService.setExtremeRefreshRateEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public int getRequestedRefreshRate() { + if (mService == null) { + Slog.e(TAG, "Failed to get requested refresh rate. Service is null"); + return -1; + } + try { + return mService.getRequestedRefreshRate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public int getRequestedMemcRefreshRate() { + if (mService == null) { + Slog.e(TAG, "Failed to get requested memc refresh rate. Service is null"); + return -1; + } + try { + return mService.getRequestedMemcRefreshRate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public boolean registerRefreshRateListener(IRefreshRateListener.Stub listener) { + if (mService == null) { + Slog.e(TAG, "Failed to register refresh rate listener. Service is null"); + return false; + } + try { + return mService.registerRefreshRateListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public boolean unregisterRefreshRateListener(IRefreshRateListener.Stub listener) { + if (mService == null) { + Slog.e(TAG, "Failed to unregister refresh rate listener. Service is null"); + return false; + } + try { + return mService.unregisterRefreshRateListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index da296f2c8346..e0ffa984b2c3 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -158,6 +158,7 @@ Refresh rate + Unknown Auto diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java new file mode 100644 index 000000000000..e54e92a4d4ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2022-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.qs.tiles; + +import static android.provider.Settings.System.MIN_REFRESH_RATE; +import static android.provider.Settings.System.PEAK_REFRESH_RATE; + +import android.content.ComponentName; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.provider.Settings; +import android.service.quicksettings.Tile; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.util.matrixx.DisplayRefreshRateHelper; + +import com.android.systemui.animation.Expandable; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; +import com.android.systemui.util.settings.SystemSettings; + +import java.util.ArrayList; + +import javax.inject.Inject; + +import org.derpfest.display.IRefreshRateListener; +import org.derpfest.display.RefreshRateManager; + +public class RefreshRateTile extends QSTileImpl { + + public static final String TILE_SPEC = "refresh_rate"; + + private static final Intent SCREEN_REFRESH_RATE_SETTINGS = + new Intent().setComponent(new ComponentName( + "com.android.settings", + "com.android.settings.Settings$ScreenRefreshRateActivity")); + + private final ArrayList mSupportedList; + private final DisplayRefreshRateHelper mHelper; + private final RefreshRateManager mManager; + private final SettingsObserver mObserver; + + private final Icon mIcon = ResourceIcon.get(R.drawable.ic_refresh_rate); + + private int mMinRefreshRate; + private int mPeakRefreshRate; + + private boolean mUpdateRefreshRate = true; + + private int mRequestedRefreshRate = -1; + private int mRequestedMemcRefreshRate = -1; + + private boolean mRegistered = false; + + private final IRefreshRateListener.Stub mRefreshRateListener = + new IRefreshRateListener.Stub() { + @Override + public void onRequestedRefreshRate(int refreshRate) { + mRequestedRefreshRate = refreshRate; + refreshState(); + } + + @Override + public void onRequestedMemcRefreshRate(int refreshRate) { + mRequestedMemcRefreshRate = refreshRate; + refreshState(); + } + }; + + @Inject + public RefreshRateTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + SystemSettings systemSettings) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, + metricsLogger, statusBarStateController, activityStarter, qsLogger); + + mHelper = DisplayRefreshRateHelper.getInstance(mContext); + mManager = mContext.getSystemService(RefreshRateManager.class); + mSupportedList = mHelper.getSupportedRefreshRateList(); + + mObserver = new SettingsObserver(mHandler, systemSettings); + + mMinRefreshRate = mHelper.getMinimumRefreshRate(); + mPeakRefreshRate = mHelper.getPeakRefreshRate(); + } + + @Override + public boolean isAvailable() { + return mSupportedList.size() > 1; + } + + @Override + public State newTileState() { + return new State(); + } + + @Override + protected void handleInitialize() { + mObserver.observe(); + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + mObserver.unobserve(); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + if (mRequestedRefreshRate > 0 || mRequestedMemcRefreshRate > 0) { + return; + } + if (!isRefreshRateValid()) { + mMinRefreshRate = mSupportedList.get(mSupportedList.size() - 1); + mPeakRefreshRate = mMinRefreshRate; + } else if (mSupportedList.indexOf(mPeakRefreshRate) == mSupportedList.size() - 1) { + if (mMinRefreshRate == mPeakRefreshRate) { + mMinRefreshRate = mSupportedList.get(0); + } else { + mMinRefreshRate = mSupportedList.get(mSupportedList.indexOf(mMinRefreshRate) + 1); + } + mPeakRefreshRate = mMinRefreshRate; + } else { + mPeakRefreshRate = mSupportedList.get(mSupportedList.indexOf(mPeakRefreshRate) + 1); + } + mUpdateRefreshRate = false; + mHelper.setRefreshRate(mMinRefreshRate, mPeakRefreshRate); + mUpdateRefreshRate = true; + } + + @Override + public Intent getLongClickIntent() { + return SCREEN_REFRESH_RATE_SETTINGS; + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.refresh_rate_tile_label); + } + + @Override + protected void handleUpdateState(State state, Object arg) { + if (!isAvailable()) { + return; + } + + state.state = mRequestedRefreshRate > 0 || mRequestedMemcRefreshRate > 0 + ? Tile.STATE_UNAVAILABLE : Tile.STATE_ACTIVE; + state.icon = mIcon; + state.label = mContext.getString(R.string.refresh_rate_tile_label); + state.contentDescription = mContext.getString(R.string.refresh_rate_tile_label); + state.secondaryLabel = getRefreshRateLabel(); + } + + @Override + public void handleSetListening(boolean listening) { + super.handleSetListening(listening); + + if (!isAvailable()) { + return; + } + + if (listening && !mRegistered) { + mManager.registerRefreshRateListener(mRefreshRateListener); + mRegistered = true; + } else if (!listening && mRegistered) { + mManager.unregisterRefreshRateListener(mRefreshRateListener); + mRegistered = false; + } + } + + private boolean isRefreshRateValid() { + return mHelper.isRefreshRateValid(mMinRefreshRate) && + mHelper.isRefreshRateValid(mPeakRefreshRate) && + mMinRefreshRate <= mPeakRefreshRate; + } + + private String getRefreshRateLabel() { + if (mRequestedMemcRefreshRate > 0) { + return String.valueOf((int) mRequestedMemcRefreshRate) + " Hz"; + } + if (mRequestedRefreshRate > 0) { + return String.valueOf((int) mRequestedRefreshRate) + " Hz"; + } + if (!isRefreshRateValid()) { + return mContext.getString(R.string.refresh_rate_unknown); + } + if (mMinRefreshRate == mPeakRefreshRate) { + return String.valueOf(mPeakRefreshRate) + " Hz"; + } + return String.valueOf(mMinRefreshRate) + " ~ " + String.valueOf(mPeakRefreshRate) + " Hz"; + } + + private final class SettingsObserver extends ContentObserver { + + private final SystemSettings mSystemSettings; + + private boolean mObserving = false; + + SettingsObserver(Handler handler, SystemSettings systemSettings) { + super(handler); + mSystemSettings = systemSettings; + } + + void observe() { + if (mObserving) { + return; + } + mSystemSettings.registerContentObserverSync(MIN_REFRESH_RATE, this); + mSystemSettings.registerContentObserverSync(PEAK_REFRESH_RATE, this); + mObserving = true; + } + + void unobserve() { + if (!mObserving) { + return; + } + mSystemSettings.unregisterContentObserverSync(this); + mObserving = false; + } + + @Override + public void onChange(boolean selfChange) { + if (mUpdateRefreshRate) { + mMinRefreshRate = mHelper.getMinimumRefreshRate(); + mPeakRefreshRate = mHelper.getPeakRefreshRate(); + } + refreshState(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt deleted file mode 100644 index 9782f37b8fa8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * 2021 AOSP-Krypton Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.qs.tiles - -import android.content.ComponentName -import android.content.Intent -import android.database.ContentObserver -import android.hardware.display.DisplayManager -import android.net.Uri -import android.os.Handler -import android.os.Looper -import android.provider.DeviceConfig -import android.provider.Settings.System.MIN_REFRESH_RATE -import android.provider.Settings.System.PEAK_REFRESH_RATE -import android.service.quicksettings.Tile -import android.util.Log -import android.view.Display - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent -import com.android.internal.logging.MetricsLogger -import com.android.systemui.animation.Expandable -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.plugins.qs.QSTile.Icon -import com.android.systemui.plugins.qs.QSTile.State -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.logging.QSLogger -import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.qs.QSHost -import com.android.systemui.qs.QsEventLogger -import com.android.systemui.res.R -import com.android.systemui.util.settings.SystemSettings - -import javax.inject.Inject - -class RefreshRateTile @Inject constructor( - host: QSHost, - uiEventLogger: QsEventLogger, - @Background backgroundLooper: Looper, - @Main private val mainHandler: Handler, - falsingManager: FalsingManager, - metricsLogger: MetricsLogger, - statusBarStateController: StatusBarStateController, - activityStarter: ActivityStarter, - qsLogger: QSLogger, - private val systemSettings: SystemSettings, -): QSTileImpl( - host, - uiEventLogger, - backgroundLooper, - mainHandler, - falsingManager, - metricsLogger, - statusBarStateController, - activityStarter, - qsLogger, -) { - - private val settingsObserver: SettingsObserver - private val tileLabel: String - private val autoModeLabel: String - private val defaultPeakRefreshRate: Float - - private var ignoreSettingsChange = false - private var refreshRateMode = Mode.MIN - private var peakRefreshRate = DEFAULT_REFRESH_RATE - - init { - with (mContext.resources) { - tileLabel = getString(R.string.refresh_rate_tile_label) - autoModeLabel = getString(R.string.auto_mode_label) - defaultPeakRefreshRate = getDefaultPeakRefreshRate(getInteger( - com.android.internal.R.integer.config_defaultPeakRefreshRate).toFloat()) - } - - val display: Display? = mContext.getSystemService( - DisplayManager::class.java)!!.getDisplay(Display.DEFAULT_DISPLAY) - display?.let { - it.getSupportedModes().forEach({ mode -> - mode.refreshRate.let { rr -> - if (rr > peakRefreshRate) peakRefreshRate = rr - } - }) - } ?: run { Log.w(Companion.TAG, "No valid default display") } - logD("peakRefreshRate = $peakRefreshRate, defaultPeakRefreshRate = $defaultPeakRefreshRate") - settingsObserver = SettingsObserver() - } - - override fun newTileState() = - State().also { - it.icon = icon - it.state = Tile.STATE_ACTIVE - } - - override fun getLongClickIntent() = displaySettingsIntent - - override fun isAvailable(): Boolean { - val displayManager = mContext.getSystemService(DisplayManager::class.java) - val display: Display? = displayManager?.getDisplay(Display.DEFAULT_DISPLAY) - display?.let { - val supportedRefreshRates = it.supportedModes.map { mode -> mode.refreshRate }.distinct() - return supportedRefreshRates.size > 1 - } - return false - } - - override fun getTileLabel(): CharSequence = tileLabel - - override protected fun handleInitialize() { - logD("handleInitialize") - updateMode() - settingsObserver.observe() - } - - override protected fun handleClick(expandable: Expandable?) { - logD("handleClick") - refreshRateMode = getNextMode(refreshRateMode) - logD("refreshRateMode = $refreshRateMode") - updateRefreshRateForMode(refreshRateMode) - refreshState() - } - - override protected fun handleUpdateState(state: State, arg: Any?) { - if (state.label == null) { - state.label = tileLabel - state.contentDescription = tileLabel - } - logD("handleUpdateState, state = $state") - state.secondaryLabel = getTitleForMode(refreshRateMode) - logD("secondaryLabel = ${state.secondaryLabel}") - } - - override fun getMetricsCategory(): Int = MetricsEvent.MATRIXX - - override fun destroy() { - settingsObserver.unobserve() - super.destroy() - } - - private fun updateMode() { - val minRate = systemSettings.getFloat(MIN_REFRESH_RATE, DEFAULT_REFRESH_RATE) - val maxRate = systemSettings.getFloat(PEAK_REFRESH_RATE, defaultPeakRefreshRate) - logD("minRate = $minRate, maxRate = $maxRate") - - if (minRate == maxRate) { - if (minRate == DEFAULT_REFRESH_RATE) refreshRateMode = Mode.MIN - else refreshRateMode = Mode.MAX - } else { - refreshRateMode = Mode.AUTO - } - logD("refreshRateMode = $refreshRateMode") - } - - private fun getDefaultPeakRefreshRate(def: Float): Float { - return DeviceConfig.getFloat(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, - DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, def) - } - - private fun getNextMode(mode: Mode) = - when (mode) { - Mode.AUTO -> Mode.MIN - Mode.MIN -> Mode.MAX - Mode.MAX -> Mode.AUTO - } - - private fun updateRefreshRateForMode(mode: Mode) { - logD("updateRefreshRateForMode, mode = $mode") - var minRate: Float; var maxRate: Float - when (mode) { - Mode.AUTO -> { - minRate = DEFAULT_REFRESH_RATE - maxRate = peakRefreshRate - } - Mode.MAX -> { - minRate = peakRefreshRate - maxRate = peakRefreshRate - } - Mode.MIN -> { - minRate = DEFAULT_REFRESH_RATE - maxRate = DEFAULT_REFRESH_RATE - } - } - ignoreSettingsChange = true - systemSettings.putFloat(MIN_REFRESH_RATE, minRate) - systemSettings.putFloat(PEAK_REFRESH_RATE, maxRate) - ignoreSettingsChange = false - } - - private fun getTitleForMode(mode: Mode) = - when (mode) { - Mode.AUTO -> autoModeLabel - Mode.MAX -> peakRefreshRate.toInt().toString() + "Hz" - Mode.MIN -> DEFAULT_REFRESH_RATE.toInt().toString() + "Hz" - } - - private enum class Mode { - MIN, - MAX, - AUTO, - } - - private inner class SettingsObserver: ContentObserver(mainHandler) { - private var isObserving = false - - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (!ignoreSettingsChange) updateMode() - } - - fun observe() { - if (isObserving) return - isObserving = true - systemSettings.registerContentObserverSync(MIN_REFRESH_RATE, this) - systemSettings.registerContentObserverSync(PEAK_REFRESH_RATE, this) - } - - fun unobserve() { - if (!isObserving) return - isObserving = false - systemSettings.unregisterContentObserverSync(this) - } - } - - companion object { - const val TILE_SPEC = "refresh_rate" - private const val TAG = "RefreshRateTile" - private const val DEBUG = false - - private const val DEFAULT_REFRESH_RATE = 60f - - private val icon: Icon = ResourceIcon.get(R.drawable.ic_refresh_rate) - private val displaySettingsIntent = Intent().setComponent(ComponentName("com.android.settings", - "com.android.settings.Settings\$DisplaySettingsActivity")) - - private fun logD(msg: String) { - if (DEBUG) Log.d(TAG, msg) - } - } -} diff --git a/services/api/current.txt b/services/api/current.txt index b55166c30965..9dd25a0b4367 100644 --- a/services/api/current.txt +++ b/services/api/current.txt @@ -18,7 +18,7 @@ package com.android.server { method public void onUserSwitching(@Nullable com.android.server.SystemService.TargetUser, @NonNull com.android.server.SystemService.TargetUser); method public void onUserUnlocked(@NonNull com.android.server.SystemService.TargetUser); method public void onUserUnlocking(@NonNull com.android.server.SystemService.TargetUser); - method protected final void publishBinderService(@NonNull String, @NonNull android.os.IBinder); + method public final void publishBinderService(@NonNull String, @NonNull android.os.IBinder); method protected final void publishBinderService(@NonNull String, @NonNull android.os.IBinder, boolean); field public static final int PHASE_ACTIVITY_MANAGER_READY = 550; // 0x226 field public static final int PHASE_BOOT_COMPLETED = 1000; // 0x3e8 diff --git a/services/core/java/com/android/server/DerpFestSystemExService.java b/services/core/java/com/android/server/DerpFestSystemExService.java new file mode 100644 index 000000000000..bcce412d0827 --- /dev/null +++ b/services/core/java/com/android/server/DerpFestSystemExService.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2022-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.server; + +import static android.content.Intent.ACTION_SCREEN_OFF; +import static android.content.Intent.ACTION_SCREEN_ON; +import static android.content.Intent.ACTION_USER_PRESENT; +import static android.os.Process.THREAD_PRIORITY_DEFAULT; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; + +import android.app.KeyguardManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.content.pm.LauncherApps; +import android.content.pm.UserInfo; +import android.os.Binder; +import android.os.Handler; +import android.os.PowerManager; +import android.os.UserHandle; + +import com.android.internal.util.matrixx.Utils; +import com.android.internal.util.matrixx.FullscreenTaskStackChangeListener; + +import com.android.server.LocalServices; +import com.android.server.ServiceThread; +import com.android.server.SystemService; +import com.android.server.pm.UserManagerInternal; + +import java.util.List; + +import com.android.server.DisplayRefreshRateController; + +public class DerpFestSystemExService extends SystemService { + + private static final String TAG = "DerpFestSystemExService"; + + private final ContentResolver mResolver; + + private Handler mHandler; + private ServiceThread mWorker; + + private PackageManagerInternal mPackageManagerInternal; + private UserManagerInternal mUserManagerInternal; + + private FullscreenTaskStackChangeListener mFullscreenTaskStackChangeListener; + private PackageRemovedListener mPackageRemovedListener; + private ScreenStateListener mScreenStateListener; + + public DerpFestSystemExService(Context context) { + super(context); + mResolver = context.getContentResolver(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); + mFullscreenTaskStackChangeListener = new FullscreenTaskStackChangeListener(getContext()) { + @Override + public void onFullscreenTaskChanged(String packageName, String activityName, int taskId) { + onTopFullscreenPackageChanged(packageName, taskId); + } + }; + mPackageRemovedListener = new PackageRemovedListener(); + mScreenStateListener = new ScreenStateListener(); + return; + } + + if (phase == PHASE_BOOT_COMPLETED) { + DisplayRefreshRateController.getInstance().onBootCompleted(); + mFullscreenTaskStackChangeListener.setListening(true); + mPackageRemovedListener.register(); + mScreenStateListener.register(); + return; + } + } + + @Override + public void onStart() { + mWorker = new ServiceThread(TAG, THREAD_PRIORITY_DEFAULT, false); + mWorker.start(); + mHandler = new Handler(mWorker.getLooper()); + + DisplayRefreshRateController.getInstance().initSystemExService(this); + } + + @Override + public void onUserSwitching(TargetUser from, TargetUser to) { + final int newUserId = to.getUserIdentifier(); + DisplayRefreshRateController.getInstance().onUserSwitching(newUserId); + } + + private void onPackageRemoved(String packageName) { + DisplayRefreshRateController.getInstance().onPackageRemoved(packageName); + } + + private void onScreenOff() { + DisplayRefreshRateController.getInstance().onScreenOff(); + } + + private void onScreenOn() { + DisplayRefreshRateController.getInstance().onScreenOn(); + } + + private void onScreenUnlocked() { + onTopFullscreenPackageChanged( + mFullscreenTaskStackChangeListener.getTopPackageName(), + mFullscreenTaskStackChangeListener.getTopTaskId() + ); + } + + public void onTopFullscreenPackageChanged(String packageName, int taskId) { + mHandler.post(() -> { + DisplayRefreshRateController.getInstance().onTopFullscreenPackageChanged(packageName); + }); + } + + public ContentResolver getContentResolver() { + return mResolver; + } + + public String getTopFullscreenPackage() { + return mFullscreenTaskStackChangeListener.getTopPackageName(); + } + + public int getTopFullscreenTaskId() { + return mFullscreenTaskStackChangeListener.getTopTaskId(); + } + + private int getCloneUserId() { + for (UserInfo userInfo : mUserManagerInternal.getUsers(false)) { + if (USER_TYPE_PROFILE_CLONE.equals(userInfo.userType)) { + return userInfo.id; + } + } + return -1; + } + + private void maybeCleanClonedUser(int userId) { + final List packages = mPackageManagerInternal.getInstalledApplicationsCrossUser( + 0, userId, Binder.getCallingUid()); + boolean hasUserApp = false; + for (ApplicationInfo info : packages) { + if ((info.flags & + (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) { + hasUserApp = true; + break; + } + } + if (!hasUserApp) { + mUserManagerInternal.removeUserEvenWhenDisallowed(userId); + Utils.forceStopDefaultLauncher(getContext()); + } + } + + private final class PackageRemovedListener extends LauncherApps.Callback { + + private final LauncherApps mLauncherApps; + + public PackageRemovedListener() { + mLauncherApps = getContext().getSystemService(LauncherApps.class); + } + + @Override + public void onPackageAdded(String packageName, UserHandle user) { + // Do nothing + } + + @Override + public void onPackageChanged(String packageName, UserHandle user) { + // Do nothing + } + + @Override + public void onPackageRemoved(String packageName, UserHandle user) { + final UserInfo userInfo = mUserManagerInternal.getUserInfo(user.getIdentifier()); + if (userInfo != null && USER_TYPE_PROFILE_CLONE.equals(userInfo.userType)) { + maybeCleanClonedUser(user.getIdentifier()); + return; + } + + DerpFestSystemExService.this.onPackageRemoved(packageName); + } + + @Override + public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { + // Do nothing + } + + @Override + public void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing) { + // Do nothing + } + + public void register() { + mLauncherApps.registerCallback(this, mHandler); + } + } + + private final class ScreenStateListener extends BroadcastReceiver { + + private final KeyguardManager mKeyguardManager; + private final PowerManager mPowerManager; + + private boolean mHandledUnlock = false; + + public ScreenStateListener() { + mKeyguardManager = getContext().getSystemService(KeyguardManager.class); + mPowerManager = getContext().getSystemService(PowerManager.class); + } + + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_SCREEN_OFF: + mHandledUnlock = false; + onScreenOff(); + break; + case ACTION_SCREEN_ON: + if (!mHandledUnlock && !mKeyguardManager.isKeyguardLocked()) { + onScreenUnlocked(); + } else { + onScreenOn(); + } + break; + case ACTION_USER_PRESENT: + mHandledUnlock = true; + onScreenUnlocked(); + break; + } + } + + public void register() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_SCREEN_OFF); + filter.addAction(ACTION_SCREEN_ON); + filter.addAction(ACTION_USER_PRESENT); + getContext().registerReceiverForAllUsers(this, filter, null, mHandler); + } + } +} diff --git a/services/core/java/com/android/server/DisplayRefreshRateController.java b/services/core/java/com/android/server/DisplayRefreshRateController.java new file mode 100644 index 000000000000..9cb51ae6c04a --- /dev/null +++ b/services/core/java/com/android/server/DisplayRefreshRateController.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2023-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.server; + +import static android.os.Process.THREAD_PRIORITY_DEFAULT; +import static android.provider.Settings.System.EXTREME_REFRESH_RATE; +import static android.provider.Settings.System.REFRESH_RATE_CONFIG_CUSTOM; +import static android.content.Context.REFRESH_RATE_MANAGER_SERVICE; + +import android.content.Context; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.util.matrixx.DisplayRefreshRateHelper; +import com.android.server.ServiceThread; + +import java.util.ArrayList; + +import org.derpfest.display.IRefreshRateListener; +import org.derpfest.display.IRefreshRateManagerService; +import com.android.server.DerpFestSystemExService; + +public final class DisplayRefreshRateController { + + private static final String TAG = "DisplayRefreshRateController"; + + private final Handler mHandler; + private final ServiceThread mServiceThread; + + private final Object mConfigLock = new Object(); + private final Object mListenerLock = new Object(); + + private DerpFestSystemExService mSystemExService; + + private static class InstanceHolder { + private static DisplayRefreshRateController INSTANCE = new DisplayRefreshRateController(); + } + + public static DisplayRefreshRateController getInstance() { + return InstanceHolder.INSTANCE; + } + + private final class RefreshRateListener { + final IRefreshRateListener mListener; + final IBinder.DeathRecipient mDeathRecipient; + + RefreshRateListener(IRefreshRateListener listener, + IBinder.DeathRecipient deathRecipient) { + mListener = listener; + mDeathRecipient = deathRecipient; + } + } + + private final ArrayList mListeners = new ArrayList<>(); + private final ArrayMap mAppRefreshRateConfigMap = new ArrayMap<>(); + + private int mRequestedRefreshRate = -1; + private int mRequestedMemcRefreshRate = -1; + + private boolean mExtremeMode = false; + + private DisplayRefreshRateHelper mHelper; + + private final class RefreshRateManagerService extends IRefreshRateManagerService.Stub { + @Override + public void requestMemcRefreshRate(int refreshRate) { + synchronized (mConfigLock) { + if (mRequestedMemcRefreshRate != refreshRate) { + logD("requestMemcRefreshRate, refreshRate: " + refreshRate); + mRequestedMemcRefreshRate = refreshRate; + mHandler.post(() -> notifyMemcRefreshRateChanged()); + } + } + } + + @Override + public void clearRequestedMemcRefreshRate() { + synchronized (mConfigLock) { + if (mRequestedMemcRefreshRate > 0) { + logD("clearRequestedMemcRefreshRate"); + mRequestedMemcRefreshRate = -1; + mHandler.post(() -> notifyMemcRefreshRateChanged()); + } + } + } + + @Override + public int getRefreshRateForPackage(String packageName) { + synchronized (mConfigLock) { + return mAppRefreshRateConfigMap.getOrDefault(packageName, -1); + } + } + + @Override + public void setRefreshRateForPackage(String packageName, int refreshRate) { + synchronized (mConfigLock) { + if (refreshRate > 0) { + logD("setRefreshRateForPackage, packageName: " + + packageName + ", refreshRate: " + refreshRate); + mAppRefreshRateConfigMap.put(packageName, refreshRate); + } else if (mAppRefreshRateConfigMap.containsKey(packageName)) { + logD("unsetRefreshRateForPackage, packageName: " + packageName); + mAppRefreshRateConfigMap.remove(packageName); + } + mHandler.post(() -> { + saveConfigIntoSettingsLocked(); + updateRefreshRateLocked(mSystemExService.getTopFullscreenPackage()); + }); + } + } + + @Override + public void unsetRefreshRateForPackage(String packageName) { + synchronized (mConfigLock) { + if (mAppRefreshRateConfigMap.containsKey(packageName)) { + logD("unsetRefreshRateForPackage, packageName: " + packageName); + mAppRefreshRateConfigMap.remove(packageName); + mHandler.post(() -> { + saveConfigIntoSettingsLocked(); + updateRefreshRateLocked(mSystemExService.getTopFullscreenPackage()); + }); + } + } + } + + @Override + public void setExtremeRefreshRateEnabled(boolean enabled) { + synchronized (mConfigLock) { + if (mExtremeMode != enabled) { + logD("setExtremeRefreshRateEnabled, enabled: " + enabled); + mExtremeMode = enabled; + Settings.System.putIntForUser(mSystemExService.getContentResolver(), EXTREME_REFRESH_RATE, + enabled ? 1 : 0, UserHandle.USER_CURRENT); + mHandler.post(() -> { + if (enabled) { + mRequestedRefreshRate = getMaxAllowedRefreshRate(); + notifyRefreshRateChanged(); + } else { + updateRefreshRateLocked(mSystemExService.getTopFullscreenPackage()); + } + }); + } + } + } + + @Override + public int getRequestedRefreshRate() { + synchronized (mConfigLock) { + logD("getRequestedRefreshRate, refreshRate: " + mRequestedRefreshRate); + return mRequestedRefreshRate; + } + } + + @Override + public int getRequestedMemcRefreshRate() { + synchronized (mConfigLock) { + logD("getRequestedMemcRefreshRate, refreshRate: " + mRequestedMemcRefreshRate); + return mRequestedMemcRefreshRate; + } + } + + @Override + public boolean registerRefreshRateListener(IRefreshRateListener listener) { + final IBinder listenerBinder = listener.asBinder(); + IBinder.DeathRecipient dr = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + synchronized (mListenerLock) { + for (int i = 0; i < mListeners.size(); i++) { + if (listenerBinder == mListeners.get(i).mListener.asBinder()) { + RefreshRateListener removed = mListeners.remove(i); + IBinder binder = removed.mListener.asBinder(); + if (binder != null) { + binder.unlinkToDeath(this, 0); + } + i--; + } + } + } + } + }; + + synchronized (mListenerLock) { + try { + listener.asBinder().linkToDeath(dr, 0); + mListeners.add(new RefreshRateListener(listener, dr)); + mHandler.post(() -> { + synchronized (mConfigLock) { + notifyRefreshRateChanged(listener); + notifyMemcRefreshRateChanged(listener); + } + }); + } catch (RemoteException e) { + // Client died, no cleanup needed. + return false; + } + return true; + } + } + + @Override + public boolean unregisterRefreshRateListener(IRefreshRateListener listener) { + boolean found = false; + final IBinder listenerBinder = listener.asBinder(); + synchronized (mListenerLock) { + for (int i = 0; i < mListeners.size(); i++) { + found = true; + RefreshRateListener refreshRateListener = mListeners.get(i); + if (listenerBinder == refreshRateListener.mListener.asBinder()) { + RefreshRateListener removed = mListeners.remove(i); + IBinder binder = removed.mListener.asBinder(); + if (binder != null) { + binder.unlinkToDeath(removed.mDeathRecipient, 0); + } + i--; + } + } + } + return found; + } + } + + private DisplayRefreshRateController() { + mServiceThread = new ServiceThread(TAG, THREAD_PRIORITY_DEFAULT, false); + mServiceThread.start(); + mHandler = new Handler(mServiceThread.getLooper()); + } + + public void initSystemExService(DerpFestSystemExService service) { + mSystemExService = service; + mSystemExService.publishBinderService(REFRESH_RATE_MANAGER_SERVICE, new RefreshRateManagerService()); + } + + public void onBootCompleted() { + mHandler.post(() -> { + synchronized (mConfigLock) { + logD("onBootCompleted"); + mHelper = DisplayRefreshRateHelper.getInstance(mSystemExService.getContext()); + parseSettingsIntoMapLocked(UserHandle.USER_CURRENT); + mExtremeMode = Settings.System.getIntForUser(mSystemExService.getContentResolver(), + EXTREME_REFRESH_RATE, 0, UserHandle.USER_CURRENT) != 0; + if (mExtremeMode) { + mRequestedRefreshRate = getMaxAllowedRefreshRate(); + notifyRefreshRateChanged(); + } + } + }); + } + + public void onUserSwitching(int newUserId) { + mHandler.post(() -> { + synchronized (mConfigLock) { + logD("onUserSwitching, newUserId: " + newUserId); + parseSettingsIntoMapLocked(newUserId); + + mExtremeMode = Settings.System.getIntForUser(mSystemExService.getContentResolver(), + EXTREME_REFRESH_RATE, 0, newUserId) != 0; + if (mExtremeMode) { + mRequestedRefreshRate = getMaxAllowedRefreshRate(); + notifyRefreshRateChanged(); + } else { + updateRefreshRateLocked(mSystemExService.getTopFullscreenPackage()); + } + } + }); + } + + public void onPackageRemoved(String packageName) { + mHandler.post(() -> { + synchronized (mConfigLock) { + logD("onPackageRemoved, packageName: " + packageName); + if (mAppRefreshRateConfigMap.containsKey(packageName)) { + logD("unsetRefreshRateForPackage, packageName: " + packageName); + mAppRefreshRateConfigMap.remove(packageName); + saveConfigIntoSettingsLocked(); + updateRefreshRateLocked(mSystemExService.getTopFullscreenPackage()); + } + } + }); + } + + public void onScreenOff() { + mHandler.post(() -> { + synchronized (mConfigLock) { + if (mRequestedRefreshRate != -1) { + logD("onScreenOff, restore refresh rate"); + mRequestedRefreshRate = -1; + notifyRefreshRateChanged(); + } + } + }); + } + + public void onScreenOn() { + mHandler.post(() -> { + synchronized (mConfigLock) { + if (mExtremeMode) { + logD("onScreenOn, set refresh rate to highest"); + mRequestedRefreshRate = getMaxAllowedRefreshRate(); + notifyRefreshRateChanged(); + } + } + }); + } + + public void onTopFullscreenPackageChanged(String packageName) { + mHandler.post(() -> { + synchronized (mConfigLock) { + updateRefreshRateLocked(packageName); + } + }); + } + + private void updateRefreshRateLocked(String packageName) { + if (mExtremeMode) { + logD("Skip update refresh rate due to in extreme mode"); + } else if (mAppRefreshRateConfigMap.containsKey(packageName)) { + mRequestedRefreshRate = mAppRefreshRateConfigMap.get(packageName); + notifyRefreshRateChanged(); + logD("Requested refresh rate " + mRequestedRefreshRate + " for package: " + packageName); + } else if (mRequestedRefreshRate > 0) { + mRequestedRefreshRate = -1; + notifyRefreshRateChanged(); + logD("Restore refresh rate"); + } + } + + private void notifyRefreshRateChanged() { + synchronized (mListenerLock) { + logD("notifyRefreshRateChanged, refreshRate: " + mRequestedRefreshRate); + for (RefreshRateListener listener : mListeners) { + notifyRefreshRateChanged(listener.mListener); + } + } + } + + private void notifyRefreshRateChanged(IRefreshRateListener listener) { + try { + listener.onRequestedRefreshRate(mRequestedRefreshRate); + } catch (RemoteException | RuntimeException e) { + logE("Failed to notify refresh rate changed"); + } + } + + private void notifyMemcRefreshRateChanged() { + synchronized (mListenerLock) { + logD("notifyMemcRefreshRateChanged, refreshRate: " + mRequestedMemcRefreshRate); + for (RefreshRateListener listener : mListeners) { + notifyMemcRefreshRateChanged(listener.mListener); + } + } + } + + private void notifyMemcRefreshRateChanged(IRefreshRateListener listener) { + try { + listener.onRequestedMemcRefreshRate(mRequestedMemcRefreshRate); + } catch (RemoteException | RuntimeException e) { + logE("Failed to notify memc refresh rate changed"); + } + } + + private int getMaxAllowedRefreshRate() { + final ArrayList supportedList = mHelper.getSupportedRefreshRateList(); + if (supportedList.size() > 0) { + return supportedList.get(supportedList.size() - 1); + } + return -1; + } + + private void parseSettingsIntoMapLocked(int userId) { + mAppRefreshRateConfigMap.clear(); + final String settings = Settings.System.getStringForUser(mSystemExService.getContentResolver(), + REFRESH_RATE_CONFIG_CUSTOM, userId); + if (TextUtils.isEmpty(settings)) { + return; + } + final String[] configs = settings.split(";"); + for (String config : configs) { + final String[] splited = config.split(","); + if (splited.length != 2) { + continue; + } + int refreshRate = -1; + try { + refreshRate = Integer.parseInt(splited[1]); + } catch (NumberFormatException e) {} + if (mHelper.isRefreshRateValid(refreshRate)) { + mAppRefreshRateConfigMap.put(splited[0], refreshRate); + logD("parseSettingsIntoMapLocked, added packageName: " + + splited[0] + ", refreshRate: " + refreshRate); + } + } + } + + private void saveConfigIntoSettingsLocked() { + StringBuilder sb = new StringBuilder(); + for (String app : mAppRefreshRateConfigMap.keySet()) { + sb.append(app).append(","); + sb.append(String.valueOf(mAppRefreshRateConfigMap.get(app))).append(";"); + } + Settings.System.putStringForUser(mSystemExService.getContentResolver(), + REFRESH_RATE_CONFIG_CUSTOM, sb.toString(), UserHandle.USER_CURRENT); + logD("saveConfigIntoSettingsLocked, config: " + sb.toString()); + } + + private static void logD(String msg) { + Slog.d(TAG, msg); + } + + private static void logE(String msg) { + Slog.e(TAG, msg); + } +} diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java index 734de2b4b034..7483fd9bf0fb 100644 --- a/services/core/java/com/android/server/SystemService.java +++ b/services/core/java/com/android/server/SystemService.java @@ -575,7 +575,7 @@ public void onUserCompletedEvent(@NonNull TargetUser user, UserCompletedEventTyp * @param name the name of the new service * @param service the service object */ - protected final void publishBinderService(@NonNull String name, @NonNull IBinder service) { + public final void publishBinderService(@NonNull String name, @NonNull IBinder service) { publishBinderService(name, service, false); } diff --git a/services/core/java/com/android/server/wm/TopActivityRecorder.java b/services/core/java/com/android/server/wm/TopActivityRecorder.java new file mode 100644 index 000000000000..155812e2c9b9 --- /dev/null +++ b/services/core/java/com/android/server/wm/TopActivityRecorder.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023-2024 The Nameless-AOSP Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.server.wm; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.os.Process.THREAD_PRIORITY_DEFAULT; + +import android.app.WindowConfiguration; +import android.content.ComponentName; +import android.os.Handler; +import android.util.Slog; + +import com.android.server.ServiceThread; + +import java.util.ArrayList; + +public class TopActivityRecorder { + + private static final String TAG = "TopActivityRecorder"; + + private static class InstanceHolder { + private static TopActivityRecorder INSTANCE = new TopActivityRecorder(); + } + + public static TopActivityRecorder getInstance() { + return InstanceHolder.INSTANCE; + } + + private final Object mFocusLock = new Object(); + + private final Handler mHandler; + private final ServiceThread mServiceThread; + + private ActivityInfo mTopFullscreenActivity = null; + + private WindowManagerService mWms; + + private TopActivityRecorder() { + mServiceThread = new ServiceThread(TAG, THREAD_PRIORITY_DEFAULT, false); + mServiceThread.start(); + mHandler = new Handler(mServiceThread.getLooper()); + } + + void initWms(WindowManagerService wms) { + mWms = wms; + } + + void onAppFocusChanged(ActivityRecord focus, Task task) { + synchronized (mFocusLock) { + final DisplayContent dc = mWms.getDefaultDisplayContentLocked(); + final ActivityRecord newFocus = focus != null ? focus : dc.topRunningActivity(); + if (newFocus == null) { + return; + } + final Task newTask = task != null ? task : newFocus.getTask(); + if (newTask == null) { + return; + } + final int windowingMode = newTask.getWindowConfiguration().getWindowingMode(); + if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED + || windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + final ComponentName oldComponent = getTopFullscreenComponentLocked(); + final ComponentName newComponent = newFocus.mActivityComponent; + if (!newComponent.equals(oldComponent)) { + if (mTopFullscreenActivity != null && + mTopFullscreenActivity.task == newTask) { + mTopFullscreenActivity.componentName = newFocus.mActivityComponent; + mTopFullscreenActivity.packageName = newFocus.packageName; + } else { + mTopFullscreenActivity = new ActivityInfo(newFocus, newTask); + } + logD("Top fullscreen window activity changed to " + newFocus); + } + } + } + } + + private ComponentName getTopFullscreenComponentLocked() { + if (mTopFullscreenActivity == null) { + return null; + } + return mTopFullscreenActivity.componentName; + } + + int getTopFullscreenTaskId() { + synchronized (mFocusLock) { + return getTopFullscreenTaskIdLocked(); + } + } + + int getTopFullscreenTaskIdLocked() { + if (mTopFullscreenActivity == null) { + return INVALID_TASK_ID; + } + if (mTopFullscreenActivity.task == null) { + return INVALID_TASK_ID; + } + return mTopFullscreenActivity.task.mTaskId; + } + + private static final class ActivityInfo { + ComponentName componentName; + String packageName; + Task task; + boolean isHome; + + ActivityInfo(ActivityRecord r, Task task) { + this.componentName = r.mActivityComponent; + this.packageName = r.packageName; + this.task = task; + this.isHome = r.isActivityTypeHome(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("componentName=").append(componentName); + sb.append(", task=").append(task); + sb.append(", isHome=").append(isHome); + return sb.toString(); + } + } + + private static void logD(String msg) { + Slog.d(TAG, msg); + } + + private static void logE(String msg) { + Slog.e(TAG, msg); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5a8d2df980f1..24022ea70495 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -350,6 +350,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; +import com.android.server.DerpFestSystemExService; + /** * Entry point to {@code system_server}. */ @@ -1767,6 +1769,10 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { wm.onInitReady(); t.traceEnd(); + t.traceBegin("StartDerpFestSystemExService"); + mSystemServiceManager.startService(DerpFestSystemExService.class); + t.traceEnd(); + // Start receiving calls from SensorManager services. Start in a separate thread // because it need to connect to SensorManager. This has to start // after PHASE_WAIT_FOR_SENSOR_SERVICE is done. diff --git a/services/proguard.flags b/services/proguard.flags index b10aa7ef79f1..accfe5f72918 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -138,3 +138,6 @@ -keep,allowoptimization,allowaccessmodification class lineageos.** { *; } -keep,allowoptimization,allowaccessmodification class org.lineageos.** { *; } -keep,allowoptimization,allowaccessmodification class vendor.lineage.** { *; } + +# DerpFest +-keep,allowoptimization,allowaccessmodification class com.android.server.DerpFestSystemExService { *; } From 3290e9173d4c32257dd4bd80ce532bde2a552424 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Sat, 9 May 2026 21:08:14 +0530 Subject: [PATCH 1146/1315] Revert "base: Allow disabling refresh rate lowering in battery saver" This reverts commit 0c47fb9979f46ad022c899bd69b131241a1dec99. Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ------ .../server/display/mode/DisplayModeDirector.java | 11 ++--------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b988d116342e..7ed0e9b5019d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5392,12 +5392,6 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String FOLD_LOCK_BEHAVIOR = "fold_lock_behavior_setting"; - /** - * Whether refresh rate should be switched to 60Hz on power save mode. - * @hide - */ - public static final String LOW_POWER_REFRESH_RATE = "low_power_rr_switch"; - /** * The amount of time in milliseconds before the device goes to sleep or begins * to dream after a period of inactivity. This value is also known as the diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index be92d964d6b0..a89555f467b8 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -960,8 +960,6 @@ final class SettingsObserver extends ContentObserver { Settings.Global.getUriFor(Settings.Global.LOW_POWER_MODE); private final Uri mMatchContentFrameRateSetting = Settings.Secure.getUriFor(Settings.Secure.MATCH_CONTENT_FRAME_RATE); - private final Uri mLowPowerRefreshRateSetting = - Settings.System.getUriFor(Settings.System.LOW_POWER_REFRESH_RATE); private final boolean mPeakRefreshRatePhysicalLimitEnabled; @@ -1031,8 +1029,6 @@ public void observe() { UserHandle.USER_ALL); cr.registerContentObserver(mMatchContentFrameRateSetting, /* notifyDescendants= */ false, this, UserHandle.USER_ALL); - cr.registerContentObserver(mLowPowerRefreshRateSetting, - /* notifyDescendants= */ false, this, UserHandle.USER_SYSTEM); mInjector.registerDisplayListener(mDisplayListener, mHandler); float deviceConfigDefaultPeakRefresh = @@ -1073,8 +1069,7 @@ public void onChange(boolean selfChange, Uri uri, int userId) { synchronized (mLock) { if (mPeakRefreshRateSetting.equals(uri) || mMinRefreshRateSetting.equals(uri)) { updateRefreshRateSettingLocked(); - } else if (mLowPowerModeSetting.equals(uri) - || mLowPowerRefreshRateSetting.equals(uri)) { + } else if (mLowPowerModeSetting.equals(uri)) { updateLowPowerModeSettingLocked(); } else if (mMatchContentFrameRateSetting.equals(uri)) { updateModeSwitchingTypeSettingLocked(); @@ -1117,10 +1112,8 @@ private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig, private void updateLowPowerModeSettingLocked() { mIsLowPower = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0; - boolean shouldSwitchRefreshRate = Settings.System.getInt(mContext.getContentResolver(), - Settings.System.LOW_POWER_REFRESH_RATE, 1 /*default*/) != 0; final Vote vote; - if (mIsLowPower && shouldSwitchRefreshRate) { + if (mIsLowPower) { vote = Vote.forRenderFrameRates(0f, 60f); } else { vote = null; From 034d6cb223c216a0b2f9abcc0788e0ab5b8acc6d Mon Sep 17 00:00:00 2001 From: cjh1249131356 Date: Sun, 8 Sep 2024 15:34:30 +0800 Subject: [PATCH 1147/1315] base: Add custom refresh rate controller support Co-authored-by: Adithya R Signed-off-by: cjh1249131356 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++ .../display/mode/DisplayModeDirector.java | 73 ++++++++++++++++++- .../com/android/server/display/mode/Vote.java | 14 +++- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7ed0e9b5019d..0b5affb833e0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -20601,6 +20601,12 @@ public static final class Global extends NameValueTable { */ public static final String ONE_HANDED_KEYGUARD_SIDE = "one_handed_keyguard_side"; + /** + * Whether refresh rate should be switched to 60Hz on power save mode. + * @hide + */ + public static final String LOW_POWER_REFRESH_RATE = "low_power_rr_switch"; + /** * A list of uids that are allowed to use restricted networks. * diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index a89555f467b8..30af45150eaa 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -103,6 +103,9 @@ import java.util.function.Function; import java.util.function.IntSupplier; +import org.derpfest.display.IRefreshRateListener; +import org.derpfest.display.RefreshRateManager; + /** * The DisplayModeDirector is responsible for determining what modes are allowed to be automatically * picked by the system based on system-wide and display-specific configuration. @@ -140,6 +143,7 @@ public class DisplayModeDirector { private final ModeChangeObserver mModeChangeObserver; private final SystemRequestObserver mSystemRequestObserver; + private final RefreshRateObserver mRefreshRateObserver; private final DeviceConfigParameterProvider mConfigParameterProvider; private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings; @@ -226,6 +230,7 @@ public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler, mSensorObserver = new ProximitySensorObserver(mVotesStorage, injector); mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, mVotesStorage); mModeChangeObserver = mInjector.getModeChangeObserver(mVotesStorage, handler.getLooper()); + mRefreshRateObserver = new RefreshRateObserver(injector, mVotesStorage); mHbmObserver = new HbmObserver(injector, mVotesStorage, BackgroundThread.getHandler(), mDeviceConfigDisplaySettings); mSystemRequestObserver = mInjector.getSystemRequestObserver(mVotesStorage); @@ -268,6 +273,7 @@ public void onBootCompleted() { // UDFPS observer registers a listener with SystemUI which might not be ready until the // system is fully booted. mUdfpsObserver.observe(); + mRefreshRateObserver.observe(); } /** @@ -960,6 +966,8 @@ final class SettingsObserver extends ContentObserver { Settings.Global.getUriFor(Settings.Global.LOW_POWER_MODE); private final Uri mMatchContentFrameRateSetting = Settings.Secure.getUriFor(Settings.Secure.MATCH_CONTENT_FRAME_RATE); + private final Uri mLowPowerRefreshRateSetting = + Settings.Global.getUriFor(Settings.Global.LOW_POWER_REFRESH_RATE); private final boolean mPeakRefreshRatePhysicalLimitEnabled; @@ -1029,6 +1037,8 @@ public void observe() { UserHandle.USER_ALL); cr.registerContentObserver(mMatchContentFrameRateSetting, /* notifyDescendants= */ false, this, UserHandle.USER_ALL); + cr.registerContentObserver(mLowPowerRefreshRateSetting, + /* notifyDescendants= */ false, this, UserHandle.USER_SYSTEM); mInjector.registerDisplayListener(mDisplayListener, mHandler); float deviceConfigDefaultPeakRefresh = @@ -1069,7 +1079,8 @@ public void onChange(boolean selfChange, Uri uri, int userId) { synchronized (mLock) { if (mPeakRefreshRateSetting.equals(uri) || mMinRefreshRateSetting.equals(uri)) { updateRefreshRateSettingLocked(); - } else if (mLowPowerModeSetting.equals(uri)) { + } else if (mLowPowerModeSetting.equals(uri) + || mLowPowerRefreshRateSetting.equals(uri)) { updateLowPowerModeSettingLocked(); } else if (mMatchContentFrameRateSetting.equals(uri)) { updateModeSwitchingTypeSettingLocked(); @@ -1112,8 +1123,10 @@ private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig, private void updateLowPowerModeSettingLocked() { mIsLowPower = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0; + final boolean shouldSwitchRefreshRate = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.LOW_POWER_REFRESH_RATE, 1 /*default*/) != 0; final Vote vote; - if (mIsLowPower) { + if (mIsLowPower && shouldSwitchRefreshRate) { vote = Vote.forRenderFrameRates(0f, 60f); } else { vote = null; @@ -3009,6 +3022,42 @@ void dumpLocked(PrintWriter pw) { } } + private final class RefreshRateObserver extends IRefreshRateListener.Stub { + private final Injector mInjector; + private final VotesStorage mVotesStorage; + + RefreshRateObserver(Injector injector, VotesStorage votesStorage) { + mInjector = injector; + mVotesStorage = votesStorage; + } + + @Override + public void onRequestedRefreshRate(int refreshRate) { + final Vote vote; + if (refreshRate > 0) { + vote = Vote.forRenderFrameRates((float) refreshRate, (float) refreshRate); + } else { + vote = null; + } + mVotesStorage.updateGlobalVote(Vote.PRIORITY_USER_PREFERRED, vote); + } + + @Override + public void onRequestedMemcRefreshRate(int refreshRate) { + final Vote vote; + if (refreshRate > 0) { + vote = Vote.forRenderFrameRates((float) refreshRate, (float) refreshRate); + } else { + vote = null; + } + mVotesStorage.updateGlobalVote(Vote.PRIORITY_MEMC, vote); + } + + public void observe() { + mInjector.registerRefreshRateListener(this); + } + } + private class DeviceConfigDisplaySettings implements DeviceConfig.OnPropertiesChangedListener { public void startListening() { mConfigParameterProvider.addOnPropertiesChangedListener( @@ -3149,12 +3198,15 @@ void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener, SystemRequestObserver getSystemRequestObserver(VotesStorage votesStorage); ModeChangeObserver getModeChangeObserver(VotesStorage votesStorage, Looper looper); + + void registerRefreshRateListener(IRefreshRateListener.Stub listener); } @VisibleForTesting static class RealInjector implements Injector { private final Context mContext; private DisplayManager mDisplayManager; + private RefreshRateManager mRefreshRateManager; RealInjector(Context context) { mContext = context; @@ -3313,6 +3365,16 @@ public ModeChangeObserver getModeChangeObserver(VotesStorage votesStorage, Loope return new ModeChangeObserver(votesStorage, this, looper); } + @Override + public void registerRefreshRateListener(IRefreshRateListener.Stub listener) { + final RefreshRateManager manager = getRefreshRateManager(); + if (manager == null) { + Slog.e(TAG, "Could not register refresh rate listener. RefreshRateManager is not available"); + return; + } + manager.registerRefreshRateListener(listener); + } + private DisplayManager getDisplayManager() { if (mDisplayManager == null) { mDisplayManager = mContext.getSystemService(DisplayManager.class); @@ -3320,6 +3382,13 @@ private DisplayManager getDisplayManager() { return mDisplayManager; } + private RefreshRateManager getRefreshRateManager() { + if (mRefreshRateManager == null) { + mRefreshRateManager = mContext.getSystemService(RefreshRateManager.class); + } + return mRefreshRateManager; + } + private IThermalService getThermalService() { return IThermalService.Stub.asInterface( ServiceManager.getService(Context.THERMAL_SERVICE)); diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java index 428ccedf8760..9785978aa04e 100644 --- a/services/core/java/com/android/server/display/mode/Vote.java +++ b/services/core/java/com/android/server/display/mode/Vote.java @@ -147,9 +147,15 @@ interface Vote { // set to a high priority. int PRIORITY_PROXIMITY = 22; + // User preferred refresh rate for specific apps + int PRIORITY_USER_PREFERRED = 23; + + // Force display to requested refresh rate in MEMC mode + int PRIORITY_MEMC = 24; + // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order // to function, so this needs to be the highest priority of all votes. - int PRIORITY_UDFPS = 23; + int PRIORITY_UDFPS = 25; @IntDef(prefix = { "PRIORITY_" }, value = { PRIORITY_DEFAULT_RENDER_FRAME_RATE, @@ -175,6 +181,8 @@ interface Vote { PRIORITY_FLICKER_REFRESH_RATE_SWITCH, PRIORITY_SKIN_TEMPERATURE, PRIORITY_PROXIMITY, + PRIORITY_USER_PREFERRED, + PRIORITY_MEMC, PRIORITY_UDFPS }) @Retention(RetentionPolicy.SOURCE) @@ -282,6 +290,10 @@ static String priorityToString(int priority) { return "PRIORITY_LOW_POWER_MODE_RENDER_RATE"; case PRIORITY_SKIN_TEMPERATURE: return "PRIORITY_SKIN_TEMPERATURE"; + case PRIORITY_USER_PREFERRED: + return "PRIORITY_USER_PREFERRED"; + case PRIORITY_MEMC: + return "PRIORITY_MEMC"; case PRIORITY_UDFPS: return "PRIORITY_UDFPS"; case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE: From 68d18c803953c5bc4bc5a18b04f118b52dafc4c1 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Sun, 22 Jun 2025 07:40:06 +0000 Subject: [PATCH 1148/1315] SystemUI: Adapt refresh rate tile for new A16 QS Signed-off-by: Dmitrii Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/lineage/LineageModule.kt | 1 + .../systemui/qs/tiles/RefreshRateTile.java | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 90df5105da03..2e7abedc5b1d 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -229,6 +229,7 @@ interface LineageModule { const val POWERSHARE_TILE_SPEC = "powershare" const val PROFILES_TILE_SPEC = "profiles" const val READING_MODE_TILE_SPEC = "reading_mode" + const val REFRESH_RATE_TILE_SPEC = "refresh_rate" const val SYNC_TILE_SPEC = "sync" const val USB_TETHER_TILE_SPEC = "usb_tether" const val VOLUME_TILE_SPEC = "volume" diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java index e54e92a4d4ff..1201e17abe7d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RefreshRateTile.java @@ -16,6 +16,7 @@ import android.os.RemoteException; import android.provider.Settings; import android.service.quicksettings.Tile; +import android.widget.Button; import androidx.annotation.Nullable; @@ -28,7 +29,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; @@ -43,7 +44,7 @@ import org.derpfest.display.IRefreshRateListener; import org.derpfest.display.RefreshRateManager; -public class RefreshRateTile extends QSTileImpl { +public class RefreshRateTile extends QSTileImpl { public static final String TILE_SPEC = "refresh_rate"; @@ -57,7 +58,8 @@ public class RefreshRateTile extends QSTileImpl { private final RefreshRateManager mManager; private final SettingsObserver mObserver; - private final Icon mIcon = ResourceIcon.get(R.drawable.ic_refresh_rate); + @Nullable + private Icon mIcon = null; private int mMinRefreshRate; private int mPeakRefreshRate; @@ -115,8 +117,10 @@ public boolean isAvailable() { } @Override - public State newTileState() { - return new State(); + public QSTile.BooleanState newTileState() { + QSTile.BooleanState state = new QSTile.BooleanState(); + state.forceExpandIcon = true; + return state; } @Override @@ -164,17 +168,20 @@ public CharSequence getTileLabel() { } @Override - protected void handleUpdateState(State state, Object arg) { + protected void handleUpdateState(QSTile.BooleanState state, Object arg) { if (!isAvailable()) { return; } - + if (mIcon == null) { + mIcon = maybeLoadResourceIcon(R.drawable.ic_refresh_rate); + } state.state = mRequestedRefreshRate > 0 || mRequestedMemcRefreshRate > 0 ? Tile.STATE_UNAVAILABLE : Tile.STATE_ACTIVE; state.icon = mIcon; state.label = mContext.getString(R.string.refresh_rate_tile_label); state.contentDescription = mContext.getString(R.string.refresh_rate_tile_label); state.secondaryLabel = getRefreshRateLabel(); + state.expandedAccessibilityClassName = Button.class.getName(); } @Override From 509c49b7a8d022f877a17d1ff5611e9d014f98ed Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sun, 22 Mar 2026 07:11:54 +0800 Subject: [PATCH 1149/1315] core: Adding sandbox support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit credit HMA OSS | Furkan Karcıoğlu Co-authored-by: Furkan Karcıoğlu <45714956+frknkrc44@users.noreply.github.com> Change-Id: I4531975e10894b46807db7c3fdebf0203ffbca77 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Mrick343 --- core/java/android/app/AxSandboxManager.java | 462 +++++++ .../android/app/SystemServiceRegistry.java | 13 + core/java/android/app/TaskInfo.java | 8 + core/java/android/content/Context.java | 5 + .../internal/app/HiddenNotificationInfo.aidl | 18 + .../internal/app/HiddenNotificationInfo.java | 94 ++ .../internal/app/IAppLockStateListener.aidl | 8 + .../internal/app/IAppSessionListener.aidl | 21 + .../internal/app/IAxSandboxManager.aidl | 55 + .../app/IHiddenNotificationListener.aidl | 26 + .../providers/settings/SettingsProvider.java | 41 + .../systemui/applocker/AxAppLockerHelper.kt | 205 +++ .../dagger/FrameworkServicesModule.java | 8 + .../dagger/SystemUICoreStartableModule.kt | 6 + .../systemui/qs/external/CustomTile.java | 12 +- .../shade/QuickSettingsControllerImpl.java | 16 + .../NotificationLockscreenUserManager.java | 2 + ...NotificationLockscreenUserManagerImpl.java | 20 +- .../SensitiveContentCoordinator.kt | 16 +- .../row/ExpandableNotificationRow.java | 53 +- .../ExpandableNotificationRowController.java | 9 +- .../stack/NotificationStackScrollLayout.java | 22 + .../StatusBarNotificationActivityStarter.java | 13 +- .../systemui/util/TaskWorkerManager.kt | 51 + .../AccessibilityManagerService.java | 12 + .../android/server/AxExtServiceFactory.java | 2 + .../com/android/server/am/ProcessList.java | 44 +- .../android/server/compat/PlatformCompat.java | 7 + .../InputMethodManagerService.java | 45 +- .../NotificationManagerService.java | 42 + .../com/android/server/pm/AppsFilterBase.java | 3 + .../com/android/server/pm/ComputerEngine.java | 225 +++- .../server/pm/CrossProfileDomainInfo.java | 8 + .../server/pm/LauncherAppsService.java | 11 + .../com/android/server/wm/ActivityRecord.java | 21 +- .../android/server/wm/ActivityStarter.java | 15 + .../server/wm/ActivityTaskSupervisor.java | 4 + .../android/server/wm/AxSandboxService.java | 1148 +++++++++++++++++ .../com/android/server/wm/DisplayContent.java | 4 + .../android/server/wm/IAxSandboxService.java | 82 ++ .../android/server/wm/KeyguardController.java | 4 + .../com/android/server/wm/RecentTasks.java | 1 + .../core/java/com/android/server/wm/Task.java | 1 + .../com/android/server/wm/TaskFragment.java | 8 + .../com/android/server/wm/Transition.java | 3 + .../wm/sandbox/AppControlController.java | 557 ++++++++ .../sandbox/HiddenNotificationController.java | 121 ++ .../wm/sandbox/SettingsSpoofController.java | 40 + 48 files changed, 3575 insertions(+), 17 deletions(-) create mode 100644 core/java/android/app/AxSandboxManager.java create mode 100644 core/java/com/android/internal/app/HiddenNotificationInfo.aidl create mode 100644 core/java/com/android/internal/app/HiddenNotificationInfo.java create mode 100644 core/java/com/android/internal/app/IAppLockStateListener.aidl create mode 100644 core/java/com/android/internal/app/IAppSessionListener.aidl create mode 100644 core/java/com/android/internal/app/IAxSandboxManager.aidl create mode 100644 core/java/com/android/internal/app/IHiddenNotificationListener.aidl create mode 100644 packages/SystemUI/src/com/android/systemui/applocker/AxAppLockerHelper.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/TaskWorkerManager.kt create mode 100644 services/core/java/com/android/server/wm/AxSandboxService.java create mode 100644 services/core/java/com/android/server/wm/IAxSandboxService.java create mode 100644 services/core/java/com/android/server/wm/sandbox/AppControlController.java create mode 100644 services/core/java/com/android/server/wm/sandbox/HiddenNotificationController.java create mode 100644 services/core/java/com/android/server/wm/sandbox/SettingsSpoofController.java diff --git a/core/java/android/app/AxSandboxManager.java b/core/java/android/app/AxSandboxManager.java new file mode 100644 index 000000000000..bef8bc9d04cd --- /dev/null +++ b/core/java/android/app/AxSandboxManager.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package android.app; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; + + +import com.android.internal.app.IAppLockStateListener; +import com.android.internal.app.IAppSessionListener; +import com.android.internal.app.IAxSandboxManager; +import com.android.internal.app.IHiddenNotificationListener; +import com.android.internal.app.HiddenNotificationInfo; + +import java.util.Collections; +import java.util.List; + +/** + * @hide + */ +@SystemService(Context.AX_SANDBOX_SERVICE) +public class AxSandboxManager { + + /** @hide */ + public static final int LOCK_BEHAVIOR_ON_LEAVE = 0; + /** @hide */ + public static final int LOCK_BEHAVIOR_TIMEOUT = 1; + /** @hide */ + public static final int LOCK_BEHAVIOR_ON_SCREEN_OFF = 2; + /** @hide */ + public static final int LOCK_BEHAVIOR_ON_KILL = 3; + + /** @hide */ + public static final String SETTING_LOCK_BEHAVIOR = "sandbox_locked_app_behavior"; + /** @hide */ + public static final String SETTING_LOCK_TIMEOUT = "sandbox_locked_app_timeout"; + /** @hide */ + public static final String SETTING_SANDBOX_CONFIG = "sandbox_config"; + + /** @hide */ + public static final String EXTRA_LOCKED_PACKAGE = "LOCKED_PACKAGE"; + /** @hide */ + public static final String EXTRA_LOCKED_UID = "LOCKED_UID"; + /** @hide */ + public static final String EXTRA_LOCKED_COMPONENT = "LOCKED_COMPONENT"; + /** @hide */ + public static final String EXTRA_NOTIFICATION_APP_LOCKED = "android.app.extra.AX_APP_LOCKED"; + + /** @hide */ + public static final int DEFAULT_LOCK_TIMEOUT = 30; + + /** @hide */ + public enum AppLockState { + NONE, + UNLOCKED, + LOCKED; + + public boolean hasAppLock() { + return this != NONE; + } + + public boolean needsAuth() { + return this == LOCKED; + } + + public static AppLockState fromOrdinal(int ordinal) { + AppLockState[] values = values(); + if (ordinal < 0 || ordinal >= values.length) return NONE; + return values[ordinal]; + } + } + + private final Context mContext; + private final IAxSandboxManager mService; + + /** @hide */ + public AxSandboxManager(@NonNull Context context, @NonNull IAxSandboxManager service) { + mContext = context; + mService = service; + } + + /** + * @hide + */ + public AppLockState getAppLockState(@NonNull String packageName) { + try { + return AppLockState.fromOrdinal(mService.getAppLockState(packageName)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void addLockedApp(@NonNull String packageName) { + try { + mService.addLockedApp(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void removeLockedApp(@NonNull String packageName) { + try { + mService.removeLockedApp(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public boolean isPackageHidden(@NonNull String packageName) { + try { + return mService.isPackageHidden(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void setPackageHidden(@NonNull String packageName, boolean hidden) { + try { + mService.setPackageHidden(packageName, hidden); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + @NonNull + public List getLockedPackages() { + try { + List result = mService.getLockedPackages(); + return result != null ? result : Collections.emptyList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + @NonNull + public List getHiddenPackages() { + try { + List result = mService.getHiddenPackages(); + return result != null ? result : Collections.emptyList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + @NonNull + public List getLockablePackages() { + try { + List result = mService.getLockablePackages(); + return result != null ? result : Collections.emptyList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public boolean isPackageLockable(@NonNull String packageName) { + try { + return mService.isPackageLockable(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void unlockApp(@NonNull String packageName, int userId) { + try { + mService.unlockApp(packageName, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void promptUnlock(@NonNull String packageName, int userId) { + try { + mService.promptUnlock(packageName, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void registerAppLockStateListener(IAppLockStateListener listener) { + try { + mService.registerAppLockStateListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void unregisterAppLockStateListener(IAppLockStateListener listener) { + try { + mService.unregisterAppLockStateListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void registerAppSessionListener(IAppSessionListener listener) { + try { + mService.registerAppSessionListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void unregisterAppSessionListener(IAppSessionListener listener) { + try { + mService.unregisterAppSessionListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // ==================== Hidden Notification Listeners ==================== + + + /** + * @hide + */ + public void registerHiddenNotificationListener(IHiddenNotificationListener listener) { + try { + mService.registerHiddenNotificationListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void unregisterHiddenNotificationListener(IHiddenNotificationListener listener) { + try { + mService.unregisterHiddenNotificationListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public List getHiddenNotifications() { + try { + return mService.getHiddenNotifications(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void onHiddenNotificationPosted(HiddenNotificationInfo info) { + try { + mService.onHiddenNotificationPosted(info); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void onHiddenNotificationRemoved(String key) { + try { + mService.onHiddenNotificationRemoved(key); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public boolean isPackageSandboxed(@NonNull String packageName) { + try { + return mService.isPackageSandboxed(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void addSandboxedPackage(@NonNull String packageName) { + try { + mService.addSandboxedPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void removeSandboxedPackage(@NonNull String packageName) { + try { + mService.removeSandboxedPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + @NonNull + public List getSandboxedPackages() { + try { + List result = mService.getSandboxedPackages(); + return result != null ? result : Collections.emptyList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public static final int GID_INET = 3003; + /** @hide */ + public static final int GID_SDCARD_RW = 1015; + /** @hide */ + public static final int GID_MEDIA_RW = 1023; + /** @hide */ + public static final int GID_EXTERNAL_STORAGE = 1077; + /** @hide */ + public static final int GID_EXT_DATA_RW = 1078; + /** @hide */ + public static final int GID_EXT_OBB_RW = 1079; + /** @hide */ + public static final int GID_SHARED_USER = 9997; + /** @hide */ + public static final int GID_PACKAGE_INFO = 1032; + + /** @hide */ + public static final int[] STORAGE_GIDS = { + GID_SDCARD_RW, GID_MEDIA_RW, GID_EXTERNAL_STORAGE, + GID_EXT_DATA_RW, GID_EXT_OBB_RW + }; + + /** @hide */ + public void setRestrictedGids(@NonNull String packageName, int[] gids) { + try { + mService.setRestrictedGids(packageName, gids); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public int[] getRestrictedGids(@NonNull String packageName) { + try { + return mService.getRestrictedGids(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** @hide */ + public boolean isSandboxDataIsolationEnabled(@NonNull String packageName) { + try { + return mService.isSandboxDataIsolationEnabled(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public void setSandboxDataIsolationEnabled(@NonNull String packageName, boolean enabled) { + try { + mService.setSandboxDataIsolationEnabled(packageName, enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public boolean isSpoofSettingEnabled(@NonNull String packageName, @NonNull String settingKey) { + try { + return mService.isSpoofSettingEnabled(packageName, settingKey); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public void setSpoofSettingEnabled(@NonNull String packageName, @NonNull String settingKey, boolean enabled) { + try { + mService.setSpoofSettingEnabled(packageName, settingKey, enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + @NonNull + public List getEnabledSpoofSettings(@NonNull String packageName) { + try { + return mService.getEnabledSpoofSettings(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + @Nullable + public String getSpoofedSetting(@NonNull String callingPackage, @NonNull String settingName) { + try { + return mService.getSpoofedSetting(callingPackage, settingName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 65fdf25d6c58..1168b51af9d8 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -298,6 +298,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; +import com.android.internal.app.IAxSandboxManager; import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; import com.android.internal.appwidget.IAppWidgetService; @@ -958,6 +959,18 @@ public AppOpsManager createService(ContextImpl ctx) throws ServiceNotFoundExcept return new AppOpsManager(ctx, service); }}); + registerService(Context.AX_SANDBOX_SERVICE, AxSandboxManager.class, + new CachedServiceFetcher() { + @Override + public AxSandboxManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.AX_SANDBOX_SERVICE); + if (b == null) { + return null; + } + IAxSandboxManager service = IAxSandboxManager.Stub.asInterface(b); + return new AxSandboxManager(ctx.getOuterContext(), service); + }}); + registerService(Context.CAMERA_SERVICE, CameraManager.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 6c71a2b377a2..962977647c55 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -409,6 +409,11 @@ public class TaskInfo { */ public boolean isAppBubble; + /** + * @hide + */ + public boolean isTopAppLocked; + /** * The top activity's main window frame if it doesn't match the top activity bounds. * {@code null}, otherwise. @@ -652,6 +657,7 @@ void readTaskFromParcel(Parcel source) { appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR); topActivityMainWindowFrame = source.readTypedObject(Rect.CREATOR); isAppBubble = source.readBoolean(); + isTopAppLocked = source.readBoolean(); } /** @@ -710,6 +716,7 @@ public void writeTaskToParcel(Parcel dest, int flags) { dest.writeTypedObject(appCompatTaskInfo, flags); dest.writeTypedObject(topActivityMainWindowFrame, flags); dest.writeBoolean(isAppBubble); + dest.writeBoolean(isTopAppLocked); } @Override @@ -760,6 +767,7 @@ public String toString() { + " appCompatTaskInfo=" + appCompatTaskInfo + " topActivityMainWindowFrame=" + topActivityMainWindowFrame + " isAppBubble=" + isAppBubble + + " isTopAppLocked=" + isTopAppLocked + "}"; } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c9bf1b8935a4..33f9de093bff 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6259,6 +6259,11 @@ public final T getSystemService(@NonNull Class serviceClass) { */ public static final String APP_OPS_SERVICE = "appops"; + /** + * @hide + */ + public static final String AX_SANDBOX_SERVICE = "ax_sandbox"; + /** * Use with {@link #getSystemService(String)} to retrieve a {@link android.app.role.RoleManager} * for managing roles. diff --git a/core/java/com/android/internal/app/HiddenNotificationInfo.aidl b/core/java/com/android/internal/app/HiddenNotificationInfo.aidl new file mode 100644 index 000000000000..7965bff3eb66 --- /dev/null +++ b/core/java/com/android/internal/app/HiddenNotificationInfo.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +parcelable HiddenNotificationInfo; diff --git a/core/java/com/android/internal/app/HiddenNotificationInfo.java b/core/java/com/android/internal/app/HiddenNotificationInfo.java new file mode 100644 index 000000000000..58a5afd0153e --- /dev/null +++ b/core/java/com/android/internal/app/HiddenNotificationInfo.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +public class HiddenNotificationInfo implements Parcelable { + public final String key; + public final String packageName; + public final Icon appIcon; + public final CharSequence title; + public final CharSequence text; + public final PendingIntent contentIntent; + public final long postTime; + public final int userId; + + public HiddenNotificationInfo( + String key, + String packageName, + Icon appIcon, + CharSequence title, + CharSequence text, + PendingIntent contentIntent, + long postTime, + int userId) { + this.key = key; + this.packageName = packageName; + this.appIcon = appIcon; + this.title = title; + this.text = text; + this.contentIntent = contentIntent; + this.postTime = postTime; + this.userId = userId; + } + + protected HiddenNotificationInfo(Parcel in) { + key = in.readString(); + packageName = in.readString(); + appIcon = in.readParcelable(Icon.class.getClassLoader(), Icon.class); + title = in.readCharSequence(); + text = in.readCharSequence(); + contentIntent = in.readParcelable(PendingIntent.class.getClassLoader(), PendingIntent.class); + postTime = in.readLong(); + userId = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(packageName); + dest.writeParcelable(appIcon, flags); + dest.writeCharSequence(title); + dest.writeCharSequence(text); + dest.writeParcelable(contentIntent, flags); + dest.writeLong(postTime); + dest.writeInt(userId); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public HiddenNotificationInfo createFromParcel(Parcel in) { + return new HiddenNotificationInfo(in); + } + + @Override + public HiddenNotificationInfo[] newArray(int size) { + return new HiddenNotificationInfo[size]; + } + }; +} diff --git a/core/java/com/android/internal/app/IAppLockStateListener.aidl b/core/java/com/android/internal/app/IAppLockStateListener.aidl new file mode 100644 index 000000000000..3008571018a5 --- /dev/null +++ b/core/java/com/android/internal/app/IAppLockStateListener.aidl @@ -0,0 +1,8 @@ +package com.android.internal.app; + +/** + * @hide + */ +oneway interface IAppLockStateListener { + void onAppLockStateChanged(String packageName, boolean locked); +} diff --git a/core/java/com/android/internal/app/IAppSessionListener.aidl b/core/java/com/android/internal/app/IAppSessionListener.aidl new file mode 100644 index 000000000000..228478a8909f --- /dev/null +++ b/core/java/com/android/internal/app/IAppSessionListener.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +oneway interface IAppSessionListener { + void onAppUnlocked(String packageName, int userId); + void onAppLocked(String packageName, int userId); +} diff --git a/core/java/com/android/internal/app/IAxSandboxManager.aidl b/core/java/com/android/internal/app/IAxSandboxManager.aidl new file mode 100644 index 000000000000..6e4104e505fa --- /dev/null +++ b/core/java/com/android/internal/app/IAxSandboxManager.aidl @@ -0,0 +1,55 @@ +package com.android.internal.app; + +import com.android.internal.app.IAppLockStateListener; +import com.android.internal.app.IAppSessionListener; +import com.android.internal.app.IHiddenNotificationListener; +import com.android.internal.app.HiddenNotificationInfo; + +/** + * @hide + */ +interface IAxSandboxManager { + int getAppLockState(String packageName); + boolean isPackageHidden(String packageName); + + void addLockedApp(String packageName); + void removeLockedApp(String packageName); + void setPackageHidden(String packageName, boolean hidden); + + List getLockedPackages(); + List getHiddenPackages(); + List getLockablePackages(); + + boolean isPackageLockable(String packageName); + + void unlockApp(String packageName, int userId); + void promptUnlock(String packageName, int userId); + + void registerAppLockStateListener(IAppLockStateListener listener); + void unregisterAppLockStateListener(IAppLockStateListener listener); + void registerAppSessionListener(IAppSessionListener listener); + void unregisterAppSessionListener(IAppSessionListener listener); + void registerHiddenNotificationListener(IHiddenNotificationListener listener); + void unregisterHiddenNotificationListener(IHiddenNotificationListener listener); + + List getHiddenNotifications(); + void onHiddenNotificationPosted(in HiddenNotificationInfo info); + void onHiddenNotificationRemoved(String key); + + boolean isPackageSandboxed(String packageName); + void addSandboxedPackage(String packageName); + void removeSandboxedPackage(String packageName); + List getSandboxedPackages(); + + void setRestrictedGids(String packageName, in int[] gids); + int[] getRestrictedGids(String packageName); + + boolean isSpoofSettingEnabled(String packageName, String settingKey); + void setSpoofSettingEnabled(String packageName, String settingKey, boolean enabled); + List getEnabledSpoofSettings(String packageName); + + boolean isSandboxDataIsolationEnabled(String packageName); + void setSandboxDataIsolationEnabled(String packageName, boolean enabled); + + String getSpoofedSetting(String callingPackage, String settingName); +} diff --git a/core/java/com/android/internal/app/IHiddenNotificationListener.aidl b/core/java/com/android/internal/app/IHiddenNotificationListener.aidl new file mode 100644 index 000000000000..8eb2659bfac3 --- /dev/null +++ b/core/java/com/android/internal/app/IHiddenNotificationListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +import com.android.internal.app.HiddenNotificationInfo; + +/** + * @hide + */ +oneway interface IHiddenNotificationListener { + void onHiddenNotificationPosted(in HiddenNotificationInfo info); + void onHiddenNotificationRemoved(String key); +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 7ed6baf9e45a..1eef12d75470 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -50,6 +50,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppGlobals; +import android.app.AxSandboxManager; import android.app.backup.BackupManager; import android.app.compat.CompatChanges; import android.app.job.JobInfo; @@ -447,6 +448,13 @@ public boolean onCreate() { public Bundle call(String method, String name, Bundle args) { final @CanBeCURRENT @UserIdInt int requestingUserId = getRequestingUserId(args); final int callingDeviceId = getDeviceId(); + if (method != null && method.startsWith("GET_")) { + String spoofed = getSpoofedValue(name); + if (spoofed != null) { + return Bundle.forPair(Settings.NameValueTable.VALUE, spoofed); + } + } + switch (method) { case Settings.CALL_METHOD_GET_CONFIG -> { Setting setting = getConfigSetting(name); @@ -619,6 +627,27 @@ public Bundle call(String method, String name, Bundle args) { return null; } + private String getSpoofedValue(String name) { + String callingPackage = getCallingPackage(); + if (callingPackage == null) return null; + + if (callingPackage.startsWith("com.android.") + || callingPackage.startsWith("com.google.android.")) { + return null; + } + + String settings = null; + try { + AxSandboxManager sandboxManager = + getContext().getSystemService(AxSandboxManager.class); + if (sandboxManager != null) { + settings = sandboxManager.getSpoofedSetting(callingPackage, name); + } + } catch (Exception e) {} + + return settings; + } + @Override public String getType(Uri uri) { Arguments args = new Arguments(uri, null, null, true); @@ -644,6 +673,18 @@ public Cursor query(Uri uri, String[] projection, String where, String[] whereAr return new MatrixCursor(normalizedProjection, 0); } + if (args.name != null) { + String spoofed = getSpoofedValue(args.name); + if (spoofed != null) { + synchronized (mLock) { + SettingsState state = mSettingsRegistry.getSettingsLocked( + SettingsState.SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM, Context.DEVICE_ID_DEFAULT); + SettingsState.Setting s = state.new Setting(args.name, spoofed, true, null, null); + return packageSettingForQuery(s, normalizedProjection); + } + } + } + final int callingDeviceId = getDeviceId(); switch (args.table) { case TABLE_GLOBAL -> { diff --git a/packages/SystemUI/src/com/android/systemui/applocker/AxAppLockerHelper.kt b/packages/SystemUI/src/com/android/systemui/applocker/AxAppLockerHelper.kt new file mode 100644 index 000000000000..3e536f1e4504 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/applocker/AxAppLockerHelper.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.applocker + +import android.app.AxSandboxManager.AppLockState +import android.content.Context +import android.os.IBinder +import android.os.RemoteException +import android.os.ServiceManager +import android.os.SystemClock +import android.util.Log +import com.android.internal.app.IAppLockStateListener +import com.android.internal.app.IAppSessionListener +import com.android.internal.app.IAxSandboxManager +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.shade.QuickSettingsControllerImpl +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.policy.KeyguardStateController +import dagger.Lazy +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject + +@SysUISingleton +class AxAppLockerHelper @Inject constructor( + private val qsController: Lazy, + private val shadeController: Lazy, + private val keyguardStateController: KeyguardStateController, + @Main private val mainExecutor: Executor, +) : CoreStartable { + + companion object { + private const val TAG = "AxAppLockerHelper" + private const val PENDING_EXPIRY_MS = 15_000L + private fun sessionKey(userId: Int, pkg: String): String = "$userId:$pkg" + } + + private val serviceRef = AtomicReference() + @Volatile private var listenersRegistered = false + private val hasLockCache = ConcurrentHashMap() + private val sessionAuthCache = ConcurrentHashMap() + private val refreshListeners = CopyOnWriteArrayList() + private val notifUnlocks = + Collections.newSetFromMap(ConcurrentHashMap()) + private val pendingNotifOnly = ConcurrentHashMap() + + private val keyguardCallback = object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + if (keyguardStateController.isShowing) { + val hadEntries = notifUnlocks.isNotEmpty() || pendingNotifOnly.isNotEmpty() + notifUnlocks.clear() + pendingNotifOnly.clear() + if (hadEntries) { + mainExecutor.execute { refreshState() } + } + } + } + } + + private val lockStateListener = object : IAppLockStateListener.Stub() { + override fun onAppLockStateChanged(packageName: String, locked: Boolean) { + if (packageName.isBlank()) return + hasLockCache[packageName] = locked + if (!locked) { + val prefix = ":$packageName" + sessionAuthCache.keys.removeAll { it.endsWith(prefix) } + notifUnlocks.removeAll { it.endsWith(prefix) } + pendingNotifOnly.keys.removeAll { it.endsWith(prefix) } + } + mainExecutor.execute { + qsController.get().onAppLockerUpdated(packageName) + refreshState() + } + } + } + + private val sessionListener = object : IAppSessionListener.Stub() { + override fun onAppUnlocked(packageName: String, userId: Int) { + if (packageName.isBlank()) return + val key = sessionKey(userId, packageName) + sessionAuthCache[key] = false + val pendingTs = pendingNotifOnly.remove(key) + val now = SystemClock.elapsedRealtime() + val notifOnly = pendingTs != null && (now - pendingTs) < PENDING_EXPIRY_MS + if (notifOnly) notifUnlocks.add(key) + mainExecutor.execute { + if (notifOnly) { + shadeController.get().animateExpandShade() + } + refreshState() + } + } + override fun onAppLocked(packageName: String, userId: Int) { + if (packageName.isBlank()) return + val key = sessionKey(userId, packageName) + sessionAuthCache[key] = true + if (notifUnlocks.contains(key)) return + mainExecutor.execute { refreshState() } + } + } + + private val deathRecipient = IBinder.DeathRecipient { + serviceRef.set(null) + listenersRegistered = false + hasLockCache.clear() + sessionAuthCache.clear() + notifUnlocks.clear() + pendingNotifOnly.clear() + } + + fun addRefreshListener(l: Runnable) { refreshListeners.addIfAbsent(l) } + fun removeRefreshListener(l: Runnable) { refreshListeners.remove(l) } + private fun refreshState() { refreshListeners.forEach { it.run() } } + + private fun getService(): IAxSandboxManager? { + var manager = serviceRef.get() + if (manager == null) { + val binder = ServiceManager.getService(Context.AX_SANDBOX_SERVICE) ?: return null + try { + binder.linkToDeath(deathRecipient, 0) + manager = IAxSandboxManager.Stub.asInterface(binder) + serviceRef.set(manager) + registerListeners(manager) + } catch (e: RemoteException) { + Log.w(TAG, "Failed to link/register", e) + } + } + return manager + } + + private fun registerListeners(manager: IAxSandboxManager) { + if (listenersRegistered) return + try { + manager.registerAppLockStateListener(lockStateListener) + manager.registerAppSessionListener(sessionListener) + listenersRegistered = true + } catch (e: RemoteException) { + Log.w(TAG, "Failed to register listeners", e) + } + } + + override fun start() { + keyguardStateController.addCallback(keyguardCallback) + getService() + } + + fun getState(packageName: String): AppLockState { + if (packageName.isBlank()) return AppLockState.NONE + val manager = getService() ?: return AppLockState.NONE + return try { + AppLockState.fromOrdinal(manager.getAppLockState(packageName)) + } catch (e: RemoteException) { + Log.w(TAG, "getState RemoteException: ${e.message}") + AppLockState.NONE + } + } + + fun hasAppLock(packageName: String): Boolean { + if (packageName.isBlank()) return false + hasLockCache[packageName]?.let { return it } + val hasLock = getState(packageName).hasAppLock() + hasLockCache[packageName] = hasLock + return hasLock + } + + fun needsAuth(packageName: String, userId: Int): Boolean { + if (packageName.isBlank()) return false + if (!hasAppLock(packageName)) return false + val key = sessionKey(userId, packageName) + if (notifUnlocks.contains(key)) return false + sessionAuthCache[key]?.let { return it } + val authoritative = getState(packageName).needsAuth() + sessionAuthCache[key] = authoritative + return authoritative + } + + fun promptUnlock(packageName: String, userId: Int) { + pendingNotifOnly[sessionKey(userId, packageName)] = SystemClock.elapsedRealtime() + shadeController.get().collapseShade() + val manager = getService() ?: return + try { + manager.promptUnlock(packageName, userId) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to prompt unlock", e) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 6a421a4567ca..1b8a722db16b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -22,6 +22,7 @@ import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.AppOpsManager; +import android.app.AxSandboxManager; import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.app.INotificationManager; @@ -860,4 +861,11 @@ static AutofillManager provideAutofillManager(Context context) { static ImsManager provideImsManager(Context context) { return context.getSystemService(ImsManager.class); } + + @Provides + @Singleton + @Nullable + static AxSandboxManager provideAxSandboxManager(Context context) { + return context.getSystemService(AxSandboxManager.class); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 2661b91e39b9..054d4c6333db 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification +import com.android.systemui.applocker.AxAppLockerHelper import com.android.systemui.ax.AxPlatformServiceImpl import com.android.systemui.axdynamicbar.domain.AxDynamicBarChipsRefiner import com.android.systemui.axdynamicbar.ui.AxDynamicBarManager @@ -361,4 +362,9 @@ abstract class SystemUICoreStartableModule { @Binds @IntoSet abstract fun bindDynamicBarChipsRefiner(impl: AxDynamicBarChipsRefiner): OngoingActivityChipsRefiner + + @Binds + @IntoMap + @ClassKey(AxAppLockerHelper::class) + abstract fun bindAxAppLockerHelper(impl: AxAppLockerHelper): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 5d35a69be910..a732df7a47e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -65,6 +65,7 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.DisplayTracker; +import com.android.systemui.applocker.AxAppLockerHelper; import dagger.Lazy; import dagger.assisted.Assisted; @@ -116,6 +117,7 @@ public class CustomTile extends QSTileImpl implements TileChangeListener, private int mServiceUid = Process.INVALID_UID; private final IUriGrantsManager mIUriGrantsManager; + private final AxAppLockerHelper mAxAppLockerHelper; @AssistedInject CustomTile( @@ -133,7 +135,8 @@ public class CustomTile extends QSTileImpl implements TileChangeListener, CustomTileStatePersister customTileStatePersister, TileServices tileServices, DisplayTracker displayTracker, - IUriGrantsManager uriGrantsManager + IUriGrantsManager uriGrantsManager, + AxAppLockerHelper axAppLockerHelper ) { super(host.get(), uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -150,6 +153,7 @@ public class CustomTile extends QSTileImpl implements TileChangeListener, mCustomTileStatePersister = customTileStatePersister; mDisplayTracker = displayTracker; mIUriGrantsManager = uriGrantsManager; + mAxAppLockerHelper = axAppLockerHelper; } @Override @@ -414,6 +418,12 @@ protected void handleClick(@Nullable Expandable expandable) { if (mTile.getState() == Tile.STATE_UNAVAILABLE) { return; } + + if (mAxAppLockerHelper.getState(mComponent.getPackageName()).needsAuth()) { + mAxAppLockerHelper.promptUnlock(mComponent.getPackageName(), mUser); + return; + } + mExpandableClicked = expandable; try { if (DEBUG) Log.d(TAG, "Adding token"); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 2b985c8545f1..6c89e52736be 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -114,6 +114,12 @@ import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.utils.windowmanager.WindowManagerProvider; +import com.android.systemui.applocker.AxAppLockerHelper; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import android.service.notification.StatusBarNotification; +import android.view.View; + import lineageos.providers.LineageSettings; import dalvik.annotation.optimization.NeverCompile; @@ -2556,4 +2562,14 @@ interface FlingQsWithoutClickListener { void onFlingQsWithoutClick(ValueAnimator animator, float qsExpansionHeight, float target, float vel); } + + public final void onAppLockerUpdated(String packageName) { + NotificationStackScrollLayoutController controller = mNotificationStackScrollLayoutController; + if (controller == null || controller.getView() == null) { + return; + } + + NotificationStackScrollLayout view = controller.getView(); + view.post(() -> view.onAppLockerUpdate(packageName)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java index fdcd39d1d616..54f80e8657b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java @@ -130,6 +130,8 @@ default boolean needsSeparateWorkChallenge(int userId) { */ void removeNotificationStateChangedListener(NotificationStateChangedListener listener); + default void onAppLockRefresh() {} + /** Notified when the current user changes. */ interface UserChangedListener { default void onUserChanged(int userId) {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 34a26cbbecec..5f200644c238 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -33,6 +33,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityOptions; +import android.app.AxSandboxManager; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -100,6 +101,7 @@ import java.util.concurrent.atomic.AtomicLong; import javax.inject.Inject; +import com.android.systemui.applocker.AxAppLockerHelper; /** * Handles keeping track of the current user, profiles, and various things related to hiding @@ -317,6 +319,7 @@ public void onUserChanging(int newUser, @NonNull Context userContext) { protected ContentObserver mSettingsObserver; private final Lazy mDeviceUnlockedInteractorLazy; + private final AxAppLockerHelper mAxAppLockerHelper; @Inject public NotificationLockscreenUserManagerImpl(Context context, @@ -341,7 +344,8 @@ public NotificationLockscreenUserManagerImpl(Context context, Lazy deviceUnlockedInteractorLazy, Lazy keyguardInteractor, Lazy wifiRepository, - @Background CoroutineScope coroutineScope + @Background CoroutineScope coroutineScope, + AxAppLockerHelper axAppLockerHelper ) { mContext = context; mMainExecutor = mainExecutor; @@ -363,6 +367,8 @@ public NotificationLockscreenUserManagerImpl(Context context, mKeyguardStateController = keyguardStateController; mFeatureFlags = featureFlags; mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy; + mAxAppLockerHelper = axAppLockerHelper; + mAxAppLockerHelper.addRefreshListener(this::notifyNotificationStateChanged); mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); @@ -729,6 +735,13 @@ public boolean userAllowsNotificationsInPublic(int userHandle) { public @RedactionType int getRedactionType(NotificationEntry ent) { int userId = ent.getSbn().getUserId(); + if (ent.getSbn().getNotification().extras + .getBoolean(AxSandboxManager.EXTRA_NOTIFICATION_APP_LOCKED, false) + && mAxAppLockerHelper.needsAuth( + ent.getSbn().getPackageName(), ent.getSbn().getUserId())) { + return REDACTION_TYPE_PUBLIC; + } + boolean isCurrentUserRedactingNotifs = !userAllowsPrivateNotificationsInPublic(mCurrentUserId); boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId); @@ -959,6 +972,11 @@ public void removeNotificationStateChangedListener(NotificationStateChangedListe mNotifStateChangedListeners.remove(listener); } + @Override + public void onAppLockRefresh() { + notifyNotificationStateChanged(); + } + private void notifyNotificationStateChanged() { if (!Looper.getMainLooper().isCurrentThread()) { for (NotificationStateChangedListener listener : mNotifStateChangedListeners) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index c44ecce21808..270316042557 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator +import android.app.AxSandboxManager import android.app.Notification import android.os.UserHandle import com.android.app.tracing.coroutines.launchTraced as launch @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.applocker.AxAppLockerHelper import dagger.Binds import dagger.Module import javax.inject.Inject @@ -76,6 +78,7 @@ constructor( SensitiveNotificationProtectionController, private val deviceEntryInteractor: DeviceEntryInteractor, private val sceneInteractor: SceneInteractor, + private val axAppLockerHelper: AxAppLockerHelper, @Application private val scope: CoroutineScope, ) : Invalidator("SensitiveContentInvalidator"), @@ -211,10 +214,19 @@ constructor( screenshareNotificationHiding() && sensitiveNotificationProtectionController.shouldProtectNotification(entry) + val isAppLocked = + entry.sbn.notification.extras.getBoolean( + AxSandboxManager.EXTRA_NOTIFICATION_APP_LOCKED, + false, + ) && axAppLockerHelper.needsAuth(entry.sbn.packageName, entry.sbn.user.identifier) val needsRedaction = - lockscreenUserManager.getRedactionType(entry) != REDACTION_TYPE_NONE + isAppLocked || + lockscreenUserManager.getRedactionType(entry) != REDACTION_TYPE_NONE val isSensitive = userPublic && needsRedaction - entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive) + entry.setSensitive( + isSensitive || shouldProtectNotification, + isAppLocked || deviceSensitive, + ) if (screenshareNotificationHiding()) { entry.row?.setPublicExpanderVisible(!shouldProtectNotification) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 4d4c744962ee..c0b9fd94b92d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -34,6 +34,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.AxSandboxManager; import android.app.Notification; import android.content.Context; import android.content.res.Configuration; @@ -49,6 +50,7 @@ import android.os.Build; import android.os.Bundle; import android.os.SystemClock; +import android.service.notification.StatusBarNotification; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -155,6 +157,8 @@ import com.android.systemui.util.ListenerSet; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.systemui.applocker.AxAppLockerHelper; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -214,6 +218,7 @@ public interface OnExpansionChangedListener { private RowContentBindStage mRowContentBindStage; private PeopleNotificationIdentifier mPeopleNotificationIdentifier; private NotificationActivityStarter mNotificationActivityStarter; + private AxAppLockerHelper mAxAppLockerHelper; private MetricsLogger mMetricsLogger; private NotificationChildrenContainerLogger mChildrenContainerLogger; private ColorUpdateLogger mColorUpdateLogger; @@ -410,6 +415,10 @@ public void toggleExpansionState() { } private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { + if (doesNotificationNeedAuth()) { + promptAppUnlock(); + return; + } if (isBundle() || (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot() && !NTForbiddenSwipeDownQSController.get(mContext).getForbiddenSwipeDownQS())) { mGroupExpansionChanging = true; @@ -469,6 +478,35 @@ private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { } } + private StatusBarNotification getAppLockSbn() { + if (NotificationBundleUi.isEnabled()) { + return mEntryAdapter != null ? mEntryAdapter.getSbn() : null; + } + return mEntry != null ? mEntry.getSbn() : null; + } + + private boolean isNotificationAppLocked() { + StatusBarNotification sbn = getAppLockSbn(); + if (sbn == null) return false; + if (!sbn.getNotification().extras + .getBoolean(AxSandboxManager.EXTRA_NOTIFICATION_APP_LOCKED, false)) { + return false; + } + return mAxAppLockerHelper.needsAuth(sbn.getPackageName(), sbn.getUserId()); + } + + private boolean doesNotificationNeedAuth() { + StatusBarNotification sbn = getAppLockSbn(); + if (sbn == null) return false; + return mAxAppLockerHelper.getState(sbn.getPackageName()).needsAuth(); + } + + private void promptAppUnlock() { + StatusBarNotification sbn = getAppLockSbn(); + if (sbn == null) return; + mAxAppLockerHelper.promptUnlock(sbn.getPackageName(), sbn.getUserId()); + } + private boolean mKeepInParentForDismissAnimation; private boolean mRemoved; public static final FloatProperty TRANSLATE_CONTENT = @@ -2275,7 +2313,8 @@ public void initialize( UiEventLogger uiEventLogger, NotificationRebindingTracker notificationRebindingTracker, BundleInteractionLogger bundleInteractionLogger, - NotificationActivityStarter notificationActivityStarter) { + NotificationActivityStarter notificationActivityStarter, + AxAppLockerHelper axAppLockerHelper) { if (NotificationBundleUi.isEnabled()) { mEntryAdapter = entryAdapter; @@ -2291,6 +2330,7 @@ public void initialize( mAppName = appName; mRebindingTracker = notificationRebindingTracker; mNotificationActivityStarter = notificationActivityStarter; + mAxAppLockerHelper = axAppLockerHelper; if (mMenuRow == null) { mMenuRow = new NotificationMenuRow( mContext, peopleNotificationIdentifier, mNotificationActivityStarter); @@ -3311,7 +3351,7 @@ public int getIntrinsicHeight() { return mGuts.getIntrinsicHeight(); } if (NotificationBundleUi.isEnabled()) { - if (mSensitive && mHideSensitiveForIntrinsicHeight) { + if ((mSensitive && mHideSensitiveForIntrinsicHeight) || isNotificationAppLocked()) { return getMinHeight(); } if (mIsSummaryWithChildren) { @@ -3323,6 +3363,10 @@ public int getIntrinsicHeight() { } else { if (isChildInGroup() && !isGroupExpanded()) { return mPrivateLayout.getMinHeight(); + } else if ((isChildInGroup() && !isGroupExpanded())) { + return mPrivateLayout.getMinHeight(); + } else if (isNotificationAppLocked()) { + return getMinHeight(); } if (mSensitive && mHideSensitiveForIntrinsicHeight) { return getMinHeight(); @@ -3638,7 +3682,7 @@ public void setHideSensitive(boolean hideSensitive, boolean animated, long delay return; } boolean oldShowingPublic = mShowingPublic; - mShowingPublic = mSensitive && hideSensitive; + mShowingPublic = (mSensitive && hideSensitive) || isNotificationAppLocked(); boolean isShowingLayoutNotChanged = mShowingPublic == oldShowingPublic; if (mShowingPublicInitialized && isShowingLayoutNotChanged) { return; @@ -3760,6 +3804,9 @@ public boolean canViewBeCleared() { } private boolean shouldShowPublic() { + if (isNotificationAppLocked()) { + return true; + } return mSensitive && mHideSensitiveForIntrinsicHeight; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 3d49de75ea60..b8c83d5bf454 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -41,6 +41,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.Flags; +import com.android.systemui.applocker.AxAppLockerHelper; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginManager; @@ -131,6 +132,7 @@ public class ExpandableNotificationRowController implements NotifViewController private final EntryAdapterFactory mEntryAdapterFactory; private final WindowRootViewBlurInteractor mWindowRootViewBlurInteractor; private final NotificationActivityStarter mNotificationActivityStarter; + private final AxAppLockerHelper mAxAppLockerHelper; private final Context mContext; @VisibleForTesting @@ -299,7 +301,8 @@ public ExpandableNotificationRowController( EntryAdapterFactory entryAdapterFactory, WindowRootViewBlurInteractor windowRootViewBlurInteractor, BundleInteractionLogger bundleInteractionLogger, - NotificationActivityStarter notificationActivityStarter) { + NotificationActivityStarter notificationActivityStarter, + AxAppLockerHelper axAppLockerHelper) { mView = view; mContext = context; mListContainer = listContainer; @@ -339,6 +342,7 @@ public ExpandableNotificationRowController( mWindowRootViewBlurInteractor = windowRootViewBlurInteractor; mBundleInteractionLogger = bundleInteractionLogger; mNotificationActivityStarter = notificationActivityStarter; + mAxAppLockerHelper = axAppLockerHelper; } String loadsGutsAppName(Context context, PipelineEntry pipelineEntry) { @@ -405,7 +409,8 @@ public void init(PipelineEntry entry) { mUiEventLogger, mNotificationRebindingTracker, mBundleInteractionLogger, - mNotificationActivityStarter + mNotificationActivityStarter, + mAxAppLockerHelper ); mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); if (mAllowLongPress) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 9c5a6352adf7..17c555a8ff3d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -147,6 +147,9 @@ import com.android.systemui.util.state.DownstreamObservableState; import com.android.systemui.util.state.ObservableState; +import com.android.systemui.applocker.AxAppLockerHelper; +import android.service.notification.StatusBarNotification; + import com.google.errorprone.annotations.CompileTimeConstant; import kotlin.Unit; @@ -7478,4 +7481,23 @@ private void spewLog(String logMsg) { Log.v(TAG, logMsg); } } + + public void onAppLockerUpdate(String packageName) { + boolean hideSensitive = mAmbientState.isHideSensitive(); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + ExpandableView child = getChildAtIndex(i); + if (child instanceof ExpandableNotificationRow row) { + NotificationEntry entry = row.getEntry(); + if (entry == null) continue; + StatusBarNotification sbn = entry.getSbn(); + if (packageName != null && !packageName.equals(sbn.getPackageName())) { + continue; + } + row.setHideSensitive(hideSensitive, false, 0, 0); + onChildHeightChanged(child, true); + } + } + updateContentHeight(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 665681152cbd..8d4ce26469d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -86,6 +86,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wmshell.BubblesManager; +import com.android.systemui.applocker.AxAppLockerHelper; + import dagger.Lazy; import kotlinx.coroutines.CoroutineScope; @@ -155,6 +157,7 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat private final PowerInteractor mPowerInteractor; private final UserTracker mUserTracker; private final OnUserInteractionCallback mOnUserInteractionCallback; + private final AxAppLockerHelper mAxAppLockerHelper; private boolean mIsCollapsingToShowActivityOverLockscreen; @@ -193,7 +196,8 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat NotificationLaunchAnimatorControllerProvider notificationAnimationProvider, LaunchFullScreenIntentProvider launchFullScreenIntentProvider, PowerInteractor powerInteractor, - UserTracker userTracker) { + UserTracker userTracker, + AxAppLockerHelper axAppLockerHelper) { mContext = context; mContextInteractor = contextInteractor; mMainThreadHandler = mainThreadHandler; @@ -227,6 +231,7 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat mNotificationAnimationProvider = notificationAnimationProvider; mPowerInteractor = powerInteractor; mUserTracker = userTracker; + mAxAppLockerHelper = axAppLockerHelper; launchFullScreenIntentProvider.registerListener(entry -> launchFullScreenIntent(entry)); } @@ -265,6 +270,12 @@ public void onNotificationBubbleIconClicked(@NonNull NotificationEntry entry) { @Override public void onNotificationClicked(@NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row) { + String packageName = entry.getSbn().getPackageName(); + if (mAxAppLockerHelper.getState(packageName).needsAuth()) { + int userId = entry.getSbn().getUserId(); + mAxAppLockerHelper.promptUnlock(packageName, userId); + return; + } mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(), mKeyguardStateController.isVisible(), mNotificationShadeWindowController.getPanelExpanded()); diff --git a/packages/SystemUI/src/com/android/systemui/util/TaskWorkerManager.kt b/packages/SystemUI/src/com/android/systemui/util/TaskWorkerManager.kt new file mode 100644 index 000000000000..d1b3bd6dd81d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/TaskWorkerManager.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util + +import android.os.Handler +import android.os.HandlerThread + +class TaskWorkerManager private constructor() { + + companion object { + val instance: TaskWorkerManager by lazy { TaskWorkerManager() } + } + + val taskWorker: TaskWorker = TaskWorker("task_worker_default") + + class TaskWorker(taskWorkerTag: String) { + + private val handler: Handler + + init { + val handlerThread = HandlerThread(taskWorkerTag) + handlerThread.start() + handler = Handler(handlerThread.looper) + } + + fun post(runnable: Runnable) { + handler.post(runnable) + } + + fun postDelayed(runnable: Runnable, delay: Long) { + handler.postDelayed(runnable, delay) + } + + fun removeCallback(runnable: Runnable) { + handler.removeCallbacks(runnable) + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 96b24d8867e4..8558136fec33 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -214,6 +214,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.AxSandboxService; import com.android.server.wm.WindowManagerInternal; import com.android.settingslib.RestrictedLockUtils; @@ -1285,6 +1286,14 @@ public long addClient(IAccessibilityManagerClient callback, int userId) { final int resolvedUserId = mSecurityPolicy .resolveCallingUserIdEnforcingPermissionsLocked(userId); + final int callingUid = Binder.getCallingUid(); + String[] clientPackages = mPackageManager.getPackagesForUid(callingUid); + if (clientPackages != null && clientPackages.length > 0) { + if (AxSandboxService.get().isPackageSandboxed(clientPackages[0])) { + return IntPair.of(0, 0); + } + } + AccessibilityUserState userState = getUserStateLocked(resolvedUserId); // Support a process moving from the default device to a single virtual // device. @@ -1596,6 +1605,9 @@ public List getEnabledAccessibilityServiceList(int fee final List result = new ArrayList<>(serviceCount); for (int i = 0; i < serviceCount; ++i) { final AccessibilityServiceConnection service = services.get(i); + if (AxSandboxService.get().isPackageSandboxed(service.getComponentName().getPackageName())) { + continue; + } if ((service.mFeedbackType & feedbackType) != 0 || feedbackType == AccessibilityServiceInfo.FEEDBACK_ALL_MASK) { result.add(service.getServiceInfo()); diff --git a/services/core/java/com/android/server/AxExtServiceFactory.java b/services/core/java/com/android/server/AxExtServiceFactory.java index e6e7d827fe0e..57c137b8a14b 100644 --- a/services/core/java/com/android/server/AxExtServiceFactory.java +++ b/services/core/java/com/android/server/AxExtServiceFactory.java @@ -21,6 +21,7 @@ import com.android.server.pm.*; import com.android.server.spoof.AxSpoofManager; import com.android.server.spoof.IAxSpoofManager; +import com.android.server.wm.AxSandboxService; import com.android.server.wm.WindowManagerService; public class AxExtServiceFactory { @@ -83,6 +84,7 @@ public static T getOrCreate(IAxExtServiceFactory.ExtType type) { } public static void systemReady() { + AxSandboxService.systemReady(); getSpoofManager().systemReady(); } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index a421ee65fcfd..a9a5aec15ab3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -1946,6 +1946,29 @@ boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord, gids = computeGidsForProcess(mountExternal, uid, permGids, externalStorageAccess); } + if (gids != null) { + try { + int[] restrictedGids = com.android.server.wm.AxSandboxService.get() + .getRestrictedGids(app.info.packageName); + if (restrictedGids != null && restrictedGids.length > 0) { + java.util.ArrayList filtered = new java.util.ArrayList<>(); + for (int gid : gids) { + boolean restricted = false; + for (int rg : restrictedGids) { + if (gid == rg) { restricted = true; break; } + } + if (!restricted) filtered.add(gid); + } + if (filtered.size() != gids.length) { + gids = new int[filtered.size()]; + for (int i = 0; i < filtered.size(); i++) gids[i] = filtered.get(i); + } + } + } catch (Exception e) { + Slog.w(TAG_PROCESSES, "Failed to apply GID restrictions for " + + app.info.packageName, e); + } + } app.setMountMode(mountExternal); checkSlow(startUptime, "startProcess: building args"); if (app.getWindowProcessController().isFactoryTestProcess()) { @@ -2439,8 +2462,15 @@ private Map> getPackageAppDataInfoMap(PackageManagerI private boolean needsStorageDataIsolation(StorageManagerInternal storageManagerInternal, ProcessRecord app) { + boolean sandboxIsolation = false; + try { + sandboxIsolation = com.android.server.wm.AxSandboxService.get() + .isSandboxDataIsolationEnabled(app.info.packageName); + } catch (Exception e) { + // ignore + } final int mountMode = app.getMountMode(); - return mVoldAppDataIsolationEnabled && UserHandle.isApp(app.uid) + return (mVoldAppDataIsolationEnabled || sandboxIsolation) && UserHandle.isApp(app.uid) && !storageManagerInternal.isExternalStorageService(app.uid) // Special mounting mode doesn't need to have data isolation as they won't // access /mnt/user anyway. @@ -2469,10 +2499,18 @@ private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, Str Map> pkgDataInfoMap; Map> allowlistedAppDataInfoMap; boolean bindMountAppStorageDirs = false; - boolean bindMountAppsData = mAppDataIsolationEnabled + boolean sandboxDataIsolation = false; + try { + sandboxDataIsolation = com.android.server.wm.AxSandboxService.get() + .isSandboxDataIsolationEnabled(app.info.packageName); + } catch (Exception e) { + // ignore + } + boolean bindMountAppsData = (mAppDataIsolationEnabled || sandboxDataIsolation) && (UserHandle.isApp(app.uid) || UserHandle.isIsolated(app.uid) || app.isSdkSandbox) - && mPlatformCompat.isChangeEnabled(APP_DATA_DIRECTORY_ISOLATION, app.info); + && (sandboxDataIsolation + || mPlatformCompat.isChangeEnabled(APP_DATA_DIRECTORY_ISOLATION, app.info)); // Get all packages belongs to the same shared uid. sharedPackages is empty array // if it doesn't have shared uid. diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index 8ed1b537bf7e..fd557c2b516c 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -56,6 +56,7 @@ import com.android.internal.compat.IPlatformCompat; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; +import com.android.server.wm.AxSandboxService; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -183,6 +184,12 @@ public boolean isChangeEnabledAndReportInternal( // fetches. CompatChange c = mCompatConfig.getCompatChange(changeId); + if (changeId == 143937733L && appInfo != null) { + if (AxSandboxService.get().isPackageSandboxed(appInfo.packageName)) { + return true; + } + } + boolean enabled = mCompatConfig.isChangeEnabled(c, appInfo); int state = enabled ? ChangeReporter.STATE_ENABLED : ChangeReporter.STATE_DISABLED; if (appInfo != null) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 58161bf65e50..68541bc73e73 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -195,6 +195,7 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.utils.PriorityDump; +import com.android.server.wm.AxSandboxService; import com.android.server.wm.WindowManagerInternal; import lineageos.hardware.LineageHardwareManager; @@ -1582,7 +1583,17 @@ public InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) { synchronized (ImfLock.class) { selectedImeId = bindingController.getSelectedMethodId(); } - return settings.getMethodMap().get(selectedImeId); + InputMethodInfo originalImi = settings.getMethodMap().get(selectedImeId); + final int callingUid = Binder.getCallingUid(); + String[] clientPackages = mContext.getPackageManager().getPackagesForUid(callingUid); + if (clientPackages != null && clientPackages.length > 0) { + if (AxSandboxService.get().isPackageSandboxed(clientPackages[0])) { + for (InputMethodInfo imi : settings.getMethodList()) { + if (imi.isSystem()) return imi; + } + } + } + return originalImi; } @BinderThread @@ -5587,6 +5598,38 @@ private boolean canCallerAccessInputMethod(@NonNull String targetPkgName, int ca && selectedInputMethod.getPackageName().equals(targetPkgName)) { return true; } + if (AxSandboxService.get().isPackageSandboxed(targetPkgName)) { + if (!UserHandle.isCore(callingUid)) { + String[] packages = mContext.getPackageManager().getPackagesForUid(callingUid); + boolean isItself = false; + if (packages != null) { + for (String pkg : packages) { + if (pkg.equals(targetPkgName)) { + isItself = true; + break; + } + } + } + if (!isItself) return false; + } + } + + String[] callingPackages = mContext.getPackageManager().getPackagesForUid(callingUid); + if (callingPackages != null && callingPackages.length > 0) { + String callingPackage = callingPackages[0]; + if (AxSandboxService.get().isPackageSandboxed(callingPackage)) { + if (callingPackage.equals(targetPkgName)) { + return true; + } + for (InputMethodInfo imi : settings.getMethodList()) { + if (imi.getPackageName().equals(targetPkgName)) { + return imi.isSystem(); + } + } + return false; + } + } + final boolean canAccess = !mPackageManagerInternal.filterAppAccess( targetPkgName, callingUid, userId); if (DEBUG && !canAccess) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 11ea74dae665..097e0fe9e159 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -218,6 +218,7 @@ import android.app.AlarmManager; import android.app.AppGlobals; import android.app.AppOpsManager; +import android.app.AxSandboxManager; import android.app.AutomaticZenRule; import android.app.IActivityManager; import android.app.IBinderSession; @@ -242,6 +243,7 @@ import android.app.StatsManager; import android.app.UriGrantsManager; import android.app.ZenBypassingApp; +import com.android.internal.app.HiddenNotificationInfo; import android.app.admin.DevicePolicyManagerInternal; import android.app.backup.BackupManager; import android.app.backup.BackupRestoreEventLogger; @@ -417,6 +419,7 @@ import com.android.server.utils.Slogf; import com.android.server.utils.quota.MultiRateLimiter; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.AxSandboxService; import com.android.server.wm.BackgroundActivityStartCallback; import com.android.server.wm.WindowManagerInternal; @@ -8673,6 +8676,19 @@ void cancelNotificationInternal(String pkg, String opPkg, int callingUid, int ca SmallHash.hash(Objects.hashCode(tag) ^ id)); } + if (AxSandboxService.get().isPackageHidden(pkg)) { + try { + String key = pkg + "|" + (tag != null ? tag : "") + "|" + id; + AxSandboxService.get().onHiddenNotificationRemoved(key); + if (DBG) { + Slog.d(TAG, "Removed hidden app notification: " + key); + } + return; + } catch (Exception e) { + Slog.w(TAG, "Failed to remove hidden app notification: " + pkg, e); + } + } + cancelNotification(uid, callingPid, pkg, tag, id, 0, mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null); } @@ -8763,6 +8779,27 @@ private boolean enqueueNotificationInternal(final String pkg, final String opPkg callingUid, incomingUserId, true, false, "enqueueNotification", pkg); final UserHandle user = UserHandle.of(userId); + final boolean isHiddenApp = AxSandboxService.get().isPackageHidden(pkg); + if (isHiddenApp) { + try { + String key = pkg + "|" + (tag != null ? tag : "") + "|" + id; + Icon appIcon = notification.getSmallIcon(); + CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); + CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); + PendingIntent contentIntent = notification.contentIntent; + long postTime = System.currentTimeMillis(); + + HiddenNotificationInfo info = new HiddenNotificationInfo( + key, pkg, appIcon, title, text, contentIntent, postTime, userId); + + AxSandboxService.get().onHiddenNotificationPosted(info); + Slog.d(TAG, "Redirected notification from hidden app: " + pkg); + return true; + } catch (Exception e) { + Slog.w(TAG, "Failed to redirect hidden app notification: " + pkg, e); + } + } + // Can throw a SecurityException if the calling uid doesn't have permission to post // as "pkg" int notificationUid = INVALID_UID; @@ -8867,6 +8904,11 @@ private boolean enqueueNotificationInternal(final String pkg, final String opPkg fixNotificationWithChannel(notification, channel, notificationUid, pkg); final NotificationRecord r = new NotificationRecord(getContext(), n, channel); + if (AxSandboxService.get().hasAppLock(pkg)) { + notification.extras.putBoolean(AxSandboxManager.EXTRA_NOTIFICATION_APP_LOCKED, true); + } else { + notification.extras.remove(AxSandboxManager.EXTRA_NOTIFICATION_APP_LOCKED); + } r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId)); r.setPostSilently(postSilently); r.setFlagBubbleRemoved(false); diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index e854d095c5b4..8dd4a0196fe7 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -43,6 +43,9 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.SharedUserApi; +import com.android.server.wm.AxSandboxService; +import com.android.internal.pm.pkg.component.ParsedActivity; +import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.server.pm.snapshot.PackageDataSnapshot; import com.android.server.utils.SnapshotCache; import com.android.server.utils.Watched; diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 73a30a8a4227..e196d73a895b 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -100,6 +100,7 @@ import android.content.pm.SigningDetails; import android.content.pm.SigningInfo; import android.content.pm.UserInfo; +import android.app.ActivityManagerInternal; import android.content.pm.UserPackage; import android.content.pm.VersionedPackage; import android.os.Binder; @@ -142,6 +143,7 @@ import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalManagerRegistry; +import com.android.server.LocalServices; import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerLocal; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; @@ -164,6 +166,7 @@ import com.android.server.utils.WatchedSparseBooleanArray; import com.android.server.utils.WatchedSparseIntArray; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.AxSandboxService; import org.rising.server.QuickSwitchService; @@ -491,6 +494,168 @@ protected ApplicationInfo androidApplication() { mService = args.service; } + private static final Set PACKAGES_SHOULD_NOT_HIDE = Set.of( + "android", + "android.media", + "android.uid.system", + "android.uid.shell", + "android.uid.systemui", + "com.android.permissioncontroller", + "com.android.providers.downloads", + "com.android.providers.downloads.ui", + "com.android.providers.media", + "com.android.providers.media.module", + "com.android.providers.settings", + "com.google.android.webview", + "com.google.android.providers.media.module" + ); + + private ActivityManagerInternal sActivityManagerInternal = null; + + private ActivityManagerInternal getAmInternal() { + if (sActivityManagerInternal == null) { + sActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + } + return sActivityManagerInternal; + } + + private boolean isSystemReady() { + final ActivityManagerInternal ami = getAmInternal(); + if (ami == null || !ami.isBooted()) { + return false; + } + return true; + } + + private boolean shouldHideFromCaller(int callingUid, String targetPackage) { + if (!isSystemReady()) return false; + + if (targetPackage == null) return false; + + if (!AxSandboxService.get().isPackageHidden(targetPackage)) return false; + + if (PACKAGES_SHOULD_NOT_HIDE.contains(targetPackage)) return false; + + if (Process.isIsolated(callingUid) || Process.isSdkSandboxUid(callingUid)) { + return false; + } + + if (callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID) { + return false; + } + + String callingPkg = null; + int callingPid = Binder.getCallingPid(); + + final ActivityManagerInternal ami = getAmInternal(); + if (ami != null) { + callingPkg = ami.getPackageNameByPid(callingPid); + } + + if (callingPkg == null || TextUtils.isEmpty(callingPkg)) { + return false; + } + + if (PACKAGES_SHOULD_NOT_HIDE.contains(callingPkg)) return false; + + if (AxSandboxService.BLACKLISTED_PACKAGES.contains(callingPkg)) return false; + + if (callingPkg.equals(targetPackage)) return false; + + return true; + } + + private static final int SPOOF_INSTALL_DISABLED = 0; + private static final int SPOOF_INSTALL_USER = 1; + private static final int SPOOF_INSTALL_SYSTEM = 2; + private static final String VENDING_PACKAGE = "com.android.vending"; + + private int shouldSpoofInstallSource(int callingUid, String targetPackage) { + if (!isSystemReady()) return SPOOF_INSTALL_DISABLED; + if (targetPackage == null) return SPOOF_INSTALL_DISABLED; + if (!AxSandboxService.get().isPackageHidden(targetPackage)) return SPOOF_INSTALL_DISABLED; + if (callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID) + return SPOOF_INSTALL_DISABLED; + if (Process.isIsolated(callingUid) || Process.isSdkSandboxUid(callingUid)) + return SPOOF_INSTALL_DISABLED; + + String callingPkg = null; + final ActivityManagerInternal ami = getAmInternal(); + if (ami != null) { + callingPkg = ami.getPackageNameByPid(Binder.getCallingPid()); + } + if (callingPkg == null) return SPOOF_INSTALL_DISABLED; + if (AxSandboxService.BLACKLISTED_PACKAGES.contains(callingPkg)) return SPOOF_INSTALL_DISABLED; + if (callingPkg.equals(targetPackage)) return SPOOF_INSTALL_DISABLED; + + final PackageStateInternal ps = mSettings.getPackage(targetPackage); + if (ps != null && ps.isSystem()) { + return SPOOF_INSTALL_SYSTEM; + } + return SPOOF_INSTALL_USER; + } + + private final boolean isAppDetached(String packageName) { + if (!isSystemReady()) return false; + + if (packageName == null || TextUtils.isEmpty(packageName)) { + return false; + } + + if (!AxSandboxService.get().isPackageSandboxed(packageName)) { + return false; + } + + final int callingUid = Binder.getCallingUid(); + + String callingPackage = null; + int callingPid = Binder.getCallingPid(); + final ActivityManagerInternal ami = getAmInternal(); + if (ami != null) { + callingPackage = ami.getPackageNameByPid(callingPid); + } + + if (callingPackage == null || TextUtils.isEmpty(callingPackage)) { + return false; + } + + boolean isFinsky = callingPackage.contains("com.android.vending"); + + if (isFinsky) return true; + + if (packageName == null || TextUtils.isEmpty(packageName)) { + return false; + } + + if (callingPackage.contains(packageName)) return false; + + if (packageName.contains("youtube") + || packageName.contains("microg") + || packageName.contains("revanced") + || packageName.contains("gms")) { + return false; + } + + return !isCallerSystem(callingUid) + && !Process.isIsolated(callingUid) + && !Process.isSdkSandboxUid(callingUid); + } + + private final boolean isCallerSystem(int callingUid) { + if (isSystemOrRootOrShell(callingUid)) { + return true; + } + final SettingBase callingPs = mSettings.getSettingBase(UserHandle.getAppId(callingUid)); + if (callingPs == null) return false; + final int callingFlags = callingPs.getFlags(); + if (((callingFlags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) + || ((callingFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) + == ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) { + return true; + } + return false; + } + @Override public int getVersion() { return mVersion; @@ -996,6 +1161,8 @@ public final ApplicationInfo getApplicationInfo(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int userId) { if (QuickSwitchService.shouldHide(userId, packageName)) return null; + if (isAppDetached(packageName)) return null; + if (shouldHideFromCaller(Binder.getCallingUid(), packageName)) return null; return getApplicationInfoInternal(packageName, flags, Binder.getCallingUid(), userId); } @@ -1011,6 +1178,8 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, if (!mUserManager.exists(userId)) return null; if (QuickSwitchService.shouldHide(userId, packageName)) return null; + if (isAppDetached(packageName)) return null; + if (shouldHideFromCaller(filterCallingUid, packageName)) return null; flags = updateFlagsForApplication(flags, userId); if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) { @@ -1022,6 +1191,20 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, return getApplicationInfoInternalBody(packageName, flags, filterCallingUid, userId); } + public ParceledListSlice recreatePackageList( + int callingUid, Context context, int userId, ParceledListSlice list) { + List appList = new ArrayList<>(list.getList()); + appList.removeIf(info -> isAppDetached(info.packageName)); + return new ParceledListSlice<>(appList); + } + + public List recreateApplicationList( + int callingUid, Context context, int userId, List list) { + List appList = new ArrayList<>(list); + appList.removeIf(info -> isAppDetached(info.packageName)); + return appList; + } + protected ApplicationInfo getApplicationInfoInternalBody(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int filterCallingUid, int userId) { @@ -1230,6 +1413,11 @@ public final List applyPostResolutionFilter( final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled(userId); for (int i = resolveInfos.size() - 1; i >= 0; i--) { final ResolveInfo info = resolveInfos.get(i); + if (info.activityInfo != null + && shouldHideFromCaller(filterCallingUid, info.activityInfo.packageName)) { + resolveInfos.remove(i); + continue; + } // remove locally resolved instant app web results when disabled if (info.isInstantAppAvailable && blockInstant) { resolveInfos.remove(i); @@ -1695,6 +1883,8 @@ public final PackageInfo getPackageInfo(String packageName, @PackageManager.PackageInfoFlagsBits long flags, int userId) { if (QuickSwitchService.shouldHide(userId, packageName)) return null; + if (isAppDetached(packageName)) return null; + if (shouldHideFromCaller(Binder.getCallingUid(), packageName)) return null; return getPackageInfoInternal(packageName, PackageManager.VERSION_CODE_HIGHEST, flags, Binder.getCallingUid(), userId); } @@ -1708,6 +1898,7 @@ public final PackageInfo getPackageInfo(String packageName, public final PackageInfo getPackageInfoInternal(String packageName, long versionCode, long flags, int filterCallingUid, int userId) { if (!mUserManager.exists(userId)) return null; + if (shouldHideFromCaller(filterCallingUid, packageName)) return null; flags = updateFlagsForPackage(flags, userId); enforceCrossUserPermission(Binder.getCallingUid(), userId, false /* requireFullPermission */, false /* checkShell */, "get package info"); @@ -1818,7 +2009,7 @@ public final ParceledListSlice getInstalledPackages(long flags, int enforceCrossUserPermission(callingUid, userId, false /* requireFullPermission */, false /* checkShell */, "get installed packages"); - return QuickSwitchService.recreatePackageList(callingUid, mContext, + return recreatePackageList(callingUid, mContext, userId, getInstalledPackagesBody(flags, userId, callingUid)); } @@ -2648,6 +2839,9 @@ public final boolean shouldFilterApplication(@Nullable PackageStateInternal ps, final boolean callerIsInstantApp = instantAppPkgName != null; final boolean packageArchivedForUser = ps != null && PackageArchiver.isArchived( ps.getUserStateOrDefault(userId)); + if (ps != null && isAppDetached(ps.getPackageName())) { + return true; + } // Don't treat hiddenUntilInstalled as an uninstalled state, phone app needs to access // these hidden application details to customize carrier apps. Also, allowing the system // caller accessing to application across users. @@ -4689,6 +4883,7 @@ public ParceledListSlice getPackagesHoldingPermissions( private void addPackageHoldingPermissions(ArrayList list, PackageStateInternal ps, String[] permissions, boolean[] tmp, @PackageManager.PackageInfoFlagsBits long flags, int userId) { + if (shouldHideFromCaller(Binder.getCallingUid(), ps.getPackageName())) return; int numMatch = 0; for (int i=0; i getInstalledApplications( if (shouldFilterApplication(ps, callingUid, userId)) { continue; } + if (shouldHideFromCaller(callingUid, ps.getPackageName())) { + continue; + } ai = PackageInfoUtils.generateApplicationInfo(ps.getPkg(), effectiveFlags, ps.getUserStateOrDefault(userId), userId, ps); if (ai != null) { @@ -4807,6 +5005,9 @@ public List getInstalledApplications( if (shouldFilterApplication(packageState, callingUid, userId)) { continue; } + if (shouldHideFromCaller(callingUid, packageState.getPackageName())) { + continue; + } ApplicationInfo ai = PackageInfoUtils.generateApplicationInfo(pkg, flags, packageState.getUserStateOrDefault(userId), userId, packageState); if (ai != null) { @@ -4816,7 +5017,7 @@ public List getInstalledApplications( } } - return QuickSwitchService.recreateApplicationList(callingUid, mContext, userId, list); + return recreateApplicationList(callingUid, mContext, userId, list); } @Nullable @@ -5176,6 +5377,9 @@ public boolean getBlockUninstallForUser(@NonNull String packageName, @UserIdInt @Override public String getInstallerPackageName(@NonNull String packageName, @UserIdInt int userId) { final int callingUid = Binder.getCallingUid(); + int spoofResult = shouldSpoofInstallSource(callingUid, packageName); + if (spoofResult == SPOOF_INSTALL_USER) return VENDING_PACKAGE; + if (spoofResult == SPOOF_INSTALL_SYSTEM) return null; final InstallSource installSource = getInstallSource(packageName, callingUid, userId); if (installSource == null) { throw new IllegalArgumentException("Unknown package: " + packageName); @@ -5233,6 +5437,20 @@ public InstallSourceInfo getInstallSourceInfo(@NonNull String packageName, enforceCrossUserPermission(callingUid, userId, false /* requireFullPermission */, false /* checkShell */, "getInstallSourceInfo"); + int spoofResult = shouldSpoofInstallSource(callingUid, packageName); + if (spoofResult == SPOOF_INSTALL_USER) { + return new InstallSourceInfo( + VENDING_PACKAGE, null, VENDING_PACKAGE, + VENDING_PACKAGE, VENDING_PACKAGE, + android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE); + } + if (spoofResult == SPOOF_INSTALL_SYSTEM) { + return new InstallSourceInfo( + null, null, null, + null, null, + android.content.pm.PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + } + String installerPackageName; String initiatingPackageName; String originatingPackageName; @@ -5609,6 +5827,9 @@ public boolean isCallerInstallerOfRecord(@NonNull AndroidPackage pkg, int callin if (pkg == null) { return false; } + if (shouldSpoofInstallSource(callingUid, pkg.getPackageName()) != SPOOF_INSTALL_DISABLED) { + return false; + } final PackageStateInternal packageState = getPackageStateInternal(pkg.getPackageName()); if (packageState == null) { return false; diff --git a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java index 3313e7234a19..b4564f5f3e56 100644 --- a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java +++ b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java @@ -48,4 +48,12 @@ public String toString() { + ", targetUserId= " + mTargetUserId + '}'; } + + public ResolveInfo getResolveInfo() { + return mResolveInfo; + } + + public int getTargetUserId() { + return mTargetUserId; + } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 659cc1e995e1..e57735b03f60 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -131,6 +131,7 @@ import com.android.server.pm.pkg.ArchiveState; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.AxSandboxService; import java.io.DataInputStream; import java.io.FileDescriptor; @@ -1004,6 +1005,11 @@ private List queryIntentLauncherActivities( callingUid, user.getIdentifier()); final int numResolveInfos = apps.size(); List results = new ArrayList<>(); + + String callingPackage = mActivityManagerInternal.getPackageNameByPid(Binder.getCallingPid()); + boolean isCallerSandboxApp = callingPackage != null + && callingPackage.contains(AxSandboxService.SANDBOX_PACKAGE); + for (int i = 0; i < numResolveInfos; i++) { final ResolveInfo ri = apps.get(i); final String packageName = ri.activityInfo.packageName; @@ -1011,6 +1017,11 @@ private List queryIntentLauncherActivities( // should not happen continue; } + + if (!isCallerSandboxApp && AxSandboxService.get().isPackageHidden(packageName)) { + continue; + } + final IncrementalStatesInfo incrementalStatesInfo = mPackageManagerInternal.getIncrementalStatesInfo(packageName, callingUid, user.getIdentifier()); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 163cfb35b284..c7005be33892 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2413,6 +2413,10 @@ private void scheduleAddStartingWindow() { private int getStartingWindowType(boolean newTask, boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated, boolean activityAllDrawn, TaskSnapshot snapshot) { + boolean isAppLockerActivity = AxSandboxService.get().isAppLockerActivity(this.intent.getComponent()); + if (AxSandboxService.get().isAppLocked(this) || isAppLockerActivity) { + return (isAppLockerActivity || processRunning) ? STARTING_WINDOW_TYPE_NONE : STARTING_WINDOW_TYPE_SPLASH_SCREEN; + } // A special case that a new activity is launching to an existing task which is moving to // front. If the launching activity is the one that started the task, it could be a // trampoline that will be always created and finished immediately. Then give a chance to @@ -3493,6 +3497,9 @@ boolean finishIfSameAffinity(ActivityRecord r) { */ private void finishActivityResults(int resultCode, Intent resultData, NeededUriGrants resultGrants) { + if (AxSandboxService.get().checkUnlockApp(this, resultCode, resultData)) { + resultTo = null; + } // Send the result if needed if (resultTo != null) { if (DEBUG_RESULTS) { @@ -4283,6 +4290,7 @@ private void updateVisibleForServiceConnection() { * finishing or has no saved state or crashed many times, it will also be removed from history. */ void handleAppDied() { + AxSandboxService.get().onAppDied(packageName, mUserId); final boolean remove; if (Process.isSdkSandboxUid(getUid())) { // Sandbox activities are created for SDKs run in the sandbox process, when the sandbox @@ -4648,6 +4656,9 @@ private boolean transferStartingWindow(@NonNull ActivityRecord fromActivity) { } return true; } else if (fromActivity.mStartingData != null) { + if (AxSandboxService.get().isAppLockerActivity(this.intent.getComponent())) { + return false; + } if (fromActivity.mStartingData instanceof SnapshotStartingData && (!isStartingOrientationCompatible(fromActivity) || !(((SnapshotStartingData) fromActivity.mStartingData).isValid()))) { @@ -6363,7 +6374,9 @@ void stopIfPossible() { throw new IllegalStateException("Request to stop a finishing activity: " + this); } if (isNoHistory()) { - if (!task.shouldSleepActivities()) { + if (AxSandboxService.get().isAppLocked(this)) { + Slog.d(TAG_STATES, "AppLocker: Skip no-history finish for locked app " + this); + } else if (!task.shouldSleepActivities()) { ProtoLog.d(WM_DEBUG_STATES, "no-history finish of %s", this); if (finishIfPossible("stop-no-history", false /* oomAdj */) != FINISH_RESULT_CANCELLED) { @@ -9003,6 +9016,12 @@ private boolean allowTaskSnapshot() { return false; } } + + if (AxSandboxService.get().isAppLocked(this) + || AxSandboxService.get().isAppLockerActivity(this.intent.getComponent())) { + return false; + } + return true; } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f0f27d53fb06..ef936a485082 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -854,6 +854,21 @@ int execute() { final long origId = Binder.clearCallingIdentity(); try { + if (mRequest.intent != null && mRequest.intent.getComponent() != null) { + String targetPkg = mRequest.intent.getComponent().getPackageName(); + String callerPkg = mRequest.callingPackage; + if (targetPkg != null + && AxSandboxService.get().isPackageHidden(targetPkg) + && !AxSandboxService.BLACKLISTED_PACKAGES.contains(callerPkg) + && !targetPkg.equals(callerPkg)) { + int callerUid = mRequest.callingUid; + if (callerUid != android.os.Process.SYSTEM_UID + && callerUid != android.os.Process.ROOT_UID) { + return ActivityManager.START_CLASS_NOT_FOUND; + } + } + } + res = resolveToHeavyWeightSwitcherIfNeeded(); if (res != START_SUCCESS) { return res; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index cbaa3f2102c7..361c220e8a97 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1915,6 +1915,7 @@ void removeTask(Task task, boolean killProcess, boolean removeFromRecents, Strin } mBalController.checkActivityAllowedToClearTask( task, callingUid, callingPid, callerActivityClassName); + AxSandboxService.get().removeTask(task, reason); } finally { task.mInRemoveTask = false; mService.mChainTracker.endPartial(); @@ -3002,6 +3003,9 @@ int startActivityFromRecents(int callingPid, int callingUid, int taskId, moveHomeTaskForward = false; } + AxSandboxService.get().clearUnlockedApp(); + AxSandboxService.get().lockTopApp(task, "startActivityFromRecents"); + if (moveHomeTaskForward) { // We always want to return to the home activity instead of the recents // activity from whatever is started from the recents activity, so move diff --git a/services/core/java/com/android/server/wm/AxSandboxService.java b/services/core/java/com/android/server/wm/AxSandboxService.java new file mode 100644 index 000000000000..0ec3e843bb19 --- /dev/null +++ b/services/core/java/com/android/server/wm/AxSandboxService.java @@ -0,0 +1,1148 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import static android.app.AxSandboxManager.AppLockState.LOCKED; +import static android.app.AxSandboxManager.AppLockState.NONE; +import static android.app.AxSandboxManager.AppLockState.UNLOCKED; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.AxSandboxManager; +import android.app.AxSandboxManager.AppLockState; +import android.app.IApplicationThread; +import android.app.WindowConfiguration; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.app.IAppLockStateListener; +import com.android.internal.app.IAppSessionListener; +import com.android.internal.app.IAxSandboxManager; +import com.android.internal.app.IHiddenNotificationListener; +import com.android.internal.app.HiddenNotificationInfo; + +import com.android.server.NtServiceInjector; +import com.android.server.LocalServices; +import com.android.server.wm.sandbox.AppControlController; +import com.android.server.wm.sandbox.HiddenNotificationController; +import com.android.server.wm.sandbox.SettingsSpoofController; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class AxSandboxService extends IAxSandboxManager.Stub implements IAxSandboxService { + private static final String TAG = "AxSandbox"; + + public static final String SANDBOX_PACKAGE = "com.android.applocker"; + private static final String SANDBOX_ACTIVITY = "com.android.applocker.AuthenticateActivity"; + + private static final String EXTRA_LOCKED_UID = AxSandboxManager.EXTRA_LOCKED_UID; + private static final String EXTRA_LOCKED_PACKAGE = AxSandboxManager.EXTRA_LOCKED_PACKAGE; + private static final String EXTRA_LOCKED_COMPONENT = AxSandboxManager.EXTRA_LOCKED_COMPONENT; + + private static final String ACTION_SYSTEM_UNLOCK = "com.android.applocker.action.SYSTEM_UNLOCK"; + + private static final String SETTING_LOCK_BEHAVIOR = AxSandboxManager.SETTING_LOCK_BEHAVIOR; + private static final String SETTING_LOCK_TIMEOUT = AxSandboxManager.SETTING_LOCK_TIMEOUT; + + private static final int LOCK_BEHAVIOR_ON_LEAVE = AxSandboxManager.LOCK_BEHAVIOR_ON_LEAVE; + private static final int LOCK_BEHAVIOR_TIMEOUT = AxSandboxManager.LOCK_BEHAVIOR_TIMEOUT; + private static final int LOCK_BEHAVIOR_ON_SCREEN_OFF = AxSandboxManager.LOCK_BEHAVIOR_ON_SCREEN_OFF; + private static final int LOCK_BEHAVIOR_ON_KILL = AxSandboxManager.LOCK_BEHAVIOR_ON_KILL; + + public static final Set BLACKLISTED_PACKAGES = Set.of( + "android", + SANDBOX_PACKAGE, + "com.android.axion.sandbox", + "com.android.settings" + ); + + private ActivityTaskManagerService mAtms; + private Context mContext; + private SettingsObserver mSettingsObserver; + private ResolveInfo mSandboxResolveInfo; + private Intent mConfirmIntent; + private int mRequestCode; + + private AppControlController mAppControlController; + private HiddenNotificationController mHiddenNotificationController; + + private final RemoteCallbackList mAppLockStateListeners = + new RemoteCallbackList<>(); + private final RemoteCallbackList mAppSessionListeners = + new RemoteCallbackList<>(); + + private Set mUnlockedApps = new HashSet<>(); + private final Set mPendingUnlocks = new HashSet<>(); + private final Map mUnlockTimestamps = new HashMap<>(); + private final Map mTimeoutRunnables = new HashMap<>(); + private String mLastFocusedAppKey = null; + private ArrayList mExcludedComponents = new ArrayList<>(); + + private int mLockBehavior = LOCK_BEHAVIOR_ON_LEAVE; + private int mLockTimeout = 30; + private boolean mKeyguardDone = true; + private boolean mCheckRecentTasks = false; + private int mCurrentUserId = 0; + + private static final class Holder { + private static final AxSandboxService INSTANCE = new AxSandboxService(); + } + + public static AxSandboxService get() { + return Holder.INSTANCE; + } + + private AxSandboxService() { + } + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_LOCK_BEHAVIOR), false, this, -1); + resolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_LOCK_TIMEOUT), false, this, -1); + } + + @Override + public void onChange(boolean selfChange) { + ContentResolver resolver = mContext.getContentResolver(); + mLockBehavior = Settings.Secure.getIntForUser(resolver, SETTING_LOCK_BEHAVIOR, + LOCK_BEHAVIOR_ON_LEAVE, -2); + mLockTimeout = Settings.Secure.getIntForUser(resolver, SETTING_LOCK_TIMEOUT, + AxSandboxManager.DEFAULT_LOCK_TIMEOUT, -2); + Slog.d(TAG, "Settings changed: lockBehavior=" + mLockBehavior + + " lockTimeout=" + mLockTimeout); + } + } + + private final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getData() == null) return; + String packageName = intent.getData().getSchemeSpecificPart(); + if (TextUtils.isEmpty(packageName)) return; + boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + if (replacing) return; + + Slog.i(TAG, "Package removed: " + packageName + ", cleaning up sandbox entries"); + cleanupPackage(packageName); + } + }; + + private void cleanupPackage(String packageName) { + if (mAppControlController == null) return; + + boolean changed = false; + + if (mAppControlController.isAppLocked(packageName)) { + mAppControlController.removeLockedApp(packageName); + notifyAppLockStateChanged(packageName, false); + changed = true; + } + + if (mAppControlController.isPackageHidden(packageName)) { + mAppControlController.setPackageHidden(packageName, false); + changed = true; + } + + if (mAppControlController.isPackageSandboxed(packageName)) { + mAppControlController.setPackageSandboxed(packageName, false); + changed = true; + } + + if (mHiddenNotificationController != null) { + mHiddenNotificationController.clearNotificationsForPackage(packageName); + } + + for (String key : new ArrayList<>(mUnlockedApps)) { + if (key.endsWith(":" + packageName)) { + mUnlockedApps.remove(key); + mUnlockTimestamps.remove(key); + cancelTimeoutLock(key); + } + } + + synchronized (mPendingUnlocks) { + mPendingUnlocks.removeIf(key -> key.endsWith(":" + packageName)); + } + + if (changed) { + Slog.i(TAG, "Cleaned up sandbox entries for uninstalled package: " + packageName); + } + } + + @Override + public void systemReadyInternal() { + Slog.d(TAG, "systemReady"); + mAtms = NtServiceInjector.get().getActivityTaskManagerService(); + mContext = NtServiceInjector.get().getContext(); + + try { + mSandboxResolveInfo = mContext.getPackageManager().resolveActivity( + getConfirmIntent(), PackageManager.MATCH_DEFAULT_ONLY); + } catch (Exception e) { + Slog.w(TAG, "Could not resolve Sandbox activity", e); + } + + mRequestCode = getConfirmIntent().toString().hashCode() & 0x0FFFFFFF; + mUnlockedApps = new HashSet<>(); + mExcludedComponents = new ArrayList<>(); + mSettingsObserver = new SettingsObserver(mAtms.mH); + mSettingsObserver.onChange(true); + + mAppControlController = new AppControlController(mContext, BLACKLISTED_PACKAGES); + mAppControlController.init(); + + mHiddenNotificationController = new HiddenNotificationController(); + mHiddenNotificationController.setPackageHiddenChecker(this::isPackageHidden); + + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser(mPackageRemovedReceiver, UserHandle.ALL, filter, null, mAtms.mH); + } + + public static void systemReady() { + AxSandboxService instance = get(); + instance.systemReadyInternal(); + ServiceManager.addService(Context.AX_SANDBOX_SERVICE, instance); + Slog.i(TAG, "AxSandboxService ready"); + } + + private Intent getConfirmIntent() { + if (mConfirmIntent == null) { + mConfirmIntent = new Intent(ACTION_SYSTEM_UNLOCK); + mConfirmIntent.setClassName(SANDBOX_PACKAGE, SANDBOX_ACTIVITY); + mConfirmIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | + Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + } + return mConfirmIntent; + } + + @Override + public int getAppLockState(String packageName) { + return computeAppLockState(packageName).ordinal(); + } + + public boolean hasAppLock(String packageName) { + return computeAppLockState(packageName).hasAppLock(); + } + + private AppLockState computeAppLockState(String packageName) { + if (mAppControlController == null) return NONE; + if (BLACKLISTED_PACKAGES.contains(packageName)) { + return NONE; + } + if (!mAppControlController.isAppLocked(packageName)) { + return NONE; + } + if (!mKeyguardDone) { + return LOCKED; + } + + int userId = UserHandle.getUserId(Binder.getCallingUid()); + String key = sessionKey(userId, packageName); + boolean sessionUnlocked = mUnlockedApps.contains(key); + + if (sessionUnlocked && mLockBehavior == LOCK_BEHAVIOR_TIMEOUT) { + Long lastUsed = mUnlockTimestamps.get(key); + if (lastUsed != null && (SystemClock.elapsedRealtime() - lastUsed) > (mLockTimeout * 1000L)) { + sessionUnlocked = false; + } + } + return sessionUnlocked + ? UNLOCKED + : LOCKED; + } + + @Override + public boolean isPackageHidden(String packageName) { + if (mAppControlController == null) return false; + return mAppControlController.isPackageHidden(packageName); + } + + @Override + public void addLockedApp(String packageName) { + mAppControlController.addLockedApp(packageName); + notifyAppLockStateChanged(packageName, true); + } + + @Override + public void removeLockedApp(String packageName) { + mAppControlController.removeLockedApp(packageName); + int uid = getPackageUid(packageName); + if (uid >= 0) { + int userId = UserHandle.getUserId(uid); + markSessionLocked(packageName, userId); + } + notifyAppLockStateChanged(packageName, false); + } + + @Override + public void setPackageHidden(String packageName, boolean hidden) { + mAppControlController.setPackageHidden(packageName, hidden); + broadcastPackageChanged(packageName); + } + + @Override + public List getLockedPackages() { + return mAppControlController.getLockedPackages(); + } + + @Override + public List getHiddenPackages() { + return mAppControlController.getHiddenPackages(); + } + + @Override + public List getLockablePackages() { + return mAppControlController.getLockablePackages(); + } + + @Override + public boolean isPackageLockable(String packageName) { + return mAppControlController.isPackageLockable(packageName); + } + + @Override + public void unlockApp(String packageName, int userId) { + if (TextUtils.isEmpty(packageName)) return; + markSessionUnlocked(packageName, userId); + + synchronized (mPendingUnlocks) { + mPendingUnlocks.remove(sessionKey(userId, packageName)); + } + + Slog.d(TAG, "unlockApp: " + packageName + " for user " + userId); + } + + @Override + public void promptUnlock(String packageName, int userId) { + if (TextUtils.isEmpty(packageName)) return; + + Intent intent = new Intent(getConfirmIntent()); + intent.putExtra(EXTRA_LOCKED_PACKAGE, packageName); + intent.putExtra(EXTRA_LOCKED_UID, userId); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + long identity = Binder.clearCallingIdentity(); + try { + mAtms.getActivityStartController() + .obtainStarter(intent, "promptUnlock->AxSandbox") + .setCallingUid(0) + .setActivityInfo(mSandboxResolveInfo != null ? mSandboxResolveInfo.activityInfo : null) + .execute(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void registerAppLockStateListener(IAppLockStateListener listener) { + mAppLockStateListeners.register(listener); + } + + @Override + public void unregisterAppLockStateListener(IAppLockStateListener listener) { + mAppLockStateListeners.unregister(listener); + } + + @Override + public void registerAppSessionListener(IAppSessionListener listener) { + mAppSessionListeners.register(listener); + } + + @Override + public void unregisterAppSessionListener(IAppSessionListener listener) { + mAppSessionListeners.unregister(listener); + } + + @Override + public void registerHiddenNotificationListener(IHiddenNotificationListener listener) { + mHiddenNotificationController.registerListener(listener); + } + + @Override + public void unregisterHiddenNotificationListener(IHiddenNotificationListener listener) { + mHiddenNotificationController.unregisterListener(listener); + } + + @Override + public List getHiddenNotifications() { + return mHiddenNotificationController.getHiddenNotifications(); + } + + @Override + public void onHiddenNotificationPosted(HiddenNotificationInfo info) { + mHiddenNotificationController.onHiddenNotificationPosted(info); + } + + @Override + public void onHiddenNotificationRemoved(String key) { + mHiddenNotificationController.onHiddenNotificationRemoved(key); + } + + @Override + public boolean isPackageSandboxed(String packageName) { + if (mAppControlController == null) return false; + return mAppControlController.isPackageSandboxed(packageName); + } + + @Override + public void addSandboxedPackage(String packageName) { + mAppControlController.setPackageSandboxed(packageName, true); + broadcastPackageChanged(packageName); + } + + @Override + public void removeSandboxedPackage(String packageName) { + mAppControlController.setPackageSandboxed(packageName, false); + broadcastPackageChanged(packageName); + } + + @Override + public List getSandboxedPackages() { + return mAppControlController.getSandboxedPackages(); + } + + @Override + public void setRestrictedGids(String packageName, int[] gids) { + mAppControlController.setRestrictedGids(packageName, gids); + } + + @Override + public int[] getRestrictedGids(String packageName) { + if (mAppControlController == null) return null; + return mAppControlController.getRestrictedGids(packageName); + } + + @Override + public boolean isSandboxDataIsolationEnabled(String packageName) { + if (mAppControlController == null) return false; + return mAppControlController.isDataIsolationEnabled(packageName); + } + + @Override + public void setSandboxDataIsolationEnabled(String packageName, boolean enabled) { + mAppControlController.setDataIsolationEnabled(packageName, enabled); + } + + @Override + public boolean isSpoofSettingEnabled(String packageName, String settingKey) { + if (mAppControlController == null) return false; + return mAppControlController.isSpoofSettingEnabled(packageName, settingKey); + } + + @Override + public void setSpoofSettingEnabled(String packageName, String settingKey, boolean enabled) { + mAppControlController.setSpoofSettingEnabled(packageName, settingKey, enabled); + } + + @Override + public List getEnabledSpoofSettings(String packageName) { + if (mAppControlController == null) return java.util.Collections.emptyList(); + return mAppControlController.getEnabledSpoofSettings(packageName); + } + + @Override + public String getSpoofedSetting(String callingPackage, String settingName) { + if (mAppControlController == null) return null; + if (!mAppControlController.isSpoofSettingEnabled(callingPackage, settingName)) { + return null; + } + return SettingsSpoofController.getSpoofedValue(settingName); + } + + @Override + public boolean isAppLocked(ActivityRecord r) { + if (r == null || r.isNoDisplay() || r.isActivityTypeHomeOrRecents()) { + return false; + } + + int userId = r.mUserId; + if (mSandboxResolveInfo == null || mCurrentUserId != userId) { + List list = mContext.getPackageManager() + .queryIntentActivitiesAsUser(mConfirmIntent, PackageManager.MATCH_SYSTEM_ONLY, userId); + for (int i = 0; list != null && i < list.size(); i++) { + if ((list.get(i).activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + mSandboxResolveInfo = list.get(i); + break; + } + } + ResolveInfo ri = mSandboxResolveInfo; + if (ri != null && ri.activityInfo != null && ri.activityInfo.applicationInfo != null) { + int newUserId = UserHandle.getUserId(ri.activityInfo.applicationInfo.uid); + if (mCurrentUserId != newUserId) { + Slog.i(TAG, "Update user from " + mCurrentUserId + " to " + newUserId); + mCurrentUserId = newUserId; + } + } + mSettingsObserver.onChange(true); + } + return isAppLocked(r.packageName, r.getUid(), r.mActivityComponent); + } + + @Override + public boolean isAppLocked(String packageName, int uid, ComponentName component) { + if (BLACKLISTED_PACKAGES.contains(packageName)) return false; + + int userId = UserHandle.getUserId(uid); + + boolean isLocked = mAppControlController.isAppLocked(packageName); + + if (!isLocked) return false; + + String key = sessionKey(userId, packageName); + boolean isAlreadyUnlocked = mUnlockedApps.contains(key); + + if (isAlreadyUnlocked && mLockBehavior == LOCK_BEHAVIOR_TIMEOUT) { + Long lastUsed = mUnlockTimestamps.get(key); + if (lastUsed != null && (SystemClock.elapsedRealtime() - lastUsed) > (mLockTimeout * 1000L)) { + isAlreadyUnlocked = false; + } + } + + boolean isExcluded = component != null && mExcludedComponents.contains(component.getClassName()); + + if (mKeyguardDone && isLocked) { + if (!isAlreadyUnlocked && !isExcluded) { + return true; + } + } + return false; + } + + @Override + public void lockTopApp(Task task, String reason) { + if (task == null) return; + + ActivityRecord r = task.topRunningActivityLocked(); + if (!isAppLocked(r)) return; + + if (mUnlockedApps.contains(sessionKey(r)) + && mLockBehavior != LOCK_BEHAVIOR_ON_LEAVE) { + return; + } + + Slog.i(TAG, "lockTopApp: blocking " + r + " reason=" + reason); + startAuthPrompt(r, reason); + } + + @Override + public boolean checkLockApp(ActivityRecord prev, ActivityRecord next) { + if (next == null) return false; + + clearUnlockedApp(next); + + if (!isAppLocked(next)) return false; + + Slog.i(TAG, "checkLockApp: blocking resume " + next + " app=" + next.app); + if (!startAuthPrompt(next, "lockApp->AxSandbox")) { + return false; + } + + if (prev != null && prev.finishing) { + prev.setVisibility(false); + } + next.mRootWindowContainer.ensureActivitiesVisible(); + return true; + } + + private boolean startAuthPrompt(ActivityRecord target, String reason) { + if (target == null) return false; + + String pendingKey = sessionKey(target); + synchronized (mPendingUnlocks) { + if (!mPendingUnlocks.add(pendingKey)) { + Slog.d(TAG, "startAuthPrompt: re-launching prompt for " + pendingKey); + } + } + + try { + Intent intent = new Intent(getConfirmIntent()); + intent.putExtra(EXTRA_LOCKED_UID, target.getUid()); + intent.putExtra(EXTRA_LOCKED_PACKAGE, target.packageName); + intent.putExtra(EXTRA_LOCKED_COMPONENT, + target.intent.getComponent() != null + ? target.intent.getComponent().flattenToString() : ""); + intent.putExtra("app_label", resolveAppLabel(target.packageName, target.mUserId)); + + WindowProcessController wpc = target.app; + Slog.d(TAG, "startAuthPrompt: launching AuthenticateActivity" + + " target=" + target.packageName + + " targetToken=" + target.token + + " targetTask=" + (target.getTask() != null ? target.getTask().mTaskId : -1) + + " wpc=" + (wpc == null ? "null" : "attached") + + " reason=" + reason); + if (wpc == null) { + mAtms.getActivityStartController() + .obtainStarter(intent, reason) + .setCallingUid(0) + .setResultTo(target.token) + .setRequestCode(mRequestCode) + .setActivityInfo(mSandboxResolveInfo != null + ? mSandboxResolveInfo.activityInfo : null) + .execute(); + } else { + startActivityAsCaller(wpc.getThread(), target.packageName, intent, + "", target.token, target.resultWho, mRequestCode); + } + + abortAnimation(target); + return true; + } catch (Exception e) { + Slog.w(TAG, "startAuthPrompt: failed for " + target, e); + synchronized (mPendingUnlocks) { + mPendingUnlocks.remove(pendingKey); + } + return false; + } + } + + private String resolveAppLabel(String packageName, int userId) { + try { + PackageManager pm = mContext.getPackageManager(); + ApplicationInfo ai = pm.getApplicationInfoAsUser(packageName, 0, userId); + CharSequence label = pm.getApplicationLabel(ai); + return label != null ? label.toString() : packageName; + } catch (Exception e) { + return packageName; + } + } + + @Override + public boolean checkUnlockApp(ActivityRecord r, int resultCode, Intent data) { + if (r.requestCode != mRequestCode) { + return false; + } + if (data == null) { + return true; + } + try { + int userId = UserHandle.getUserId(data.getIntExtra(EXTRA_LOCKED_UID, 0)); + String packageName = data.getStringExtra(EXTRA_LOCKED_PACKAGE); + String pendingKey = sessionKey(userId, packageName); + + Slog.d(TAG, "checkUnlockApp: unlocking pkg=" + packageName + + " userId=" + userId + " resultCode=" + resultCode); + + synchronized (mPendingUnlocks) { + mPendingUnlocks.remove(pendingKey); + } + + if (resultCode == Activity.RESULT_OK && packageName != null) { + markSessionUnlocked(packageName, userId); + } else if (r.resultTo != null) { + Slog.d(TAG, "checkUnlockApp: finishing target " + r.resultTo + " on cancel"); + r.resultTo.finishIfPossible("applock-canceled", false); + } + return true; + } catch (Exception e) { + Slog.w(TAG, "checkUnlockApp: failed", e); + return false; + } + } + + @Override + public boolean isSandboxActivity(ComponentName component) { + return component != null + && SANDBOX_PACKAGE.equals(component.getPackageName()) + && SANDBOX_ACTIVITY.equals(component.getClassName()); + } + + @Override + public boolean isTopAppLocked(ActivityManager.RecentTaskInfo rti, int topUserId) { + rti.isTopAppLocked = false; + if (mCheckRecentTasks) { + ComponentName component = rti.baseIntent.getComponent(); + String packageName; + int uid = topUserId; + if (component != null) { + packageName = component.getPackageName(); + } else { + packageName = ""; + uid = 0; + } + + long identity = Binder.clearCallingIdentity(); + try { + int userId = UserHandle.getUserId(uid); + if (isSandboxActivity(component)) { + rti.isTopAppLocked = true; + } else { + if (mAppControlController.isAppLocked(packageName)) { + if (mLockBehavior == LOCK_BEHAVIOR_ON_LEAVE + && mUnlockedApps.contains(sessionKey(userId, packageName))) { + rti.isTopAppLocked = false; + } else { + rti.isTopAppLocked = true; + } + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + return rti.isTopAppLocked; + } + + @Override + public void getRecentTasksCheck(int callingUid, int userId) { + mCheckRecentTasks = callingUid != 1000 && mAtms.mWindowManager.isKeyguardSecure(userId); + } + + @Override + public void setKeyguardDoneLocked(boolean done) { + try { + if (done) { + mKeyguardDone = true; + mAtms.mWindowManager.getDefaultDisplayContentLocked() + .getDefaultTaskDisplayArea().forAllTasks(this::addVisibleTaskToUnlocked); + } else { + mKeyguardDone = false; + + if (mLockBehavior == LOCK_BEHAVIOR_TIMEOUT && mLastFocusedAppKey != null) { + scheduleTimeoutLock(mLastFocusedAppKey); + } + + if (mLockBehavior == LOCK_BEHAVIOR_ON_SCREEN_OFF) { + lockAllSessionsAndNotify(); + } + } + } catch (Exception e) { + Slog.w(TAG, "setKeyguardDoneLocked: failed", e); + mKeyguardDone = done; + } + } + + @Override + public void onAppFocusChanged(ActivityRecord newFocus, Task newTask) { + String newKey = (newFocus != null) ? sessionKey(newFocus) : null; + + if (mLastFocusedAppKey != null && !mLastFocusedAppKey.equals(newKey)) { + scheduleTimeoutLock(mLastFocusedAppKey); + if (mLockBehavior == LOCK_BEHAVIOR_ON_LEAVE + && mUnlockedApps.contains(mLastFocusedAppKey)) { + int colon = mLastFocusedAppKey.indexOf(':'); + if (colon > 0 && colon < mLastFocusedAppKey.length() - 1) { + try { + int oldUserId = Integer.parseInt( + mLastFocusedAppKey.substring(0, colon)); + String oldPkg = mLastFocusedAppKey.substring(colon + 1); + markSessionLocked(oldPkg, oldUserId); + } catch (NumberFormatException ignored) { } + } + } + } + + if (newKey != null) { + cancelTimeoutLock(newKey); + mUnlockTimestamps.put(newKey, SystemClock.elapsedRealtime()); + } + + mLastFocusedAppKey = newKey; + lockTopApp(newTask, "onAppFocusChanged"); + } + + @Override + public void onWindowingModeChanged(Task task, int prevWindowingMode) { + if (task == null || mLockBehavior != LOCK_BEHAVIOR_ON_LEAVE) { + return; + } + + int currMode = task.getWindowingMode(); + + if (!WindowConfiguration.isFloating(prevWindowingMode) + && WindowConfiguration.isFloating(currMode) && task.isVisible()) { + ActivityRecord r = task.topRunningActivityLocked(); + if (isAppLocked(r)) { + markSessionUnlocked(r.packageName, r.mUserId); + } + return; + } + + if (mUnlockedApps.size() > 0) { + if ((!WindowConfiguration.inMultiWindowMode(prevWindowingMode) + && !WindowConfiguration.isFloating(prevWindowingMode)) + || WindowConfiguration.inMultiWindowMode(currMode) + || WindowConfiguration.isFloating(currMode)) { + return; + } + + ActivityRecord r = task.topRunningActivityLocked(); + if (task.isVisible()) { + if (currMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + if (r != null) { + clearUnlockedApp(r); + } else { + clearUnlockedApp(); + } + } + } else { + if (r != null) { + markSessionLocked(r.packageName, r.mUserId); + } + ActivityRecord lastPaused = task.mLastPausedActivity; + if (lastPaused != null) { + markSessionLocked(lastPaused.packageName, lastPaused.mUserId); + } + ComponentName realActivity = task.realActivity; + if (realActivity != null) { + markSessionLocked(realActivity.getPackageName(), task.effectiveUid); + } + } + } + } + + @Override + public void clearUnlockedApp() { + boolean tracksUnlockSession = mLockBehavior == LOCK_BEHAVIOR_ON_LEAVE; + if (!tracksUnlockSession || mUnlockedApps.size() <= 0) { + return; + } + int size = mUnlockedApps.size(); + lockAllSessionsAndNotify(); + lockVisibleMultiWindowApps(mAtms.mWindowManager.getDefaultDisplayContentLocked()); + Slog.d(TAG, "clearUnlockedApp: size=" + size); + } + + @Override + public void clearUnlockedApp(ActivityRecord r) { + if (r == null || mLockBehavior != LOCK_BEHAVIOR_ON_LEAVE) { + return; + } + + if (r.occludesParent() || r.isActivityTypeHomeOrRecents()) { + if (r.isActivityTypeHomeOrRecents() && r.mTransitionController.isTransientLaunch(r)) { + return; + } + boolean wasUnlocked = mUnlockedApps.contains(sessionKey(r)); + clearUnlockedApp(); + if (wasUnlocked) { + markSessionUnlocked(r.packageName, r.mUserId); + } else { + markSessionLocked(r.packageName, r.mUserId); + } + if (WindowConfiguration.isFloating(r.getWindowingMode())) { + lockVisibleFullscreenApps(mAtms.mWindowManager.getDefaultDisplayContentLocked()); + } + } + } + + @Override + public void removeTask(Task task, String reason) { + if (task == null || !"remove-task".equals(reason) + || mLockBehavior != LOCK_BEHAVIOR_ON_LEAVE || mUnlockedApps.size() <= 0) { + return; + } + + if (WindowConfiguration.inMultiWindowMode(task.getWindowingMode()) + || WindowConfiguration.isFloating(task.getWindowingMode())) { + ActivityRecord r = task.topRunningActivityLocked(); + if (r != null) { + markSessionLocked(r.packageName, r.mUserId); + } + ActivityRecord lastPaused = task.mLastPausedActivity; + if (lastPaused != null) { + markSessionLocked(lastPaused.packageName, lastPaused.mUserId); + } + ComponentName realActivity = task.realActivity; + if (realActivity != null) { + markSessionLocked(realActivity.getPackageName(), task.effectiveUid); + } + } + } + + @Override + public void onAppDied(String packageName, int userId) { + if (SANDBOX_PACKAGE.equals(packageName)) { + synchronized (mPendingUnlocks) { + if (!mPendingUnlocks.isEmpty()) { + Slog.d(TAG, "onAppDied: clearing " + mPendingUnlocks.size() + " pending unlocks"); + mPendingUnlocks.clear(); + } + } + return; + } + if (mLockBehavior == LOCK_BEHAVIOR_ON_KILL) { + markSessionLocked(packageName, userId); + } + } + + private static String sessionKey(int userId, String packageName) { + return userId + ":" + packageName; + } + + private static String sessionKey(ActivityRecord r) { + return sessionKey(r.mUserId, r.packageName); + } + + private void markSessionUnlocked(String packageName, int userId) { + String key = sessionKey(userId, packageName); + if (mUnlockedApps.add(key)) { + mUnlockTimestamps.put(key, SystemClock.elapsedRealtime()); + Slog.d(TAG, "markSessionUnlocked: " + packageName + " userId=" + userId); + notifyAppUnlocked(packageName, userId); + } + } + + private void notifyAppUnlocked(String packageName, int userId) { + final int count = mAppSessionListeners.beginBroadcast(); + for (int i = 0; i < count; i++) { + try { + mAppSessionListeners.getBroadcastItem(i).onAppUnlocked(packageName, userId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify app session listener", e); + } + } + mAppSessionListeners.finishBroadcast(); + } + + private void markSessionLocked(String packageName, int userId) { + String key = sessionKey(userId, packageName); + if (mUnlockedApps.remove(key)) { + mUnlockTimestamps.remove(key); + cancelTimeoutLock(key); + Slog.d(TAG, "markSessionLocked: " + packageName + " userId=" + userId); + notifyAppLocked(packageName, userId); + } + } + + private void notifyAppLocked(String packageName, int userId) { + final int count = mAppSessionListeners.beginBroadcast(); + for (int i = 0; i < count; i++) { + try { + mAppSessionListeners.getBroadcastItem(i).onAppLocked(packageName, userId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify app session listener (locked)", e); + } + } + mAppSessionListeners.finishBroadcast(); + } + + private void lockAllSessionsAndNotify() { + if (mUnlockedApps.isEmpty()) return; + String[] keys = mUnlockedApps.toArray(new String[0]); + mUnlockedApps.clear(); + mUnlockTimestamps.clear(); + clearAllTimeouts(); + for (String key : keys) { + int colon = key.indexOf(':'); + if (colon <= 0 || colon >= key.length() - 1) continue; + try { + int userId = Integer.parseInt(key.substring(0, colon)); + String pkg = key.substring(colon + 1); + notifyAppLocked(pkg, userId); + } catch (NumberFormatException ignored) { } + } + } + + private void scheduleTimeoutLock(String key) { + if (mLockBehavior != LOCK_BEHAVIOR_TIMEOUT || !mUnlockedApps.contains(key)) { + return; + } + + cancelTimeoutLock(key); + + Runnable r = () -> { + synchronized (mAtms.mGlobalLock) { + ActivityRecord top = mAtms.mRootWindowContainer.getTopResumedActivity(); + String topKey = (top != null) ? sessionKey(top) : null; + + if (!key.equals(topKey)) { + int index = key.indexOf(":"); + if (index != -1) { + try { + int userId = Integer.parseInt(key.substring(0, index)); + String packageName = key.substring(index + 1); + markSessionLocked(packageName, userId); + } catch (NumberFormatException ignored) {} + } + } + mTimeoutRunnables.remove(key); + } + }; + + mTimeoutRunnables.put(key, r); + mAtms.mH.postDelayed(r, mLockTimeout * 1000L); + } + + private void cancelTimeoutLock(String key) { + Runnable r = mTimeoutRunnables.remove(key); + if (r != null) { + mAtms.mH.removeCallbacks(r); + } + } + + private void clearAllTimeouts() { + for (Runnable r : mTimeoutRunnables.values()) { + mAtms.mH.removeCallbacks(r); + } + mTimeoutRunnables.clear(); + } + + private void broadcastPackageChanged(String packageName) { + final long token = Binder.clearCallingIdentity(); + try { + int uid = getPackageUid(packageName); + int userId = uid >= 0 ? UserHandle.getUserId(uid) : mCurrentUserId; + + Intent intent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + intent.setData(Uri.fromParts("package", packageName, null)); + intent.putExtra(Intent.EXTRA_UID, uid); + intent.putExtra(Intent.EXTRA_USER_HANDLE, userId); + String[] components = { packageName }; + intent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, components); + + mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); + Slog.d(TAG, "broadcastPackageChanged: " + packageName); + } catch (Exception e) { + Slog.w(TAG, "Failed to broadcast package change for " + packageName, e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void notifyAppLockStateChanged(String packageName, boolean locked) { + final int count = mAppLockStateListeners.beginBroadcast(); + for (int i = 0; i < count; i++) { + try { + mAppLockStateListeners.getBroadcastItem(i).onAppLockStateChanged(packageName, locked); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify app lock state listener", e); + } + } + mAppLockStateListeners.finishBroadcast(); + } + + private void lockVisibleTask(Task task) { + if (task.isLeafTask() && task.shouldBeVisible(null) && isAppLocked(task.topRunningActivityLocked())) { + lockTopApp(task, "setKeyguardDone"); + } + } + + private void addVisibleTaskToUnlocked(Task task) { + if (task.isLeafTask() && task.isVisible()) { + ActivityRecord r = task.topRunningActivityLocked(); + if (isAppLocked(r)) { + lockVisibleTask(task); + Slog.d(TAG, "addVisibleTaskToUnlocked: should be locked pkg: " + r.packageName + " userId=" + r.mUserId); + } + } + } + + private void lockVisibleMultiWindowApps(DisplayContent dc) { + if (dc == null) { + dc = mAtms.mWindowManager.getDefaultDisplayContentLocked(); + } + dc.getDefaultTaskDisplayArea().forAllTasks(task -> { + if (task.isLeafTask() + && (WindowConfiguration.inMultiWindowMode(task.getWindowingMode()) + || WindowConfiguration.isFloating(task.getWindowingMode())) + && task.isVisible()) { + ActivityRecord r = task.topRunningActivityLocked(); + if (isAppLocked(r)) { + markSessionUnlocked(r.packageName, r.mUserId); + } + } + }); + } + + private void lockVisibleFullscreenApps(DisplayContent dc) { + if (dc == null) { + dc = mAtms.mWindowManager.getDefaultDisplayContentLocked(); + } + dc.getDefaultTaskDisplayArea().forAllTasks(task -> { + if (task.isLeafTask() && task.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN + && task.isVisible()) { + ActivityRecord r = task.topRunningActivityLocked(); + if (isAppLocked(r)) { + markSessionUnlocked(r.packageName, r.mUserId); + } + } + }); + } + + private void abortAnimation(ActivityRecord r) { + if (r == null) return; + try { + if (r.getOptions() != null && r.getOptions().getRemoteAnimationAdapter() != null) { + r.getOptions().getRemoteAnimationAdapter().getRunner().onAnimationCancelled(); + } + } catch (Exception e) { + Slog.w(TAG, "abortAnimation failed for " + r, e); + } + r.abortAndClearOptionsAnimation(); + } + + private int startActivityAsCaller(IApplicationThread caller, String callingPackage, + Intent intent, String resolvedType, IBinder resultTo, String resultWho, + int requestCode) { + return mAtms.getActivityStartController() + .obtainStarter(intent, "startActivityAsCaller") + .setCaller(caller) + .setCallingPackage(callingPackage) + .setResolvedType(resolvedType) + .setResultTo(resultTo) + .setResultWho(resultWho) + .setRequestCode(requestCode) + .execute(); + } + + private int getPackageUid(String packageName) { + try { + return mContext.getPackageManager() + .getApplicationInfo(packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + return -1; + } + } + + public boolean isAppLockerActivity(ComponentName componentName) { + return componentName != null && + SANDBOX_PACKAGE.equals(componentName.getPackageName()) && + SANDBOX_ACTIVITY.equals(componentName.getClassName()); + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 3b79e0650ee9..c3c45769060b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4150,6 +4150,10 @@ boolean setFocusedApp(ActivityRecord newFocus) { if (newTask != null) newTask.onAppFocusChanged(true); } + if (mDisplayId == DEFAULT_DISPLAY && newFocus != null) { + AxSandboxService.get().onAppFocusChanged(newFocus, newTask); + } + getInputMonitor().setFocusedAppLw(newFocus); return true; } diff --git a/services/core/java/com/android/server/wm/IAxSandboxService.java b/services/core/java/com/android/server/wm/IAxSandboxService.java new file mode 100644 index 000000000000..53cc107dc833 --- /dev/null +++ b/services/core/java/com/android/server/wm/IAxSandboxService.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Intent; + +/** + * @hide + */ +public interface IAxSandboxService { + + public static final IAxSandboxService DEFAULT = new IAxSandboxService() {}; + + default void systemReadyInternal() { + } + + default void setKeyguardDoneLocked(boolean done) { + } + + default void onAppFocusChanged(ActivityRecord newFocus, Task newTask) { + } + + default void onWindowingModeChanged(Task task, int prevWindowingMode) { + } + + default boolean isAppLocked(String packageName, int uid, ComponentName componentName) { + return false; + } + + default boolean isAppLocked(ActivityRecord r) { + return false; + } + + default void lockTopApp(Task task, String reason) { + } + + default boolean isSandboxActivity(ComponentName componentName) { + return false; + } + + default boolean checkLockApp(ActivityRecord prev, ActivityRecord next) { + return false; + } + + default boolean checkUnlockApp(ActivityRecord r, int resultCode, Intent resultData) { + return false; + } + + default boolean isTopAppLocked(ActivityManager.RecentTaskInfo rti, int topUserId) { + return false; + } + + default void getRecentTasksCheck(int callingUid, int userId) { + } + + default void clearUnlockedApp() { + } + + default void clearUnlockedApp(ActivityRecord r) { + } + + default void removeTask(Task task, String reason) { + } + + default void onAppDied(String packageName, int userId) { + } +} diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index bc07397ba65e..4c110747e40d 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -273,6 +273,10 @@ void setKeyguardShown(int displayId, boolean keyguardShowing, boolean aodShowing scheduleGoingAwayTimeout(displayId); } + if (displayId == DEFAULT_DISPLAY && keyguardChanged) { + AxSandboxService.get().setKeyguardDoneLocked(!keyguardShowing); + } + // Update the sleep token first such that ensureActivitiesVisible has correct sleep token // state when evaluating visibilities. updateKeyguardSleepToken(); diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 2e79cb56d6da..782c2786d7c3 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -2058,6 +2058,7 @@ ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras if (!getTasksAllowed) { Task.trimIneffectiveInfo(tr, rti); } + AxSandboxService.get().isTopAppLocked(rti, tr.effectiveUid); return rti; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index b5698711ed6c..6098896cf7f0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2215,6 +2215,7 @@ public void onConfigurationChanged(Configuration newParentConfig) { if (prevWindowingMode != getWindowingMode()) { taskDisplayArea.onRootTaskWindowingModeChanged(this); + AxSandboxService.get().onWindowingModeChanged(this, prevWindowingMode); } if (!isOrganized() && !getRequestedOverrideBounds().isEmpty() && mDisplayContent != null) { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 6ee790889d59..c1154751d116 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -705,6 +705,10 @@ void setResumedActivity(ActivityRecord r, String reason) { getTask().touchActiveTime(); } + if (AxSandboxService.get().checkLockApp(mResumedActivity, r)) { + return; + } + final ActivityRecord prevR = mResumedActivity; mResumedActivity = r; final ActivityRecord topResumed = mTaskSupervisor.updateTopResumedActivityIfNeeded(reason); @@ -1866,6 +1870,10 @@ final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options, mTaskSupervisor.startSpecificActivity(next, true, true); } + if (AxSandboxService.get().checkLockApp(prev, next)) { + return true; + } + return true; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index ff3cd9989bb0..9569cdd7388b 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1637,6 +1637,9 @@ void finishTransition(@NonNull ActionChain chain) { mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId); dc.getInputMonitor().setActiveRecents(null /* task */, null /* layer */); dc.getInputMonitor().updateInputWindowsLw(false /* force */); + if (mRecentsDisplayId == DEFAULT_DISPLAY) { + AxSandboxService.get().clearUnlockedApp(dc.mFocusedApp); + } } if (mTransientLaunches != null) { for (int i = mTransientLaunches.size() - 1; i >= 0; --i) { diff --git a/services/core/java/com/android/server/wm/sandbox/AppControlController.java b/services/core/java/com/android/server/wm/sandbox/AppControlController.java new file mode 100644 index 000000000000..b1bc220a9182 --- /dev/null +++ b/services/core/java/com/android/server/wm/sandbox/AppControlController.java @@ -0,0 +1,557 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm.sandbox; + +import android.app.AxSandboxManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class AppControlController { + private static final String TAG = "AxSandbox.AppControl"; + + public static final String SETTING_SANDBOX_CONFIG = AxSandboxManager.SETTING_SANDBOX_CONFIG; + + private static final String KEY_HIDDEN_PKGS = "hidden_pkgs"; + private static final String KEY_LOCKED_PKGS = "locked_pkgs"; + private static final String KEY_SANDBOXED_PKGS = "sandboxed_pkgs"; + private static final String KEY_HIDEDEVOPTS_PKGS = "hidedevopts_pkgs"; + private static final String KEY_GID_RESTRICTIONS = "gid_restrictions"; + private static final String KEY_SPOOF_SETTINGS_MAP = "spoof_settings_map"; + private static final String KEY_DATA_ISOLATION = "data_isolation_pkgs"; + + private final Context mContext; + private final ContentResolver mContentResolver; + private final LauncherApps mLauncherApps; + private final Set mBlacklistedPackages; + private final Handler mHandler; + + private final Set mLockedPackages = new HashSet<>(); + private final Set mHiddenPackages = new HashSet<>(); + private final Set mSandboxedPackages = new HashSet<>(); + private final Set mHideDevOptsPackages = new HashSet<>(); + private final Map mGidRestrictions = new HashMap<>(); + private final Map> mSpoofSettingsMap = new HashMap<>(); + private final Set mDataIsolationPackages = new HashSet<>(); + + private ContentObserver mConfigObserver; + + public interface OnUpdateListener { + void onSandboxUpdated(); + } + + private OnUpdateListener mUpdateListener; + + public AppControlController(Context context, Set blacklistedPackages) { + mContext = context; + mContentResolver = context.getContentResolver(); + mLauncherApps = context.getSystemService(LauncherApps.class); + mBlacklistedPackages = blacklistedPackages; + mHandler = new Handler(Looper.getMainLooper()); + } + + public void setUpdateListener(OnUpdateListener listener) { + mUpdateListener = listener; + } + + public void init() { + registerSettingsObserver(); + loadConfigFromSettings(); + } + + private void registerSettingsObserver() { + mConfigObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + loadConfigFromSettings(); + broadcastPackageChanges(); + notifyUpdate(); + } + }; + + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_SANDBOX_CONFIG), + false, mConfigObserver, UserHandle.USER_ALL); + + Slog.d(TAG, "Registered Settings observer for sandbox_config"); + } + + private void loadConfigFromSettings() { + String jsonStr = Settings.Secure.getString(mContentResolver, SETTING_SANDBOX_CONFIG); + + synchronized (this) { + mLockedPackages.clear(); + mHiddenPackages.clear(); + mSandboxedPackages.clear(); + mHideDevOptsPackages.clear(); + mGidRestrictions.clear(); + mSpoofSettingsMap.clear(); + mDataIsolationPackages.clear(); + + if (!TextUtils.isEmpty(jsonStr)) { + try { + JSONObject config = new JSONObject(jsonStr); + + loadPackageSet(config, KEY_LOCKED_PKGS, mLockedPackages); + loadPackageSet(config, KEY_HIDDEN_PKGS, mHiddenPackages); + loadPackageSet(config, KEY_SANDBOXED_PKGS, mSandboxedPackages); + loadPackageSet(config, KEY_HIDEDEVOPTS_PKGS, mHideDevOptsPackages); + loadSpoofSettingsMap(config); + loadPackageSet(config, KEY_DATA_ISOLATION, mDataIsolationPackages); + loadGidRestrictions(config); + + } catch (JSONException e) { + Slog.e(TAG, "Failed to parse sandbox_config JSON", e); + } + } + } + + Slog.i(TAG, "Loaded config: locked=" + mLockedPackages.size() + + " hidden=" + mHiddenPackages.size() + + " sandboxed=" + mSandboxedPackages.size() + + " hideDevOpts=" + mHideDevOptsPackages.size()); + } + + private void loadPackageSet(JSONObject config, String key, Set target) { + JSONArray arr = config.optJSONArray(key); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + String pkg = arr.optString(i); + if (!TextUtils.isEmpty(pkg) && !mBlacklistedPackages.contains(pkg)) { + target.add(pkg); + } + } + } + } + + private void saveConfigToSettings() { + try { + JSONObject config = new JSONObject(); + + synchronized (this) { + config.put(KEY_LOCKED_PKGS, new JSONArray(mLockedPackages)); + config.put(KEY_HIDDEN_PKGS, new JSONArray(mHiddenPackages)); + config.put(KEY_SANDBOXED_PKGS, new JSONArray(mSandboxedPackages)); + config.put(KEY_HIDEDEVOPTS_PKGS, new JSONArray(mHideDevOptsPackages)); + saveSpoofSettingsMap(config); + config.put(KEY_DATA_ISOLATION, new JSONArray(mDataIsolationPackages)); + saveGidRestrictions(config); + } + + Settings.Secure.putString(mContentResolver, SETTING_SANDBOX_CONFIG, config.toString()); + + } catch (JSONException e) { + Slog.e(TAG, "Failed to save sandbox_config JSON", e); + } + } + + private void broadcastPackageChanges() { + Set allPackages = new HashSet<>(); + synchronized (this) { + allPackages.addAll(mHiddenPackages); + } + + for (String packageName : allPackages) { + int uid = getPackageUid(packageName); + if (uid >= 0) { + broadcastPackageChange(packageName, uid); + } + } + } + + private void broadcastPackageChange(String packageName, int uid) { + try { + Intent intent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + intent.setData(Uri.fromParts("package", packageName, null)); + intent.putExtra(Intent.EXTRA_UID, uid); + intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(uid)); + intent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[]{packageName}); + intent.putExtra(Intent.EXTRA_DONT_KILL_APP, true); + mContext.sendBroadcastAsUser(intent, UserHandle.of(UserHandle.getUserId(uid))); + } catch (Exception e) { + Slog.w(TAG, "Failed to broadcast package change for " + packageName, e); + } + } + + private void notifyUpdate() { + if (mUpdateListener != null) { + mUpdateListener.onSandboxUpdated(); + } + } + + public boolean isAppLocked(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + if (mBlacklistedPackages.contains(packageName)) return false; + synchronized (this) { + return mLockedPackages.contains(packageName); + } + } + + public boolean isPackageHidden(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + synchronized (this) { + final boolean hidden = mHiddenPackages.contains(packageName); + return hidden; + } + } + + public boolean isPackageSandboxed(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + synchronized (this) { + return mSandboxedPackages.contains(packageName); + } + } + + public boolean isDevOptionsHidden(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + synchronized (this) { + return mHideDevOptsPackages.contains(packageName); + } + } + + public void addLockedApp(String packageName) { + if (TextUtils.isEmpty(packageName)) return; + if (!isPackageLockable(packageName)) { + Slog.w(TAG, "Cannot lock package - not lockable: " + packageName); + return; + } + int uid = getPackageUid(packageName); + synchronized (this) { + if (mLockedPackages.add(packageName)) { + saveConfigToSettings(); + if (uid >= 0) { + broadcastPackageChange(packageName, uid); + } + } + } + Slog.d(TAG, "addLockedApp: " + packageName); + } + + public void removeLockedApp(String packageName) { + if (TextUtils.isEmpty(packageName)) return; + int uid = getPackageUid(packageName); + synchronized (this) { + if (mLockedPackages.remove(packageName)) { + saveConfigToSettings(); + if (uid >= 0) { + broadcastPackageChange(packageName, uid); + } + } + } + Slog.d(TAG, "removeLockedApp: " + packageName); + } + + public void setPackageHidden(String packageName, boolean hidden) { + if (TextUtils.isEmpty(packageName)) return; + if (hidden && !isPackageLockable(packageName)) { + Slog.w(TAG, "Cannot hide package - not lockable: " + packageName); + return; + } + int uid = getPackageUid(packageName); + synchronized (this) { + boolean changed = hidden ? mHiddenPackages.add(packageName) + : mHiddenPackages.remove(packageName); + if (changed) { + saveConfigToSettings(); + if (uid >= 0) { + broadcastPackageChange(packageName, uid); + } + } + } + Slog.d(TAG, "setPackageHidden: " + packageName + " hidden=" + hidden); + } + + public void setPackageSandboxed(String packageName, boolean sandboxed) { + if (TextUtils.isEmpty(packageName)) return; + synchronized (this) { + boolean changed = sandboxed ? mSandboxedPackages.add(packageName) + : mSandboxedPackages.remove(packageName); + if (changed) { + saveConfigToSettings(); + } + } + Slog.d(TAG, "setPackageSandboxed: " + packageName + " sandboxed=" + sandboxed); + } + + public void setDevOptionsHidden(String packageName, boolean hidden) { + if (TextUtils.isEmpty(packageName)) return; + synchronized (this) { + boolean changed = hidden ? mHideDevOptsPackages.add(packageName) + : mHideDevOptsPackages.remove(packageName); + if (changed) { + saveConfigToSettings(); + } + } + Slog.d(TAG, "setDevOptionsHidden: " + packageName + " hidden=" + hidden); + } + + public List getLockedPackages() { + synchronized (this) { + return new ArrayList<>(mLockedPackages); + } + } + + public List getHiddenPackages() { + synchronized (this) { + return new ArrayList<>(mHiddenPackages); + } + } + + public List getSandboxedPackages() { + synchronized (this) { + return new ArrayList<>(mSandboxedPackages); + } + } + + public List getDevOptionsHiddenPackages() { + synchronized (this) { + return new ArrayList<>(mHideDevOptsPackages); + } + } + + public boolean isPackageLockable(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + if (mBlacklistedPackages.contains(packageName)) return false; + + if (mLauncherApps == null) return false; + + try { + List activities = mLauncherApps.getActivityList( + packageName, UserHandle.of(UserHandle.USER_SYSTEM)); + return activities != null && !activities.isEmpty(); + } catch (Exception e) { + Slog.w(TAG, "Failed to check if package is lockable: " + packageName, e); + return false; + } + } + + public List getLockablePackages() { + List result = new ArrayList<>(); + + if (mLauncherApps != null) { + try { + List activities = mLauncherApps.getActivityList( + null, UserHandle.of(UserHandle.USER_SYSTEM)); + + Set seen = new HashSet<>(); + for (LauncherActivityInfo info : activities) { + String pkgName = info.getApplicationInfo().packageName; + if (!mBlacklistedPackages.contains(pkgName) && !seen.contains(pkgName)) { + result.add(pkgName); + seen.add(pkgName); + } + } + } catch (Exception e) { + Slog.e(TAG, "Failed to get lockable packages from LauncherApps", e); + } + } + + if (result.isEmpty()) { + Slog.w(TAG, "LauncherApps returned empty, using PackageManager fallback"); + try { + PackageManager pm = mContext.getPackageManager(); + List apps = pm.getInstalledApplications(0); + for (ApplicationInfo appInfo : apps) { + if (mBlacklistedPackages.contains(appInfo.packageName)) continue; + if (isSystemUid(appInfo.uid)) continue; + if (pm.getLaunchIntentForPackage(appInfo.packageName) != null) { + result.add(appInfo.packageName); + } + } + } catch (Exception e) { + Slog.e(TAG, "Failed to get lockable packages from PackageManager", e); + } + } + + return result; + } + + private int getPackageUid(String packageName) { + try { + return mContext.getPackageManager() + .getApplicationInfo(packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + return -1; + } + } + + private static boolean isSystemUid(int uid) { + int appId = UserHandle.getAppId(uid); + return appId == android.os.Process.ROOT_UID || appId == android.os.Process.SYSTEM_UID; + } + + private void loadGidRestrictions(JSONObject config) { + JSONObject gidObj = config.optJSONObject(KEY_GID_RESTRICTIONS); + if (gidObj == null) return; + java.util.Iterator keys = gidObj.keys(); + while (keys.hasNext()) { + String pkg = keys.next(); + JSONArray arr = gidObj.optJSONArray(pkg); + if (arr != null && arr.length() > 0) { + int[] gids = new int[arr.length()]; + for (int i = 0; i < arr.length(); i++) { + gids[i] = arr.optInt(i); + } + mGidRestrictions.put(pkg, gids); + } + } + } + + private void saveGidRestrictions(JSONObject config) throws JSONException { + JSONObject gidObj = new JSONObject(); + for (Map.Entry entry : mGidRestrictions.entrySet()) { + JSONArray arr = new JSONArray(); + for (int gid : entry.getValue()) { + arr.put(gid); + } + gidObj.put(entry.getKey(), arr); + } + config.put(KEY_GID_RESTRICTIONS, gidObj); + } + + public void setRestrictedGids(String packageName, int[] gids) { + if (TextUtils.isEmpty(packageName)) return; + synchronized (this) { + if (gids == null || gids.length == 0) { + mGidRestrictions.remove(packageName); + } else { + mGidRestrictions.put(packageName, gids); + } + saveConfigToSettings(); + } + } + + public int[] getRestrictedGids(String packageName) { + if (TextUtils.isEmpty(packageName)) return null; + synchronized (this) { + return mGidRestrictions.get(packageName); + } + } + + public boolean isSettingsSpoofEnabled(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + synchronized (this) { + return mSpoofSettingsMap.containsKey(packageName); + } + } + + public boolean isSpoofSettingEnabled(String packageName, String settingKey) { + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(settingKey)) return false; + synchronized (this) { + Set settings = mSpoofSettingsMap.get(packageName); + return settings != null && settings.contains(settingKey); + } + } + + public void setSpoofSettingEnabled(String packageName, String settingKey, boolean enabled) { + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(settingKey)) return; + synchronized (this) { + Set settings = mSpoofSettingsMap.get(packageName); + boolean changed; + if (enabled) { + if (settings == null) { + settings = new HashSet<>(); + mSpoofSettingsMap.put(packageName, settings); + } + changed = settings.add(settingKey); + } else { + if (settings == null) return; + changed = settings.remove(settingKey); + if (settings.isEmpty()) { + mSpoofSettingsMap.remove(packageName); + } + } + if (changed) saveConfigToSettings(); + } + } + + public List getEnabledSpoofSettings(String packageName) { + if (TextUtils.isEmpty(packageName)) return java.util.Collections.emptyList(); + synchronized (this) { + Set settings = mSpoofSettingsMap.get(packageName); + if (settings == null || settings.isEmpty()) return java.util.Collections.emptyList(); + return new java.util.ArrayList<>(settings); + } + } + + private void loadSpoofSettingsMap(JSONObject config) { + JSONObject mapObj = config.optJSONObject(KEY_SPOOF_SETTINGS_MAP); + if (mapObj == null) return; + java.util.Iterator keys = mapObj.keys(); + while (keys.hasNext()) { + String pkg = keys.next(); + JSONArray arr = mapObj.optJSONArray(pkg); + if (arr != null && arr.length() > 0) { + Set settings = new HashSet<>(); + for (int i = 0; i < arr.length(); i++) { + String s = arr.optString(i); + if (!TextUtils.isEmpty(s)) settings.add(s); + } + if (!settings.isEmpty()) { + mSpoofSettingsMap.put(pkg, settings); + } + } + } + } + + private void saveSpoofSettingsMap(JSONObject config) throws JSONException { + JSONObject mapObj = new JSONObject(); + for (Map.Entry> entry : mSpoofSettingsMap.entrySet()) { + mapObj.put(entry.getKey(), new JSONArray(entry.getValue())); + } + config.put(KEY_SPOOF_SETTINGS_MAP, mapObj); + } + + public boolean isDataIsolationEnabled(String packageName) { + if (TextUtils.isEmpty(packageName)) return false; + synchronized (this) { + return mDataIsolationPackages.contains(packageName); + } + } + + public void setDataIsolationEnabled(String packageName, boolean enabled) { + if (TextUtils.isEmpty(packageName)) return; + synchronized (this) { + boolean changed = enabled ? mDataIsolationPackages.add(packageName) + : mDataIsolationPackages.remove(packageName); + if (changed) saveConfigToSettings(); + } + } +} diff --git a/services/core/java/com/android/server/wm/sandbox/HiddenNotificationController.java b/services/core/java/com/android/server/wm/sandbox/HiddenNotificationController.java new file mode 100644 index 000000000000..07f69769459a --- /dev/null +++ b/services/core/java/com/android/server/wm/sandbox/HiddenNotificationController.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm.sandbox; + +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.app.HiddenNotificationInfo; +import com.android.internal.app.IHiddenNotificationListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class HiddenNotificationController { + private static final String TAG = "AxSandbox.Notif"; + + private final RemoteCallbackList mListeners = new RemoteCallbackList<>(); + private final Map mHiddenNotifications = new HashMap<>(); + + private Function mIsPackageHidden; + + public HiddenNotificationController() { + } + + public void setPackageHiddenChecker(Function checker) { + mIsPackageHidden = checker; + } + + public void registerListener(IHiddenNotificationListener listener) { + if (listener != null) { + mListeners.register(listener); + } + } + + public void unregisterListener(IHiddenNotificationListener listener) { + if (listener != null) { + mListeners.unregister(listener); + } + } + + public List getHiddenNotifications() { + synchronized (mHiddenNotifications) { + return new ArrayList<>(mHiddenNotifications.values()); + } + } + + public void onHiddenNotificationPosted(HiddenNotificationInfo info) { + if (info == null) return; + + if (mIsPackageHidden != null && !mIsPackageHidden.apply(info.packageName)) { + Slog.w(TAG, "Rejecting notification from non-hidden package: " + info.packageName); + return; + } + + synchronized (mHiddenNotifications) { + mHiddenNotifications.put(info.key, info); + } + + int count = mListeners.beginBroadcast(); + for (int i = 0; i < count; i++) { + try { + mListeners.getBroadcastItem(i).onHiddenNotificationPosted(info); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify hidden notification listener", e); + } + } + mListeners.finishBroadcast(); + } + + public void onHiddenNotificationRemoved(String key) { + if (key == null) return; + + synchronized (mHiddenNotifications) { + mHiddenNotifications.remove(key); + } + + int count = mListeners.beginBroadcast(); + for (int i = 0; i < count; i++) { + try { + mListeners.getBroadcastItem(i).onHiddenNotificationRemoved(key); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify hidden notification listener", e); + } + } + mListeners.finishBroadcast(); + } + + public void clearNotificationsForPackage(String packageName) { + if (packageName == null) return; + + List keysToRemove = new ArrayList<>(); + synchronized (mHiddenNotifications) { + for (Map.Entry entry : mHiddenNotifications.entrySet()) { + if (packageName.equals(entry.getValue().packageName)) { + keysToRemove.add(entry.getKey()); + } + } + } + + for (String key : keysToRemove) { + onHiddenNotificationRemoved(key); + } + } +} diff --git a/services/core/java/com/android/server/wm/sandbox/SettingsSpoofController.java b/services/core/java/com/android/server/wm/sandbox/SettingsSpoofController.java new file mode 100644 index 000000000000..9adfc5af18be --- /dev/null +++ b/services/core/java/com/android/server/wm/sandbox/SettingsSpoofController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm.sandbox; + +import java.util.HashMap; +import java.util.Map; + +public class SettingsSpoofController { + + private static final Map SPOOFED_SETTINGS = new HashMap<>(); + + static { + SPOOFED_SETTINGS.put("adb_enabled", "0"); + SPOOFED_SETTINGS.put("development_settings_enabled", "0"); + SPOOFED_SETTINGS.put("adb_wifi_enabled", "0"); + SPOOFED_SETTINGS.put("package_verifier_user_consent", "0"); + SPOOFED_SETTINGS.put("verify_apps_over_usb", "0"); + SPOOFED_SETTINGS.put("accessibility_enabled", "0"); + SPOOFED_SETTINGS.put("enabled_accessibility_services", ""); + SPOOFED_SETTINGS.put("accessibility_display_inversion_enabled", "0"); + } + + public static String getSpoofedValue(String settingName) { + if (settingName == null) return null; + return SPOOFED_SETTINGS.get(settingName); + } +} From cff062b76dc1eec1a27f807fff227ac19b451570 Mon Sep 17 00:00:00 2001 From: Saikrishna1504 Date: Sat, 2 May 2026 18:34:38 +0000 Subject: [PATCH 1150/1315] sandbox: Adding secure file vault support Change-Id: I77d30b18613702d340e1987316dd4097837a282c Signed-off-by: Saikrishna1504 Signed-off-by: Pranav Vashi Signed-off-by: Mrick343 --- core/java/android/app/AxSandboxManager.java | 10 ++++++++++ .../android/internal/app/IAxSandboxManager.aidl | 1 + .../com/android/server/wm/AxSandboxService.java | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/core/java/android/app/AxSandboxManager.java b/core/java/android/app/AxSandboxManager.java index bef8bc9d04cd..5c3e1298cacf 100644 --- a/core/java/android/app/AxSandboxManager.java +++ b/core/java/android/app/AxSandboxManager.java @@ -459,4 +459,14 @@ public String getSpoofedSetting(@NonNull String callingPackage, @NonNull String throw e.rethrowFromSystemServer(); } } + + /** @hide */ + @Nullable + public String getFileVaultPath() { + try { + return mService.getFileVaultPath(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/com/android/internal/app/IAxSandboxManager.aidl b/core/java/com/android/internal/app/IAxSandboxManager.aidl index 6e4104e505fa..ae75ec695c23 100644 --- a/core/java/com/android/internal/app/IAxSandboxManager.aidl +++ b/core/java/com/android/internal/app/IAxSandboxManager.aidl @@ -52,4 +52,5 @@ interface IAxSandboxManager { void setSandboxDataIsolationEnabled(String packageName, boolean enabled); String getSpoofedSetting(String callingPackage, String settingName); + String getFileVaultPath(); } diff --git a/services/core/java/com/android/server/wm/AxSandboxService.java b/services/core/java/com/android/server/wm/AxSandboxService.java index 0ec3e843bb19..a44368b6addc 100644 --- a/services/core/java/com/android/server/wm/AxSandboxService.java +++ b/services/core/java/com/android/server/wm/AxSandboxService.java @@ -14,6 +14,10 @@ * limitations under the License. */ package com.android.server.wm; +import android.os.Environment; +import android.os.UserHandle; +import android.system.Os; +import java.io.File; import static android.app.AxSandboxManager.AppLockState.LOCKED; import static android.app.AxSandboxManager.AppLockState.NONE; @@ -1145,4 +1149,17 @@ public boolean isAppLockerActivity(ComponentName componentName) { SANDBOX_PACKAGE.equals(componentName.getPackageName()) && SANDBOX_ACTIVITY.equals(componentName.getClassName()); } + @Override + public String getFileVaultPath() { + File vaultDir = new File(Environment.getDataSystemCeDirectory(UserHandle.getCallingUserId()), "sandbox/vault"); + if (!vaultDir.exists()) { + vaultDir.mkdirs(); + try { + Os.chmod(vaultDir.getPath(), 0700); + } catch (Exception e) { + Slog.e(TAG, "Failed to set vault permissions", e); + } + } + return vaultDir.getAbsolutePath(); + } } From 650ddff2794e1804bae127a4019d8e610165100a Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:09:36 +0800 Subject: [PATCH 1151/1315] services: optimizing sandbox call sites Change-Id: I6f9de3eb0f1460862fb2c0b276a95ca9587a83a9 Signed-off-by: Mrick343 --- .../android/server/wm/AxSandboxService.java | 19 ++++++++++++++++--- .../wm/sandbox/AppControlController.java | 6 ++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/wm/AxSandboxService.java b/services/core/java/com/android/server/wm/AxSandboxService.java index a44368b6addc..1965946e6ec3 100644 --- a/services/core/java/com/android/server/wm/AxSandboxService.java +++ b/services/core/java/com/android/server/wm/AxSandboxService.java @@ -493,15 +493,18 @@ public List getEnabledSpoofSettings(String packageName) { @Override public String getSpoofedSetting(String callingPackage, String settingName) { if (mAppControlController == null) return null; + final String spoofedValue = SettingsSpoofController.getSpoofedValue(settingName); + if (spoofedValue == null) return null; if (!mAppControlController.isSpoofSettingEnabled(callingPackage, settingName)) { return null; } - return SettingsSpoofController.getSpoofedValue(settingName); + return spoofedValue; } @Override public boolean isAppLocked(ActivityRecord r) { - if (r == null || r.isNoDisplay() || r.isActivityTypeHomeOrRecents()) { + if (r == null || !hasLockedPackages() || r.isNoDisplay() + || r.isActivityTypeHomeOrRecents()) { return false; } @@ -530,6 +533,7 @@ public boolean isAppLocked(ActivityRecord r) { @Override public boolean isAppLocked(String packageName, int uid, ComponentName component) { + if (mAppControlController == null) return false; if (BLACKLISTED_PACKAGES.contains(packageName)) return false; int userId = UserHandle.getUserId(uid); @@ -560,7 +564,7 @@ public boolean isAppLocked(String packageName, int uid, ComponentName component) @Override public void lockTopApp(Task task, String reason) { - if (task == null) return; + if (task == null || !hasLockedPackages()) return; ActivityRecord r = task.topRunningActivityLocked(); if (!isAppLocked(r)) return; @@ -763,6 +767,11 @@ public void setKeyguardDoneLocked(boolean done) { @Override public void onAppFocusChanged(ActivityRecord newFocus, Task newTask) { + if (!hasLockedPackages()) { + mLastFocusedAppKey = null; + return; + } + String newKey = (newFocus != null) ? sessionKey(newFocus) : null; if (mLastFocusedAppKey != null && !mLastFocusedAppKey.equals(newKey)) { @@ -923,6 +932,10 @@ private static String sessionKey(ActivityRecord r) { return sessionKey(r.mUserId, r.packageName); } + private boolean hasLockedPackages() { + return mAppControlController != null && mAppControlController.hasLockedPackages(); + } + private void markSessionUnlocked(String packageName, int userId) { String key = sessionKey(userId, packageName); if (mUnlockedApps.add(key)) { diff --git a/services/core/java/com/android/server/wm/sandbox/AppControlController.java b/services/core/java/com/android/server/wm/sandbox/AppControlController.java index b1bc220a9182..55e6b6f1c5d6 100644 --- a/services/core/java/com/android/server/wm/sandbox/AppControlController.java +++ b/services/core/java/com/android/server/wm/sandbox/AppControlController.java @@ -224,6 +224,12 @@ public boolean isAppLocked(String packageName) { } } + public boolean hasLockedPackages() { + synchronized (this) { + return !mLockedPackages.isEmpty(); + } + } + public boolean isPackageHidden(String packageName) { if (TextUtils.isEmpty(packageName)) return false; synchronized (this) { From 8cf310316a83772396ecc3946ec66d167025de6e Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 12:25:01 +0800 Subject: [PATCH 1152/1315] SystemUI: fix platform hooks regressions Change-Id: Iaf3d512bfcb5f4a9010b0b65f49597aba2266b76 Signed-off-by: Mrick343 --- .../systemui/ax/AxPlatformObservers.kt | 66 +++++++++++++------ .../systemui/ax/AxPlatformStateManager.kt | 30 ++++++++- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt index d323b044e2c1..43640e0e9902 100644 --- a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformObservers.kt @@ -70,6 +70,8 @@ import com.android.systemui.statusbar.policy.ZenModeController import com.android.wifitrackerlib.WifiEntry import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -120,6 +122,27 @@ class AxPlatformObservers @Inject constructor( private var lastMediaTrack: String? = null private var lastMediaArtist: String? = null private var lastMediaPackage: String? = null + private var lastMobileDataEnabled: Boolean? = null + + private val wifiStateFlow = MutableSharedFlow( + replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + private val mobileStateFlow = MutableSharedFlow( + replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + init { + scope.launch(bgDispatcher) { + wifiStateFlow.collect { bundle -> + stateManager.broadcastState(AxPlatformClient.FEATURE_WIFI, bundle) + } + } + scope.launch(bgDispatcher) { + mobileStateFlow.collect { bundle -> + stateManager.broadcastState(AxPlatformClient.FEATURE_MOBILE_DATA, bundle) + } + } + } fun registerAll() { registerControllerCallbacks() @@ -324,7 +347,7 @@ class AxPlatformObservers @Inject constructor( override fun setWifiIndicators(wifiIndicators: WifiIndicators) { val connected = wifiIndicators.statusIcon?.visible == true val enabled = wifiIndicators.enabled - stateManager.broadcastState(AxPlatformClient.FEATURE_WIFI, Bundle().apply { + wifiStateFlow.tryEmit(Bundle().apply { putBoolean("enabled", enabled) putBoolean("active", enabled) putBoolean("connected", connected) @@ -335,25 +358,28 @@ class AxPlatformObservers @Inject constructor( } override fun setMobileDataIndicators(mobileDataIndicators: MobileDataIndicators) { - val isEnabled = networkController.mobileDataController?.isMobileDataEnabled == true - stateManager.broadcastState( - AxPlatformClient.FEATURE_MOBILE_DATA, - Bundle().apply { - putBoolean("enabled", isEnabled) - putBoolean("active", isEnabled) - putString( - "type", - mobileDataIndicators.typeContentDescription?.toString() ?: "" - ) - putString("description", mobileDataIndicators.qsDescription?.toString() ?: "") - putBoolean("roaming", mobileDataIndicators.roaming) - putBoolean("isDefault", mobileDataIndicators.isDefault) - putInt("subId", mobileDataIndicators.subId) - putBoolean("activityIn", mobileDataIndicators.activityIn) - putBoolean("activityOut", mobileDataIndicators.activityOut) - putInt("level", mobileDataIndicators.level) - } - ) + val isEnabled = lastMobileDataEnabled ?: ( + networkController.mobileDataController?.isMobileDataEnabled == true + ).also { lastMobileDataEnabled = it } + mobileStateFlow.tryEmit(Bundle().apply { + putBoolean("enabled", isEnabled) + putBoolean("active", isEnabled) + putString( + "type", + mobileDataIndicators.typeContentDescription?.toString() ?: "" + ) + putString("description", mobileDataIndicators.qsDescription?.toString() ?: "") + putBoolean("roaming", mobileDataIndicators.roaming) + putBoolean("isDefault", mobileDataIndicators.isDefault) + putInt("subId", mobileDataIndicators.subId) + putBoolean("activityIn", mobileDataIndicators.activityIn) + putBoolean("activityOut", mobileDataIndicators.activityOut) + putInt("level", mobileDataIndicators.level) + }) + } + + override fun setMobileDataEnabled(enabled: Boolean) { + lastMobileDataEnabled = enabled } override fun setNoSims(show: Boolean, simDetected: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt index edd1d39f9520..1ac9e9f1444b 100644 --- a/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/ax/AxPlatformStateManager.kt @@ -74,13 +74,18 @@ class AxPlatformStateManager @Inject constructor( } fun broadcastState(key: String, state: Bundle) { - enrichBundle(key, state) - stateCache[key] = state + val normalizedState = Bundle(state) + enrichBundle(key, normalizedState) + val oldState = stateCache[key] + if (oldState != null && bundlesEqual(oldState, normalizedState)) { + return + } + stateCache[key] = normalizedState synchronized(callbacks) { val count = callbacks.beginBroadcast() for (i in 0 until count) { try { - callbacks.getBroadcastItem(i).onStateChanged(key, state) + callbacks.getBroadcastItem(i).onStateChanged(key, normalizedState) } catch (e: RemoteException) { Log.w(TAG, "Callback failed for key=$key", e) } @@ -117,6 +122,25 @@ class AxPlatformStateManager @Inject constructor( } } + private fun bundlesEqual(first: Bundle, second: Bundle): Boolean { + if (first.size() != second.size()) return false + for (key in first.keySet()) { + if (!second.containsKey(key)) return false + if (!bundleValueEquals(first.get(key), second.get(key))) return false + } + return true + } + + private fun bundleValueEquals(first: Any?, second: Any?): Boolean = + when { + first is Bundle && second is Bundle -> bundlesEqual(first, second) + first is IntArray && second is IntArray -> first.contentEquals(second) + first is LongArray && second is LongArray -> first.contentEquals(second) + first is BooleanArray && second is BooleanArray -> first.contentEquals(second) + first is Array<*> && second is Array<*> -> first.contentDeepEquals(second) + else -> first == second + } + fun getSecureBool(key: String, def: Boolean = false): Boolean = secureSettings.getIntForUser(key, if (def) 1 else 0, UserHandle.USER_CURRENT) == 1 From 2dc2c05ed353164156a14e6b6592276bc0d6ced5 Mon Sep 17 00:00:00 2001 From: Quince Date: Fri, 1 May 2026 00:53:30 +0200 Subject: [PATCH 1153/1315] services: Allow early null AxSandbox manager lookups AxSandboxService is registered in SystemServiceRegistry before its binder service is published by system_server. During boot, callers such as SettingsProvider can ask for AxSandboxManager before AxSandboxService.systemReady() adds the binder to ServiceManager. The AxSandbox fetcher correctly returns null in that window and retries on later lookups, but SystemServiceRegistry treats the null result as a missing manager wrapper and logs a WTF when service-not-found diagnostics are enabled. This produces thousands of false boot-time errors until AxSandboxService becomes ready. Treat ax_sandbox like other framework services that can legitimately be unavailable during early boot, while preserving the existing retry and manager creation behavior once the service is published. Signed-off-by: Quince Signed-off-by: Mrick343 --- core/java/android/app/SystemServiceRegistry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 1168b51af9d8..5a6cb24f08b7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -2122,6 +2122,7 @@ public static Object getSystemService(@NonNull ContextImpl ctx, String name) { case Context.DROPBOX_SERVICE: case Context.PERSISTENT_DATA_BLOCK_SERVICE: case Context.OEM_LOCK_SERVICE: + case Context.AX_SANDBOX_SERVICE: return null; case Context.VCN_MANAGEMENT_SERVICE: if (!hasSystemFeatureOpportunistic(ctx, From 5a5c3381566370d0455f3cd413578d41ace47729 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Tue, 12 May 2026 14:20:21 +0000 Subject: [PATCH 1154/1315] SettingsLib: Make expressive text title normal Signed-off-by: Mrick343 --- .../res/layout/settingslib_expressive_preference_text_frame.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_preference_text_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_preference_text_frame.xml index 4bd59d606a30..2240ab6e1aff 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_preference_text_frame.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_preference_text_frame.xml @@ -28,7 +28,7 @@ android:textAlignment="viewStart" android:maxLines="2" android:textSize="16sp" - android:textStyle="bold" + android:textStyle="normal" android:textColor="@color/luna_text_color_primary" android:ellipsize="marquee"/> From 1e4365265703bf6f5ae4923d303c3a9a4b9624ec Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 13 Feb 2026 14:33:13 +0000 Subject: [PATCH 1155/1315] SystemUI: Redesigned qs tile style [1/2] Co-authored-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 15 ++ .../panels/ui/compose/QuickQuickSettings.kt | 25 +- .../ui/compose/infinitegrid/AxTileStyle.kt | 200 ++++++++++++++ .../ui/compose/infinitegrid/CommonTile.kt | 14 +- .../infinitegrid/InfiniteGridLayout.kt | 25 +- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 248 ++++++++++++++---- 6 files changed, 461 insertions(+), 66 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0b5affb833e0..64bb296b9dcb 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7371,6 +7371,21 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_SHAPE = "qs_tile_shape"; + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL = "qs_tile_style_minimal"; + + /** + * @hide + */ + public static final String QS_USE_MODIFIED_TILE_SPACING = "qs_use_modified_tile_spacing"; + + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL_INVERT = "qs_tile_style_minimal_invert"; + /** * Customize Brightness slider shape. * @hide diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 84869c71eac5..ab97fe28dc51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -16,7 +16,9 @@ package com.android.systemui.qs.panels.ui.compose +import android.provider.Settings import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -24,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -31,6 +34,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel @@ -48,6 +52,11 @@ fun ContentScope.QuickQuickSettings( val tiles = sizedTiles.fastMap { it.tile } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + + val context = androidx.compose.ui.platform.LocalContext.current + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } Box(modifier = modifier) { GridAnchor() @@ -80,12 +89,22 @@ fun ContentScope.QuickQuickSettings( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, - modifier = Modifier.sysuiResTag("qqs_tile_layout"), + modifier = Modifier.sysuiResTag("qqs_tile_layout") + .then(if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier), keys = { sizedTiles[it].tile.spec }, ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt new file mode 100644 index 000000000000..60b000d369fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import android.content.Context +import android.graphics.drawable.Drawable +import android.service.quicksettings.Tile.STATE_ACTIVE +import android.service.quicksettings.Tile.STATE_INACTIVE +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.thenIf +import com.android.systemui.Flags +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabelSettings +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.qs.ui.compose.borderOnFocus + +object AxTileDefaults { + val TileCornerRadius = 50.dp + val ActiveTileCornerRadius = 26.dp + + val LargeIconSize = 24.dp + val DividerWidth = 1.dp + val DividerHeight = 16.dp + val IconDividerSpacing = 12.dp + val DividerLabelSpacing = 16.dp + val LargeTileStartPadding = 24.dp + val LargeTileEndPadding = 16.dp + + @Composable + fun dividerColor(state: Int): Color { + return when (state) { + STATE_ACTIVE -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f) + STATE_INACTIVE -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.15f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f) + } + } + + @Composable + fun animateTileShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = if (invertRadius) TileCornerRadius else ActiveTileCornerRadius, + inactiveCornerRadius = if (invertRadius) ActiveTileCornerRadius else TileCornerRadius, + label = "AxTileCornerRadius", + ) + + @Composable + fun animateIconShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = TileCornerRadius, + inactiveCornerRadius = TileCornerRadius, + label = "AxIconCornerRadius", + ) + + @Composable + private fun animateAxShape( + state: Int, + activeCornerRadius: Dp, + inactiveCornerRadius: Dp, + label: String, + ): State { + val radius by animateDpAsState( + targetValue = if (state == STATE_ACTIVE) activeCornerRadius else inactiveCornerRadius, + label = label, + ) + return remember { + mutableStateOf( + RoundedCornerShape( + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float = + with(density) { radius.toPx() } + } + ) + ) + } + } +} + +@Composable +fun AxLargeTileContent( + label: String, + secondaryLabel: String?, + iconProvider: Context.() -> Icon, + sideDrawable: Drawable?, + colors: TileColors, + squishiness: () -> Float, + tileState: Int, + modifier: Modifier = Modifier, + isVisible: () -> Boolean = { true }, + accessibilityUiState: AccessibilityUiState? = null, + iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + textScale: () -> Float = { 1f }, + toggleClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, +) { + val isDualTarget = toggleClick != null + val dividerColor = AxTileDefaults.dividerColor(tileState) + val focusBorderColor = MaterialTheme.colorScheme.secondary + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding( + start = AxTileDefaults.LargeTileStartPadding, + end = AxTileDefaults.LargeTileEndPadding, + ), + ) { + val longPressLabel = longPressLabelSettings().takeIf { onLongClick != null } + + Box( + modifier = Modifier + .fillMaxHeight() + .thenIf(isDualTarget) { + Modifier + .borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .combinedClickable( + onClick = toggleClick!!, + onLongClick = onLongClick, + onLongClickLabel = longPressLabel, + hapticFeedbackEnabled = !Flags.msdlFeedback(), + ) + }, + contentAlignment = Alignment.Center, + ) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + size = { AxTileDefaults.LargeIconSize }, + modifier = Modifier, + ) + } + + if (isDualTarget) { + Spacer(modifier = Modifier.width(AxTileDefaults.IconDividerSpacing)) + Box( + modifier = Modifier + .width(AxTileDefaults.DividerWidth) + .height(AxTileDefaults.DividerHeight) + .background(dividerColor) + ) + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } else { + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } + + LargeTileLabels( + label = label, + secondaryLabel = secondaryLabel, + colors = colors, + accessibilityUiState = accessibilityUiState, + isVisible = isVisible, + modifier = Modifier + .weight(1f) + .bounceScale(TransformOrigin(0f, .5f), textScale), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 39dd82ba9119..5a9cbf29eec9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -416,19 +416,27 @@ object TileBounceMotionTestKeys { } object CommonTileDefaults { - val IconSize = 32.dp - val LargeTileIconSize = 28.dp + val IconSize = 24.dp + val LargeTileIconSize = 24.dp + val SideIconWidth = 32.dp val SideIconHeight = 20.dp val ChevronSize = 14.dp val ToggleTargetSize = 56.dp + val TileHeight = 72.dp + val TileStartPadding = 8.dp val TileEndPadding = 12.dp val TileDualTargetEndPadding = 8.dp + val TileArrangementPadding = 6.dp + val TileColumnSpacing = 20.dp + val TileRowSpacing = 20.dp + + val InactiveCornerRadius = 36.dp val ActiveTileCornerRadius = 24.dp - val InactiveCornerRadius = 50.dp + val TileLabelBlurWidth = 32.dp const val TILE_MARQUEE_ITERATIONS = 1 const val TILE_INITIAL_DELAY_MILLIS = 2000 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 734377e8d216..52ad368b4118 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -16,6 +16,8 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid +import android.provider.Settings +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,6 +30,7 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -83,11 +86,14 @@ constructor( rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) { textFeedbackContentViewModelFactory.create(context) } + + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } val columns = viewModel.columnsWithMediaViewModel.columns val largeTilesSpan = viewModel.columnsWithMediaViewModel.largeSpan val largeTiles by viewModel.iconTilesViewModel.largeTilesState - // Tiles or largeTiles may be updated while this is composed, so listen to any changes val sizedTiles = remember(tiles, largeTiles, largeTilesSpan) { tiles.map { @@ -124,13 +130,24 @@ constructor( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, keys = { sizedTiles[it].tile.spec }, - modifier = modifier, + modifier = modifier.then( + if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier + ), ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index d30e2a3e2283..96ba63d1b902 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -205,14 +205,16 @@ fun ContentScope.Tile( tile.state.collect { value = it.toIconProvider() } } - val colors = TileDefaults.getColorForState(uiState, iconOnly) + val useMinimalStyle = rememberAxTileStyle() + + val colors = TileDefaults.getColorForState(uiState, iconOnly, forceMonochrome = useMinimalStyle) val hapticsViewModel: TileHapticsViewModel? = if (rememberTileHaptic()) { rememberViewModel(traceName = "TileHapticsViewModel") { tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) } } else { - null + null } if (tile.spec.spec == "sound" && !iconOnly) { @@ -220,11 +222,18 @@ fun ContentScope.Tile( return@trace } - val shapeMode = rememberTileShapeMode() - val wantCircle = shapeMode == 4 && iconOnly + val shapeMode = if (useMinimalStyle) 0 else rememberTileShapeMode() + val wantCircle = !useMinimalStyle && shapeMode == 4 && iconOnly val tileShape = - if (wantCircle) CircleShape - else TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + if (wantCircle) { + CircleShape + } else if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + AxTileDefaults.animateTileShapeAsState(uiState.state, useMinimalInvert).value + } else { + TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + } + val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") val isDualTarget = uiState.handlesSecondaryClick @@ -254,8 +263,8 @@ fun ContentScope.Tile( modifier = modifier .then(surfaceRevealModifier) - .thenIf(!wantCircle) { - modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) + .thenIf(!wantCircle) { + Modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) } .fillMaxWidth() .height(CommonTileDefaults.TileHeight) @@ -331,6 +340,7 @@ fun ContentScope.Tile( requestToggleTextFeedback(tile.spec) } } + if (wantCircle) { val interaction = remember { MutableInteractionSource() } @@ -384,7 +394,6 @@ fun ContentScope.Tile( }, ) } else { - val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) val secondaryClick: (() -> Unit)? = { hapticsViewModel?.setTileInteractionState( @@ -393,22 +402,43 @@ fun ContentScope.Tile( tile.toggleClick() } .takeIf { isDualTarget } - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - iconProvider = iconProvider, - sideDrawable = uiState.sideDrawable, - colors = colors, - iconShape = iconShape, - toggleClick = secondaryClick, - onLongClick = longClick, - accessibilityUiState = uiState.accessibilityUiState, - squishiness = squishiness, - isVisible = isVisible, - textScale = { contentBounceable.textBounceScale }, - modifier = - Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), - ) + if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + val iconShape by AxTileDefaults.animateIconShapeAsState(uiState.state, useMinimalInvert) + AxLargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + tileState = uiState.state, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + ) + } else { + val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + modifier = + Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), + ) + } } } } @@ -550,6 +580,46 @@ data class TileColors( val icon: Color, ) +@Composable +fun rememberAxTileStyle(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readAxStyle(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var axStyle by remember { mutableStateOf(readAxStyle()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + axStyle = readAxStyle() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return axStyle +} + @Composable fun rememberTileShapeMode(): Int { val context = LocalContext.current @@ -590,6 +660,46 @@ fun rememberTileShapeMode(): Int { return shapeMode } +@Composable +fun rememberAxMinimalInvert(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readMinimalInvert(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL_INVERT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var minimalInvert by remember { mutableStateOf(readMinimalInvert()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + minimalInvert = readMinimalInvert() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL_INVERT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return minimalInvert +} + @Composable fun rememberTileHaptic(): Boolean { val context = LocalContext.current @@ -651,7 +761,7 @@ private object TileDefaults { fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + return if (isSingleToneStyle) { TileColors( background = MaterialTheme.colorScheme.primary, @@ -676,7 +786,7 @@ private object TileDefaults { fun inactiveDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + return if (isSingleToneStyle) { TileColors( background = CustomColorScheme.current.qsTileColor, @@ -723,11 +833,38 @@ private object TileDefaults { @Composable @ReadOnlyComposable - fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors { + fun activeDualTargetMonochromeTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.primary, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onPrimary, + secondaryLabel = MaterialTheme.colorScheme.onPrimary, + icon = MaterialTheme.colorScheme.onPrimary, + ) + + @Composable + @ReadOnlyComposable + fun inactiveDualTargetMonochromeTileColors(): TileColors = + TileColors( + background = CustomColorScheme.current.qsTileColor, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onSurface, + secondaryLabel = MaterialTheme.colorScheme.onSurface, + icon = MaterialTheme.colorScheme.onSurface, + ) + + @Composable + @ReadOnlyComposable + fun getColorForState( + uiState: TileUiState, + iconOnly: Boolean, + forceMonochrome: Boolean = false, + ): TileColors { return when (uiState.state) { STATE_ACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - activeDualTargetTileColors() + if (forceMonochrome) activeDualTargetMonochromeTileColors() + else activeDualTargetTileColors() } else { activeTileColors() } @@ -735,12 +872,15 @@ private object TileDefaults { STATE_INACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - inactiveDualTargetTileColors() + if (forceMonochrome) inactiveDualTargetMonochromeTileColors() + else inactiveDualTargetTileColors() } else { inactiveTileColors() } } + STATE_UNAVAILABLE -> unavailableTileColors() + else -> unavailableTileColors() } } @@ -772,25 +912,23 @@ private object TileDefaults { label: String, shapeMode: Int, ): State { - val animatedCornerRadius by - animateDpAsState( - targetValue = when (shapeMode) { - 1 -> InactiveCornerRadius // Circle-ish - 2 -> activeCornerRadius // Rounded Square - 3 -> 0.dp // Square - 4 -> InactiveCornerRadius // Circle - else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius - }, - label = label, - ) + val animatedCornerRadius by animateDpAsState( + targetValue = when (shapeMode) { + 1 -> InactiveCornerRadius // Circle-ish + 2 -> activeCornerRadius // Rounded Square + 3 -> 0.dp // Square + 4 -> InactiveCornerRadius // Circle + else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius + }, + label = label, + ) return remember { - val corner = - object : CornerSize { - override fun toPx(shapeSize: Size, density: Density): Float { - return with(density) { animatedCornerRadius.toPx() } - } + val corner = object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } } + } mutableStateOf(RoundedCornerShape(corner)) } } @@ -819,25 +957,23 @@ enum class DualTargetTileStyle { } object DualTargetTileStyleProvider { - + fun getStyle(context: android.content.Context): DualTargetTileStyle { val value = Settings.System.getInt( context.contentResolver, Settings.System.DUAL_TARGET_TILE_STYLE, 0 ) - + return when (value) { 1 -> DualTargetTileStyle.SINGLE else -> DualTargetTileStyle.DUAL } } - - fun isSingleToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.SINGLE - } - - fun isDualToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.DUAL - } + + fun isSingleToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.SINGLE + + fun isDualToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.DUAL } From e6104d6b7adfe4aac525554aa91e5676717af03b Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:49:05 +0000 Subject: [PATCH 1156/1315] SystemUI: Redesigned brightness slider style [1/2] Co-authored-by: Ghosuto Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 5 + .../brightness/ui/compose/BrightnessSlider.kt | 169 ++++++++++++++---- 2 files changed, 144 insertions(+), 30 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 64bb296b9dcb..348a029c2c66 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7392,6 +7392,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_BRIGHTNESS_SLIDER_SHAPE = "qs_brightness_slider_shape"; + /** + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_STYLE = "qs_brightness_slider_style"; + /** * Haptic feedback on QS tiles * @hide diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 1ba29a25f010..cf5d355252a2 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -34,6 +34,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -83,6 +84,7 @@ import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -99,6 +101,8 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.PlatformSlider +import com.android.compose.PlatformSliderDefaults import com.android.compose.modifiers.padding import com.android.compose.modifiers.sliderPercentage import com.android.compose.modifiers.thenIf @@ -156,6 +160,8 @@ fun BrightnessSlider( val cr = context.contentResolver var hapticsEnabled by remember { mutableStateOf(readEnableHaptics(cr)) } + var useAxStyle by remember { mutableStateOf(readUseAxStyle(cr)) } + var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } val shapeMode = rememberSliderShapeMode() val trackCornerDp: Dp = when (shapeMode) { @@ -173,6 +179,7 @@ fun BrightnessSlider( val enabled = !isRestricted val contentDescription = stringResource(R.string.accessibility_brightness) val interactionSource = remember { MutableInteractionSource() } + val hapticsViewModel: SliderHapticsViewModel? = if (hapticsEnabled) { rememberViewModel(traceName = "SliderHapticsViewModel") { @@ -189,6 +196,128 @@ fun BrightnessSlider( } else { null } + + val hasAutoBrightness = context.resources.getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available + ) + + DisposableEffect(Unit) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + hapticsEnabled = readEnableHaptics(cr) + useAxStyle = readUseAxStyle(cr) + showAutoBrightness = readShowAutoBrightness(cr) + } + } + } + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_STYLE), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + cr.unregisterContentObserver(observer) + } + } + + if (useAxStyle) { + val iconRes by + remember(gammaValue, valueRange) { + derivedStateOf { + val percentage = + (value - valueRange.first) * 100f / (valueRange.last - valueRange.first) + iconResProvider(percentage) + } + } + val axIconRes = if (autoMode) R.drawable.ic_qs_brightness_auto_on else iconRes + val axIconSize = 56.dp + val iconTapScope = rememberCoroutineScope() + val sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors().copy( + trackColor = CustomColorScheme.current.qsTileColor, + ) + + Box(modifier = modifier) { + PlatformSlider( + value = animatedValue, + onValueChange = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChange(it) + value = it.toInt() + onDrag(value) + } + }, + onValueChangeFinished = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChangeEnded() + onStop(value) + } + }, + valueRange = floatValueRange, + enabled = enabled, + interactionSource = interactionSource, + colors = sliderColors, + modifier = Modifier + .fillMaxWidth() + .height(axIconSize) + .sysuiResTag("slider") + .semantics(mergeDescendants = true) { + this.text = AnnotatedString(contentDescription) + } + .sliderPercentage { + (value - valueRange.first).toFloat() / (valueRange.last - valueRange.first) + } + .thenIf(isRestricted) { + Modifier.clickable { + if (restriction is PolicyRestriction.Restricted) { + onRestrictedClick(restriction) + } + } + }, + icon = { _ -> + Icon( + painter = painterResource(axIconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + ) + + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .size(axIconSize) + .clip(CircleShape) + .pointerInput(Unit) { + detectTapGestures { + iconTapScope.launch { onIconClick() } + } + } + ) + } + + val currentShowToast by rememberUpdatedState(showToast) + LaunchedEffect(interactionSource, overriddenByAppState) { + interactionSource.interactions.collect { interaction -> + if (interaction is DragInteraction.Start && overriddenByAppState) { + currentShowToast() + } + } + } + return + } + val colors = colors() // The value state is recreated every time gammaValue changes, so we recreate this derivedState @@ -236,36 +365,6 @@ fun BrightnessSlider( } } - val hasAutoBrightness = context.resources.getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available - ) - var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } - - DisposableEffect(Unit) { - val observer = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - context.mainExecutor.execute { - showAutoBrightness = readShowAutoBrightness(cr) - hapticsEnabled = readEnableHaptics(cr) - } - } - } - - cr.registerContentObserver( - LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), - false, observer, UserHandle.USER_ALL - ) - - cr.registerContentObserver( - Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), - false, observer, UserHandle.USER_ALL - ) - - onDispose { - cr.unregisterContentObserver(observer) - } - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -499,6 +598,16 @@ private fun readEnableHaptics(cr: ContentResolver): Boolean = false } +private fun readUseAxStyle(cr: ContentResolver): Boolean = + try { + Settings.System.getIntForUser( + cr, Settings.System.QS_BRIGHTNESS_SLIDER_STYLE, + 0, UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + @Composable private fun drawAutoBrightnessButton( autoMode: Boolean, From d94d7dadc596ccf896f22cd9ee403d021b80093d Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 25 Mar 2026 23:53:38 +0530 Subject: [PATCH 1157/1315] fixup! SystemUI: Add QS tile layout settings [1/2] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../panels/data/repository/QuickQuickSettingsRowRepository.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QuickQuickSettingsRowRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QuickQuickSettingsRowRepository.kt index 4602a43d08b1..268aeda20c20 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QuickQuickSettingsRowRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QuickQuickSettingsRowRepository.kt @@ -33,6 +33,7 @@ import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge @@ -71,6 +72,7 @@ class QuickQuickSettingsRowRepository @Inject constructor( merge(configurationRepository.onConfigurationChange, settingsChanges()) .emitOnStart() .mapLatest { readRows() } + .distinctUntilChanged() val defaultRows: Int = resources.getInteger(R.integer.quick_qs_paginated_grid_num_rows) From 3055f08ff0bc0289e6ee961cde036cd8b7c1d380 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 25 Feb 2026 21:16:11 +0530 Subject: [PATCH 1158/1315] SystemUI: Add QS tile gradient customization Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 ++ .../ui/compose/infinitegrid/CommonTile.kt | 9 +- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 99 +++++++++++++++++-- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 348a029c2c66..2bf1d4888a24 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7734,6 +7734,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String UNLIMIT_SCREENRECORD = "unlimit_screenrecord"; + /** + * Gradient on QS tiles + * @hide + */ + public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 5a9cbf29eec9..e06e545bb728 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -149,7 +149,14 @@ fun LargeTileContent( Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) .clip(iconShape) .verticalSquish(squishiness) - .drawBehind { drawRect(animatedBackgroundColor) } + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedBackgroundColor) + } + } .combinedClickable( onClick = toggleClick!!, onLongClick = onLongClick, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 96ba63d1b902..47c3d9dc11b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -68,9 +68,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -350,7 +354,14 @@ fun ContentScope.Tile( .size(CommonTileDefaults.TileHeight) .align(Alignment.Center) .clip(CircleShape) - .background(animatedColor) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + } .indication(interaction, LocalIndication.current) .tileCombinedClickable( onClick = { click?.invoke() ?: Unit }, @@ -382,6 +393,7 @@ fun ContentScope.Tile( iconOnly = iconOnly, isDualTarget = isDualTarget, modifier = contentRevealModifier, + colors = colors, ) { val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } if (iconOnly) { @@ -473,6 +485,7 @@ fun TileContainer( isDualTarget: Boolean, interactionSource: MutableInteractionSource?, modifier: Modifier = Modifier, + colors: TileColors, content: @Composable BoxScope.() -> Unit, ) { Box( @@ -488,7 +501,16 @@ fun TileContainer( isDualTarget = isDualTarget, interactionSource = interactionSource, ) - .tileTestTag(iconOnly), + .tileTestTag(iconOnly) + .thenIf(!isDualTarget || iconOnly) { + Modifier + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } + } + }, content = content, ) } @@ -506,7 +528,14 @@ fun LargeStaticTile( Box( modifier .clip(TileDefaults.animateTileShapeAsState(state = uiState.state, shapeMode = shapeMode).value) - .background(colors.background) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = colors.background) + } + } .height(TileHeight) .largeTilePadding() ) { @@ -578,6 +607,7 @@ data class TileColors( val label: Color, val secondaryLabel: Color, val icon: Color, + val iconBackgroundGradient: Brush? = null, ) @Composable @@ -740,27 +770,66 @@ fun rememberTileHaptic(): Boolean { return hapticEnabled } +@Composable +fun rememberQsGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return enabled +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp /** An active tile uses the active color as background */ @Composable - @ReadOnlyComposable - fun activeTileColors(): TileColors = - TileColors( + fun activeTileColors(): TileColors { + val gradientEnabled = rememberQsGradient() + val gradient = qsTileBackgroundBrush(gradientEnabled) + + return TileColors( background = MaterialTheme.colorScheme.primary, iconBackground = MaterialTheme.colorScheme.primary, label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) + } /** An active tile with dual target only show the active color on the icon */ @Composable - @ReadOnlyComposable fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) + val gradientEnabled = rememberQsGradient() + val gradient = qsTileBackgroundBrush(gradientEnabled) return if (isSingleToneStyle) { TileColors( @@ -777,6 +846,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) } } @@ -854,7 +924,6 @@ private object TileDefaults { ) @Composable - @ReadOnlyComposable fun getColorForState( uiState: TileUiState, iconOnly: Boolean, @@ -932,6 +1001,20 @@ private object TileDefaults { mutableStateOf(RoundedCornerShape(corner)) } } + + @Composable + fun qsTileBackgroundBrush(enabled: Boolean): Brush? { + if (!enabled) return null + + return Brush.linearGradient( + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ), + start = Offset(0f, 0f), + end = Offset.Infinite + ) + } } /** From 9cb80ebf02ce34dc5050e8f83fb7723f315d44c6 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 26 Feb 2026 00:00:05 +0530 Subject: [PATCH 1159/1315] SystemUI: Add QS brightness slider gradient customization Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + .../brightness/ui/compose/BrightnessSlider.kt | 259 +++++++++++++++++- 2 files changed, 253 insertions(+), 12 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2bf1d4888a24..3d7c944dbc14 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7740,6 +7740,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + /** + * Gradient on QS brightness slider + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_GRADIENT = "qs_brightness_slider_gradient"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index cf5d355252a2..9815e8a8cc62 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -74,11 +74,16 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.BitmapPainter @@ -171,6 +176,8 @@ fun BrightnessSlider( else -> Dimensions.SliderTrackRoundedCorner } + val brightnessGradient = brightnessSliderGradient() + var value by remember(gammaValue) { mutableIntStateOf(gammaValue) } val animatedValue by animateFloatAsState(targetValue = value.toFloat(), label = "BrightnessSliderAnimatedValue") @@ -244,8 +251,9 @@ fun BrightnessSlider( val axIconRes = if (autoMode) R.drawable.ic_qs_brightness_auto_on else iconRes val axIconSize = 56.dp val iconTapScope = rememberCoroutineScope() + val axTrackColor = brightnessGradient?.brush?.let { null } ?: CustomColorScheme.current.qsTileColor val sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors().copy( - trackColor = CustomColorScheme.current.qsTileColor, + trackColor = axTrackColor ?: CustomColorScheme.current.qsTileColor, ) Box(modifier = modifier) { @@ -278,6 +286,38 @@ fun BrightnessSlider( .sliderPercentage { (value - valueRange.first).toFloat() / (valueRange.last - valueRange.first) } + .then( + if (brightnessGradient != null) { + // Draw gradient over the active portion of the AX style track + Modifier.drawWithContent { + drawContent() + val fraction = (value - valueRange.first).toFloat() / (valueRange.last - valueRange.first) + val activeEnd = (size.width * fraction).coerceAtLeast(0f) + if (activeEnd > 0f) { + val cornerRadius = CornerRadius(28.dp.toPx()) + val clipPath = Path().apply { + addRoundRect( + RoundRect( + left = 0f, + top = 0f, + right = activeEnd.coerceAtMost(size.width), + bottom = size.height, + radiusX = cornerRadius.x, + radiusY = cornerRadius.y, + ) + ) + } + clipPath(clipPath) { + drawRect( + brush = brightnessGradient.brush, + topLeft = Offset.Zero, + size = Size(activeEnd.coerceAtMost(size.width), size.height) + ) + } + } + } + } else Modifier + ) .thenIf(isRestricted) { Modifier.clickable { if (restriction is PolicyRestriction.Restricted) { @@ -299,6 +339,15 @@ fun BrightnessSlider( .align(Alignment.CenterStart) .size(axIconSize) .clip(CircleShape) + .then( + if (brightnessGradient != null) { + // Draw gradient brush over the thumb circle + Modifier.drawWithContent { + drawContent() + drawRect(brush = brightnessGradient.brush) + } + } else Modifier + ) .pointerInput(Unit) { detectTapGestures { iconTapScope.launch { onIconClick() } @@ -318,7 +367,9 @@ fun BrightnessSlider( return } - val colors = colors() + val colors = colors(brightnessGradient) + + val trackShape = RoundedCornerShape(trackCornerDp) // The value state is recreated every time gammaValue changes, so we recreate this derivedState // We have to use value as that's the value that changes when the user is dragging (gammaValue @@ -469,12 +520,29 @@ fun BrightnessSlider( ) if (activeTrackEnd > 0f) { - drawRoundRect( - color = colors.activeTrackColor, - topLeft = Offset(0f, 0f), - size = Size(activeTrackEnd, trackHeight), - cornerRadius = cornerRadius - ) + if (brightnessGradient != null) { + // Draw gradient over the active track portion + val outline = trackShape.createOutline( + Size(activeTrackEnd.coerceAtMost(trackWidth), trackHeight), + layoutDirection, + this + ) + val clipPath = outline.asPath() + clipPath(clipPath) { + drawRect( + brush = brightnessGradient.brush, + topLeft = Offset.Zero, + size = Size(activeTrackEnd.coerceAtMost(trackWidth), trackHeight) + ) + } + } else { + drawRoundRect( + color = colors.activeTrackColor, + topLeft = Offset(0f, 0f), + size = Size(activeTrackEnd, trackHeight), + cornerRadius = cornerRadius + ) + } } val yOffset = trackHeight / 2 - IconSize.toSize().height / 2 @@ -528,6 +596,14 @@ fun BrightnessSlider( } } +fun Outline.asPath(): Path { + return when (this) { + is Outline.Generic -> path + is Outline.Rounded -> Path().apply { addRoundRect(roundRect) } + is Outline.Rectangle -> Path().apply { addRect(rect) } + } +} + @Composable fun rememberSliderShapeMode(): Int { val context = LocalContext.current @@ -568,6 +644,155 @@ fun rememberSliderShapeMode(): Int { return shapeMode } +private data class BrightnessGradient( + val brush: Brush, +) + +@Composable +private fun rememberSliderGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return enabled +} + +@Composable +private fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +private fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + +@Composable +private fun brightnessSliderGradient(): BrightnessGradient? { + if (!rememberSliderGradient()) return null + + val mode = rememberGradientColorMode() + val colors = if (mode == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } + + return BrightnessGradient( + brush = Brush.horizontalGradient(colors) + ) +} + private fun Modifier.sliderBackground(color: Color, corner: Dp) = drawWithCache { val offsetAround = SliderBackgroundFrameSize.toSize() val newSize = Size(size.width + 2 * offsetAround.width, size.height + 2 * offsetAround.height) @@ -630,9 +855,11 @@ private fun drawAutoBrightnessButton( 3 -> RoundedCornerShape(0.dp) else -> RoundedCornerShape(animatedCornerRadius) } + val brightnessGradient = brightnessSliderGradient() + val autoIconBrush: Brush? = if (autoMode) brightnessGradient?.brush else null val backgroundColor by animateColorAsState( targetValue = if (autoMode) { - MaterialTheme.colorScheme.primary + if (autoIconBrush == null) MaterialTheme.colorScheme.primary else Color.Unspecified } else { CustomColorScheme.current.qsTileColor } @@ -659,7 +886,13 @@ private fun drawAutoBrightnessButton( modifier = Modifier .size(TrackHeight) .clip(autoIconShape) - .background(backgroundColor) + .then( + if (autoIconBrush != null) { + Modifier.background(autoIconBrush) + } else { + Modifier.background(backgroundColor) + } + ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, // Disable ripple effect @@ -833,9 +1066,11 @@ object BrightnessSliderMotionTestKeys { } @Composable -private fun colors(): SliderColors { - return SliderDefaults.colors() +private fun colors(brightnessGradient: BrightnessGradient?): SliderColors { + val base = SliderDefaults.colors() + return base .copy( + activeTrackColor = if (brightnessGradient != null) Color.Transparent else base.activeTrackColor, inactiveTrackColor = CustomColorScheme.current.qsTileColor, activeTickColor = MaterialTheme.colorScheme.onPrimary, inactiveTickColor = MaterialTheme.colorScheme.onSurface, From 008453b4508039b93ea4d333498ad9a8f7eb6237 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 26 Feb 2026 23:04:11 +0530 Subject: [PATCH 1160/1315] SystemUI: Add volume slider gradient customization Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 6 + .../ui/compose/VolumeDialogSliderTrack.kt | 222 +++++++++++++++++- 2 files changed, 221 insertions(+), 7 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3d7c944dbc14..b384d963b250 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7746,6 +7746,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_BRIGHTNESS_SLIDER_GRADIENT = "qs_brightness_slider_gradient"; + /** + * Gradient on Volume slider + * @hide + */ + public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt index 28557c854e1a..54b11f04a1a1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -16,29 +16,47 @@ package com.android.systemui.volume.dialog.sliders.ui.compose +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -77,21 +95,35 @@ fun SliderTrack( Layout( measurePolicy = measurePolicy, content = { - SliderDefaults.Track( + + val gradientEnabled = rememberVolumeGradientEnabled() + + val gStart = MaterialTheme.colorScheme.primary + val gEnd = MaterialTheme.colorScheme.secondary + + val activeBrush = + if (gradientEnabled && gStart != Color(0) && gEnd != Color(0)) { + listOf(gStart, gEnd) + } else { + null + } + + GradientSliderTrack( sliderState = sliderState, + isEnabled = isEnabled, colors = colors, - enabled = isEnabled, trackCornerSize = trackCornerSize, trackInsideCornerSize = trackInsideCornerSize, - drawStopIndicator = null, thumbTrackGapSize = thumbTrackGapSize, - drawTick = { _, _ -> }, + isVertical = isVertical, + gradientColors = activeBrush, modifier = - Modifier.then( + Modifier + .then( if (isVertical) { - Modifier.width(trackSize) + Modifier.width(trackSize).fillMaxHeight() } else { - Modifier.height(trackSize) + Modifier.height(trackSize).fillMaxWidth() } ) .layoutId(Contents.Track), @@ -130,6 +162,182 @@ fun SliderTrack( ) } +private data class TrackGradient( + val brush: Brush, +) + +@Composable +private fun rememberVolumeGradientEnabled(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return enabled +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun GradientSliderTrack( + sliderState: SliderState, + isEnabled: Boolean, + colors: SliderColors, + trackCornerSize: Dp, + trackInsideCornerSize: Dp, + thumbTrackGapSize: Dp, + isVertical: Boolean, + gradientColors: List?, + modifier: Modifier = Modifier, +) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + val inactiveColor = + if (isEnabled) colors.inactiveTrackColor else colors.disabledInactiveTrackColor + val activeSolidColor = + if (isEnabled) colors.activeTrackColor else colors.disabledActiveTrackColor + + Box( + modifier = + modifier.drawBehind { + val w = size.width + val h = size.height + if (w <= 0f || h <= 0f) return@drawBehind + + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val outerR = trackCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val innerR = trackInsideCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val halfGap = (thumbTrackGapSize.toPx() / 2f).coerceAtLeast(0f) + + drawRoundRect( + color = inactiveColor, + size = size, + cornerRadius = CornerRadius(outerR, outerR) + ) + + if (!isVertical) { + val splitX = if (isRtl) w * (1f - frac) else w * frac + + val gapEff = + if (isRtl) { + minOf(halfGap, splitX) + } else { + minOf(halfGap, w - splitX) + } + + val activeStart = if (isRtl) splitX + gapEff else 0f + val activeEnd = if (isRtl) w else splitX - gapEff + + if (activeEnd <= activeStart) return@drawBehind + + val eps = 0.5f + + val hitsStart = activeStart <= eps + val hitsEnd = activeEnd >= (w - eps) + + val leftR = + if (isRtl) { + if (hitsStart) outerR else innerR // rounded only at max (when it reaches start) + } else { + outerR // far end always rounded + } + + val rightR = + if (isRtl) { + outerR // far end always rounded + } else { + if (hitsEnd) outerR else innerR // rounded only at max (when it reaches end) + } + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(activeStart, 0f, activeEnd, h), + topLeft = CornerRadius(leftR, leftR), + bottomLeft = CornerRadius(leftR, leftR), + topRight = CornerRadius(rightR, rightR), + bottomRight = CornerRadius(rightR, rightR), + ) + ) + } + + val brush = + if (gradientColors != null) Brush.horizontalGradient(gradientColors) + else Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + + drawPath(path = path, brush = brush) + } else { + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val splitY = h * (1f - frac) + + val gapEff = minOf(halfGap, splitY) + + val activeTop = splitY + gapEff + val activeBottom = h + if (activeBottom <= activeTop) return@drawBehind + + val eps = 0.5f + val hitsTop = activeTop <= eps + + val topR = if (hitsTop) outerR else innerR + val bottomR = outerR + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(0f, activeTop, w, activeBottom), + topLeft = CornerRadius(topR, topR), + bottomLeft = CornerRadius(bottomR, bottomR), + topRight = CornerRadius(topR, topR), + bottomRight = CornerRadius(bottomR, bottomR), + ) + ) + } + + val brush = + if (gradientColors != null) { + Brush.verticalGradient( + colors = gradientColors, + startY = activeBottom, + endY = activeTop + ) + } else { + Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + } + + drawPath(path = path, brush = brush) + } + } + ) +} + @Composable private fun TrackIcon( icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?, From 4fb529352837a1cadea2b7826c1a3fb9bc9e2a60 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 27 Feb 2026 00:57:28 +0530 Subject: [PATCH 1161/1315] SystemUI: Add custom gradient start/end color support Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 18 +++ .../qs/panels/ui/compose/infinitegrid/Tile.kt | 101 +++++++++++++++- .../ui/VolumeDialogSliderViewBinder.kt | 19 ++- .../ui/compose/VolumeDialogSliderTrack.kt | 111 ++++++++++++++++-- 4 files changed, 236 insertions(+), 13 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b384d963b250..c02225d30b3d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7752,6 +7752,24 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient"; + /** + * Gradient color mode + * @hide + */ + public static final String CUSTOM_GRADIENT_COLOR_MODE = "custom_gradient_color_mode"; + + /** + * Gradient start color + * @hide + */ + public static final String CUSTOM_GRADIENT_START_COLOR = "custom_gradient_start_color"; + + /** + * Gradient end color + * @hide + */ + public static final String CUSTOM_GRADIENT_END_COLOR = "custom_gradient_end_color"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 47c3d9dc11b9..6a86bd88c455 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -804,6 +804,93 @@ fun rememberQsGradient(): Boolean { return enabled } +@Composable +private fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +private fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp @@ -1006,11 +1093,19 @@ private object TileDefaults { fun qsTileBackgroundBrush(enabled: Boolean): Brush? { if (!enabled) return null - return Brush.linearGradient( - colors = listOf( + val mode = rememberGradientColorMode() + val colors = if (mode == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary - ), + ) + } + + return Brush.linearGradient( + colors = colors, start = Offset(0f, 0f), end = Offset.Infinite ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index bb6b59c08e97..7bdffaee7302 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -46,6 +47,9 @@ import com.android.systemui.res.R import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientColorMode +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientCustomColors +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberVolumeGradientEnabled import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider @@ -128,6 +132,16 @@ private fun VolumeDialogSlider( } } + val thumbColorOverride: Color? = + if (!rememberVolumeGradientEnabled()) { + null + } else if (rememberGradientColorMode() == 1) { + val (customStart, _) = rememberGradientCustomColors() + customStart + } else { + MaterialTheme.colorScheme.primary + } + Slider( value = sliderStateModel.value, valueRange = sliderStateModel.valueRange, @@ -191,6 +205,7 @@ private fun VolumeDialogSlider( isVisible = iconsState.isInactiveTrackEndIconVisible, ) }, + ignoreGradient = false, ) }, thumb = { sliderState, interactions -> @@ -198,7 +213,9 @@ private fun VolumeDialogSlider( sliderState = sliderState, interactionSource = interactions, enabled = !sliderStateModel.isDisabled, - colors = colors, + colors = SliderDefaults.colors( + thumbColor = thumbColorOverride ?: SliderDefaults.colors().thumbColor + ), thumbSize = if (isVolumeDialogVertical) { DpSize(52.dp, 4.dp) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt index 54b11f04a1a1..c8234357c2eb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -81,6 +82,7 @@ fun SliderTrack( activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + ignoreGradient: Boolean = true, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val measurePolicy = @@ -96,17 +98,21 @@ fun SliderTrack( measurePolicy = measurePolicy, content = { - val gradientEnabled = rememberVolumeGradientEnabled() - - val gStart = MaterialTheme.colorScheme.primary - val gEnd = MaterialTheme.colorScheme.secondary + val gradientColors = if (rememberGradientColorMode() == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } val activeBrush = - if (gradientEnabled && gStart != Color(0) && gEnd != Color(0)) { - listOf(gStart, gEnd) - } else { + if (!ignoreGradient && rememberVolumeGradientEnabled()) + gradientColors + else null - } GradientSliderTrack( sliderState = sliderState, @@ -167,7 +173,7 @@ private data class TrackGradient( ) @Composable -private fun rememberVolumeGradientEnabled(): Boolean { +fun rememberVolumeGradientEnabled(): Boolean { val context = LocalContext.current val contentResolver = context.contentResolver @@ -204,6 +210,93 @@ private fun rememberVolumeGradientEnabled(): Boolean { return enabled } +@Composable +fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + @Composable @OptIn(ExperimentalMaterial3Api::class) private fun GradientSliderTrack( From f9f28ca7b04759d15c161f15fadda4bbe59e6edb Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 26 Mar 2026 09:01:22 +0530 Subject: [PATCH 1162/1315] SystemUI: Add classic QS panel style for tiles Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 18 ++ .../repository/LargeTileSpanRepository.kt | 38 ++++ .../interactor/LargeTileSpanInteractor.kt | 2 + .../ui/compose/infinitegrid/CommonTile.kt | 79 +++++++ .../ui/compose/infinitegrid/EditTile.kt | 4 +- .../compose/infinitegrid/QSTileIconShapes.kt | 213 ++++++++++++++++++ .../qs/panels/ui/compose/infinitegrid/Tile.kt | 207 +++++++++++++++-- .../ui/viewmodel/InfiniteGridViewModel.kt | 6 +- .../panels/ui/viewmodel/QSColumnsViewModel.kt | 11 +- 9 files changed, 551 insertions(+), 27 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c02225d30b3d..64c0ca92c991 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7770,6 +7770,24 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String CUSTOM_GRADIENT_END_COLOR = "custom_gradient_end_color"; + /** + * Whether to use classic QS panel style for tiles + * @hide + */ + public static final String QS_PANEL_STYLE = "qs_panel_style"; + + /** + * Whether to hide label under tiles in classic QS panel style + * @hide + */ + public static final String QS_TILE_LABEL_HIDE = "qs_tile_label_hide"; + + /** + * Select tile shape for classic QS panel style + * @hide + */ + public static final String QS_TILE_ICON_SHAPE = "qs_tile_icon_shape"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt index 5ec2cfde14cb..f3f1e52e9219 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt @@ -16,15 +16,23 @@ package com.android.systemui.qs.panels.data.repository +import android.content.Context import android.content.res.Resources +import android.database.ContentObserver +import android.net.Uri +import android.os.UserHandle +import android.provider.Settings import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest @@ -33,6 +41,7 @@ import kotlinx.coroutines.flow.mapLatest class LargeTileSpanRepository @Inject constructor( + @Application private val context: Context, @ShadeDisplayAware private val resources: Resources, @ShadeDisplayAware configurationRepository: ConfigurationRepository, ) { @@ -60,4 +69,33 @@ constructor( const val FONT_SCALE_THRESHOLD = 1.8f const val DEFAULT_LARGE_TILE_WIDTH = 2 } + + private fun settingsChanges(): Flow = callbackFlow { + val observer = object : ContentObserver(/* handler */ null) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + trySend(Unit) + } + } + val cr = context.contentResolver + cr.registerContentObserver(Settings.System.getUriFor(Settings.System.QS_PANEL_STYLE), + /* notifyForDescendants */ false, observer, UserHandle.USER_ALL) + awaitClose { cr.unregisterContentObserver(observer) } + } + + private fun readQSPanelStyleEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + context.contentResolver, Settings.System.QS_PANEL_STYLE, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + val classicStyle: Flow = + settingsChanges() + .emitOnStart() + .mapLatest { readQSPanelStyleEnabled() } + .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/LargeTileSpanInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/LargeTileSpanInteractor.kt index 2d57158437e1..55f08daa6c51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/LargeTileSpanInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/LargeTileSpanInteractor.kt @@ -28,4 +28,6 @@ class LargeTileSpanInteractor @Inject constructor(repo: LargeTileSpanRepository) val tileMaxWidth: Flow = repo.tileMaxWidth val defaultTileMaxWidth: Int = repo.defaultTileMaxWidth + + val classicStyle: Flow = repo.classicStyle } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index e06e545bb728..2beb83f8f855 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -39,6 +39,7 @@ 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.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -84,9 +85,13 @@ import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.painter.rememberDrawablePainter @@ -116,6 +121,79 @@ private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target" private const val TEST_TAG_SMALL = "qs_tile_small" private const val TEST_TAG_LARGE = "qs_tile_large" +@Composable +fun ClassicTileContent( + label: String, + iconProvider: Context.() -> Icon, + iconShapeKey: String, + colors: TileColors, + labelHide: Boolean, + modifier: Modifier = Modifier, +) { + val iconShape = remember(iconShapeKey) { + QSTileIconShapes.shapeForKey(iconShapeKey) + } + + val animatedColor by animateColorAsState(colors.background, label = "QSTileCircleBgColor") + + val tileHeight = if (labelHide) { + CommonTileDefaults.TileHeight + } else { + CommonTileDefaults.TileHeight - 8.dp + } + + val iconSize = if (labelHide) { + CommonTileDefaults.IconSize + } else { + CommonTileDefaults.LargeTileIconSize + } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + Box( + modifier = Modifier + .size(tileHeight) + .clip(iconShape) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + }, + ) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + size = { iconSize }, + modifier = Modifier.align(Alignment.Center), + ) + } + + if (!labelHide) { + val labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f) + BasicText( + text = label, + style = TextStyle( + fontSize = CommonTileDefaults.ClassicLabelSize, + textAlign = TextAlign.Center, + hyphens = Hyphens.Auto, + ), + color = { labelColor }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth(), + ) + } + } +} + @Composable fun LargeTileContent( label: String, @@ -445,6 +523,7 @@ object CommonTileDefaults { val ActiveTileCornerRadius = 24.dp val TileLabelBlurWidth = 32.dp + val ClassicLabelSize = 10.sp const val TILE_MARQUEE_ITERATIONS = 1 const val TILE_INITIAL_DELAY_MILLIS = 2000 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index a4b1bc30d125..025af09e395f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -190,6 +190,7 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaul import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.GridBackgroundCornerRadius import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.TilePlacementSpec import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberTileShapeMode +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQSPanelStyle import com.android.systemui.qs.panels.ui.compose.selection.InteractiveTileContainer import com.android.systemui.qs.panels.ui.compose.selection.MutableSelectionState import com.android.systemui.qs.panels.ui.compose.selection.ResizingState @@ -1414,8 +1415,9 @@ private fun Modifier.tileBackground( color: () -> Color, iconOnly: Boolean, ): Modifier { + val panelStyle = rememberQSPanelStyle() val shapeMode = rememberTileShapeMode() - return if (shapeMode == 4 && iconOnly) { + return if (panelStyle || (shapeMode == 4 && iconOnly)) { // Draw a centered circle that fits the tile's min dimension drawBehind { val border = 0f diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt new file mode 100644 index 000000000000..d7af5af8b868 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-License-Identifier: Apache-2.0 + */ + + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import android.graphics.Matrix +import android.util.PathParser +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +object QSTileIconShapes { + + const val DEFAULT_KEY = "circle" + + val PATH_BY_KEY: Map = linkedMapOf( + "circle" to + "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0", + "square" to + "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 " + + "87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 " + + "99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 " + + "93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 " + + "53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 " + + "12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 " + + ".82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 " + + "6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 " + + "46.311 .82Z", + "four_sided_cookie" to + "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3" + + "C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888" + + "C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605" + + "C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483" + + "C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97" + + "C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112" + + "C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395" + + "C -6.176 15.268 15.267 -6.176 36.395 3Z", + "seven_sided_cookie" to + "M 35.209 6.878 C 36.326 5.895 36.884 5.404 37.397 5.006 " + + "C 44.82 -0.742 55.18 -0.742 62.603 5.006 C 63.116 5.404 63.674 5.895 " + + "64.791 6.878 C 65.164 7.207 65.351 7.371 65.539 7.529 " + + "C 68.167 9.734 71.303 11.248 74.663 11.932 C 74.902 11.981 75.147 12.025 " + + "75.637 12.113 C 77.1 12.375 77.831 12.506 78.461 12.66 " + + "C 87.573 14.893 94.032 23.011 94.176 32.412 C 94.186 33.062 94.151 33.805 " + + "94.08 35.293 C 94.057 35.791 94.045 36.04 94.039 36.285 " + + "C 93.958 39.72 94.732 43.121 96.293 46.18 C 96.404 46.399 96.522 46.618 " + + "96.759 47.056 C 97.467 48.366 97.821 49.021 98.093 49.611 " + + "C 102.032 58.143 99.727 68.266 92.484 74.24 C 91.983 74.653 91.381 75.089 " + + "90.177 75.961 C 89.774 76.254 89.572 76.4 89.377 76.548 " + + "C 86.647 78.626 84.477 81.353 83.063 84.483 C 82.962 84.707 82.865 84.936 " + + "82.671 85.395 C 82.091 86.766 81.8 87.451 81.51 88.033 " + + "C 77.31 96.44 67.977 100.945 58.801 98.994 C 58.166 98.859 57.451 98.659 " + + "56.019 98.259 C 55.54 98.125 55.3 98.058 55.063 97.998 " + + "C 51.74 97.154 48.26 97.154 44.937 97.998 C 44.699 98.058 44.46 98.125 " + + "43.981 98.259 C 42.549 98.659 41.834 98.859 41.199 98.994 " + + "C 32.023 100.945 22.69 96.44 18.49 88.033 C 18.2 87.451 17.909 86.766 " + + "17.329 85.395 C 17.135 84.936 17.038 84.707 16.937 84.483 " + + "C 15.523 81.353 13.353 78.626 10.623 76.548 C 10.428 76.4 10.226 76.254 " + + "9.823 75.961 C 8.619 75.089 8.017 74.653 7.516 74.24 " + + "C 0.273 68.266 -2.032 58.143 1.907 49.611 C 2.179 49.021 2.533 48.366 " + + "3.241 47.056 C 3.478 46.618 3.596 46.399 3.707 46.18 " + + "C 5.268 43.121 6.042 39.72 5.961 36.285 C 5.955 36.04 5.943 35.791 " + + "5.92 35.293 C 5.849 33.805 5.814 33.062 5.824 32.412 " + + "C 5.968 23.011 12.427 14.893 21.539 12.66 C 22.169 12.506 22.9 12.375 " + + "24.363 12.113 C 24.853 12.025 25.098 11.981 25.337 11.932 " + + "C 28.697 11.248 31.833 9.734 34.461 7.529 C 34.649 7.371 34.836 7.207 " + + "35.209 6.878 Z", + "arch" to + "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 " + + "99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100" + + "H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 " + + ".1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z", + + "ios_rounded_square" to + "M24.00,0.00L76.00,0.00A24.00 24.00 0 0 1 100.00,24.00L100.00,76.00" + + "A24.00 24.00 0 0 1 76.00,100.00L24.00,100.00A24.00 24.00 0 0 1 " + + "0.00,76.00L0.00,24.00A24.00 24.00 0 0 1 24.00,0.00z", + + "meow" to + "M 4.432 31.65 C 4.656 31.126 4.736 30.551 4.663 29.985 " + + "C 4.026 26.269 1.314 11.591 0.116 4.701 C -0.235 2.992 0.443 0.912 " + + "1.895 0.145 C 2.336 -0.072 2.839 -0.014 3.293 0.112 " + + "C 4.378 0.473 5.381 1.08 6.357 1.718 C 10.502 4.416 14.621 7.167 " + + "18.732 9.933 C 19.823 10.666 21.252 10.658 22.334 9.911 " + + "C 30.185 4.6 39.651 1.503 49.833 1.503 C 60.18 1.503 69.786 4.701 " + + "77.716 10.161 C 78.797 10.922 80.235 10.937 81.331 10.199 " + + "C 86.109 6.997 90.875 3.776 95.646 0.564 C 96.99 -0.438 99.098 0.259 " + + "99.76 1.907 C 100.193 3.164 99.88 4.529 99.634 5.789 " + + "C 98.325 13.019 95.754 27.409 95.207 30.68 C 95.169 31.181 95.249 31.684 " + + "95.441 32.148 C 97.788 37.889 99.082 44.17 99.082 50.752 " + + "C 99.082 77.932 77.014 100 49.833 100 C 22.653 100 0.585 77.932 " + + "0.585 50.752 C 0.585 43.98 1.955 37.526 4.432 31.65 Z", + + "flower" to + "M50,0 C60.6,0 69.9,5.3 75.6,13.5 78.5,17.8 82.3,21.5 86.6,24.5 " + + "94.7,30.1 100,39.4 100,50 100,60.6 94.7,69.9 86.5,75.6 " + + "82.2,78.5 78.5,82.3 75.5,86.6 69.9,94.7 60.6,100 50,100 " + + "39.4,100 30.1,94.7 24.4,86.5 21.5,82.2 17.7,78.5 13.4,75.5 " + + "5.3,69.9 0,60.6 0,50 0,39.4 5.3,30.1 13.5,24.4 " + + "17.8,21.5 21.5,17.7 24.5,13.4 30.1,5.3 39.4,0 50,0 Z", + + "heart" to + "M50,20 C45,0 30,0 25,0 20,0 0,5 0,34 0,72 40,97 50,100 " + + "60,97 100,72 100,34 100,5 80,0 75,0 70,0 55,0 50,20 Z", + + "hexagon" to + "M12,0 88,0 100,50 88,100 12,100 0,50 12,0 Z", + + "leaf" to + "M-0.06,0.07h67.37C85.36,0.07,100,14.71,100,32.76v67.37" + + "H32.63c-18.06,0-32.69-14.64-32.69-32.69L-0.06,0.07z", + + "cloudy" to + "M4,50 C2,45 0,39 0,33 C0,15 15,0 33,0 C39,0 45,2 50,4 " + + "C55,2 61,0 67,0 C85,0 100,15 100,33 C100,39 98,45 96,50 " + + "C98,55 100,61 100,66 C100,85 85,100 66,100 C61,100 55,98 50,96 " + + "C45,98 39,100 33,100 C15,100 0,85 0,66 C0,61 2,55 3,50 Z", + + "cylindrical" to + "M50,0A50,30 0,0,1 100,30V70A50,30 0,0,1 0,70V30A50,30 0,0,1 50,0z", + + "stretched" to + "M100,50 C100,77 77,100 50,100 L10,100 C4,100 0,96 0,90 L0,50 " + + "C0,22 22,0 50,0 L90,0 C96,0 100,4 100,10 L100,50 Z", + + "pebble" to + "M55,0 C25,0 0,25 0,50 0,78 28,100 55,100 85,100 100,85 100,58 " + + "100,30 86,0 55,0 Z", + + "rice_balls" to + "M46.16,0.43C45.23,0.49,41.86,0.75,40.93,0.84C40.44,0.88,39.61,0.98,39.07,1.06C38.54,1.14,37.83,1.25,37.5,1.3C36.88,1.39,36.21,1.51,35.42,1.65C34.76,1.77,33.25,2.12,32.91,2.22C32.75,2.27,32.55,2.32,32.47,2.32C32.38,2.32,32.24,2.35,32.15,2.4C32.06,2.45,31.67,2.55,31.27,2.64C30.88,2.73,30.53,2.84,30.5,2.88C30.48,2.93,30.35,2.96,30.23,2.96C30.1,2.96,29.93,3,29.84,3.04C29.75,3.09,29.43,3.2,29.12,3.29C28.82,3.38,28.18,3.59,27.71,3.76C27.23,3.93,26.8,4.07,26.75,4.07C26.69,4.07,26.52,4.13,26.36,4.2C25.52,4.57,24.84,4.82,24.7,4.82C24.62,4.82,24.52,4.86,24.49,4.91C24.46,4.96,24.35,5,24.25,5C24.15,5,24.04,5.06,24.02,5.14C23.98,5.21,23.86,5.28,23.74,5.28C23.62,5.28,23.52,5.32,23.52,5.37C23.52,5.42,23.43,5.46,23.33,5.46C23.23,5.46,23.12,5.53,23.09,5.6C23.06,5.68,22.97,5.74,22.88,5.75C22.8,5.75,22.61,5.83,22.45,5.93C22.3,6.02,22.03,6.18,21.85,6.27C21.67,6.35,21.51,6.45,21.48,6.48C21.46,6.5,21.27,6.61,21.07,6.71C20.86,6.81,20.45,7.04,20.14,7.22C19.83,7.4,19.43,7.62,19.23,7.72C19.04,7.82,18.89,7.93,18.89,7.98C18.89,8.02,18.84,8.05,18.78,8.05C18.63,8.05,17.5,8.8,17.5,8.9C17.5,8.95,17.45,8.98,17.4,8.98C17.34,8.98,17.16,9.1,16.99,9.23C16.82,9.38,16.43,9.7,16.1,9.95C15.77,10.21,15.43,10.49,15.32,10.59C15.22,10.68,14.9,10.95,14.61,11.17C14.31,11.4,14.07,11.62,14.07,11.66C14.07,11.7,13.97,11.79,13.84,11.85C13.71,11.92,13.61,12,13.61,12.04C13.61,12.08,13.14,12.58,12.57,13.16C11.99,13.74,11.41,14.36,11.28,14.54C11.14,14.71,10.92,14.99,10.77,15.14C10.63,15.3,10.41,15.58,10.28,15.77C10.15,15.96,10.02,16.11,9.98,16.11C9.94,16.11,9.91,16.17,9.91,16.25C9.91,16.33,9.88,16.39,9.84,16.39C9.79,16.39,9.69,16.5,9.61,16.64C9.52,16.79,9.36,17,9.24,17.13C9.12,17.26,8.9,17.56,8.75,17.8C8.6,18.04,8.45,18.24,8.41,18.24C8.38,18.24,8.33,18.34,8.3,18.47C8.26,18.6,8.2,18.7,8.15,18.7C8.1,18.7,8.05,18.74,8.05,18.79C8.05,18.84,7.94,19.06,7.79,19.28C7.64,19.5,7.37,19.97,7.18,20.32C6.98,20.68,6.73,21.12,6.62,21.3C6.51,21.47,6.34,21.8,6.25,22.02C6.15,22.23,6.04,22.41,6,22.41C5.96,22.41,5.93,22.48,5.93,22.56C5.93,22.64,5.83,22.84,5.71,23C5.6,23.16,5.46,23.39,5.4,23.52C5.34,23.64,5.19,23.98,5.06,24.26C4.93,24.54,4.76,24.98,4.67,25.23C4.58,25.48,4.47,25.73,4.43,25.78C4.39,25.83,4.35,25.96,4.35,26.07C4.35,26.17,4.32,26.3,4.27,26.35C4.23,26.39,4.12,26.68,4.02,26.99C3.93,27.3,3.83,27.59,3.79,27.64C3.76,27.69,3.66,27.96,3.57,28.24C3.47,28.52,3.34,28.91,3.27,29.1C3.2,29.29,3.15,29.53,3.15,29.63C3.15,29.73,3.11,29.82,3.05,29.82C3,29.82,2.96,29.94,2.96,30.09C2.96,30.24,2.92,30.38,2.88,30.41C2.83,30.44,2.76,30.65,2.73,30.88C2.69,31.11,2.63,31.32,2.58,31.35C2.54,31.38,2.5,31.54,2.5,31.7C2.5,31.86,2.46,32.04,2.43,32.09C2.38,32.14,2.3,32.41,2.23,32.68C2.17,32.96,2.08,33.36,2.04,33.57C1.87,34.28,1.68,35.14,1.58,35.67C1.35,36.88,1.29,37.23,1.16,38.24C1.07,38.83,0.97,39.52,0.93,39.77C0.88,40.02,0.82,40.54,0.79,40.93C0.77,41.31,0.68,42.45,0.59,43.47C0.35,46.14,0.35,53.59,0.59,56.3C0.68,57.34,0.79,58.59,0.83,59.07C0.88,59.56,0.98,60.39,1.06,60.93C1.14,61.46,1.25,62.17,1.3,62.5C1.43,63.48,1.67,64.74,1.89,65.7C1.97,66.05,2.08,66.53,2.13,66.76C2.18,66.99,2.26,67.32,2.31,67.5C2.36,67.68,2.48,68.18,2.59,68.61C2.7,69.04,2.83,69.44,2.88,69.48C2.93,69.54,2.96,69.66,2.96,69.75C2.96,69.95,3.29,70.98,3.76,72.29C3.93,72.77,4.07,73.2,4.07,73.25C4.07,73.31,4.13,73.48,4.2,73.64C4.28,73.8,4.41,74.14,4.5,74.4C4.59,74.65,4.7,74.9,4.74,74.95C4.78,75,4.82,75.13,4.82,75.25C4.82,75.36,4.86,75.48,4.91,75.51C4.96,75.54,5,75.65,5,75.75C5,75.85,5.06,75.96,5.14,75.98C5.21,76.02,5.28,76.14,5.28,76.26C5.28,76.38,5.32,76.48,5.37,76.48C5.42,76.48,5.46,76.57,5.46,76.67C5.46,76.77,5.53,76.88,5.6,76.91C5.68,76.94,5.74,77.03,5.75,77.12C5.75,77.2,5.83,77.39,5.93,77.55C6.02,77.7,6.2,78.01,6.32,78.24C6.65,78.88,7.13,79.73,7.38,80.11C7.5,80.3,7.59,80.49,7.59,80.52C7.59,80.65,8.81,82.5,8.9,82.5C8.94,82.5,8.98,82.55,8.98,82.62C8.98,82.69,9.1,82.88,9.23,83.04C9.38,83.2,9.7,83.59,9.95,83.91C10.21,84.23,10.43,84.51,10.46,84.54C10.49,84.56,10.7,84.81,10.92,85.09C11.69,86.05,13.74,88.11,14.67,88.84C14.86,89,15.11,89.2,15.22,89.3C15.34,89.41,15.58,89.59,15.77,89.72C15.96,89.85,16.11,89.98,16.11,90.02C16.11,90.06,16.17,90.09,16.25,90.09C16.33,90.09,16.39,90.13,16.39,90.18C16.39,90.23,16.5,90.33,16.64,90.41C16.79,90.48,17,90.64,17.14,90.77C17.27,90.89,17.56,91.11,17.78,91.25C18.01,91.39,18.25,91.57,18.32,91.63C18.38,91.7,18.5,91.76,18.56,91.76C18.63,91.76,18.7,91.79,18.72,91.84C18.77,91.95,21.25,93.39,22.73,94.16C22.93,94.27,23.23,94.44,23.38,94.54C23.53,94.63,23.73,94.71,23.82,94.71C23.91,94.72,23.98,94.76,23.98,94.81C23.98,94.86,24.07,94.92,24.19,94.95C24.3,94.98,24.48,95.05,24.58,95.09C24.68,95.14,24.98,95.24,25.23,95.33C25.48,95.42,25.73,95.53,25.78,95.57C25.83,95.61,25.94,95.65,26.03,95.65C26.12,95.65,26.31,95.71,26.45,95.78C26.71,95.91,27.16,96.07,28.2,96.41C28.5,96.51,28.89,96.65,29.05,96.72C29.22,96.79,29.44,96.85,29.54,96.85C29.63,96.85,29.74,96.89,29.77,96.95C29.8,97,29.93,97.04,30.05,97.04C30.18,97.04,30.35,97.07,30.44,97.12C30.71,97.27,31.55,97.5,31.79,97.5C31.92,97.5,32.05,97.54,32.08,97.58C32.11,97.63,32.24,97.68,32.38,97.71C32.52,97.73,33.22,97.88,33.93,98.04C34.65,98.2,35.45,98.38,35.72,98.43C36.69,98.61,37.18,98.7,37.55,98.75C37.75,98.78,38.33,98.87,38.84,98.95C40.09,99.13,40.54,99.18,44.49,99.5C46.86,99.69,53.84,99.64,56.48,99.41C59.71,99.12,60.04,99.09,61.02,98.94C61.55,98.86,62.26,98.75,62.59,98.7C63.23,98.6,63.91,98.49,64.68,98.35C65.14,98.26,67,97.83,67.59,97.67C67.77,97.62,68.06,97.55,68.24,97.5C69.02,97.33,69.45,97.2,69.5,97.12C69.53,97.07,69.64,97.04,69.75,97.04C69.94,97.04,70.27,96.95,70.93,96.71C71.43,96.54,72.4,96.21,72.92,96.05C73.17,95.98,73.5,95.86,73.66,95.79C73.81,95.72,74.14,95.59,74.4,95.5C74.65,95.41,74.9,95.3,74.95,95.26C75,95.22,75.15,95.18,75.3,95.18C75.44,95.18,75.55,95.14,75.55,95.09C75.55,95.04,75.63,95,75.73,95C75.82,95,75.95,94.94,76.02,94.86C76.08,94.79,76.21,94.72,76.31,94.72C76.4,94.72,76.48,94.68,76.48,94.63C76.48,94.58,76.57,94.54,76.67,94.54C76.77,94.54,76.88,94.47,76.91,94.4C76.94,94.32,77.02,94.26,77.08,94.26C77.2,94.26,77.79,93.99,77.87,93.89C77.89,93.87,78.06,93.77,78.24,93.68C78.42,93.6,78.86,93.36,79.21,93.16C79.57,92.96,80.07,92.68,80.32,92.54C81.04,92.16,83.15,90.73,83.15,90.64C83.15,90.59,83.21,90.55,83.28,90.55C83.35,90.55,83.43,90.52,83.44,90.48C83.46,90.44,83.66,90.27,83.89,90.09C84.11,89.91,84.35,89.73,84.41,89.68C84.46,89.62,84.67,89.46,84.86,89.3C85.94,88.45,88.68,85.68,89.05,85.07C89.14,84.93,89.23,84.82,89.27,84.82C89.3,84.82,89.46,84.61,89.64,84.35C89.81,84.1,89.98,83.89,90.02,83.89C90.06,83.89,90.09,83.85,90.09,83.8C90.09,83.7,90.18,83.59,90.61,83.11C90.73,82.97,90.83,82.82,90.83,82.77C90.83,82.73,90.87,82.68,90.91,82.68C90.95,82.68,91.07,82.54,91.16,82.36C91.26,82.18,91.37,82.04,91.41,82.04C91.45,82.04,91.48,81.98,91.48,81.91C91.48,81.83,91.52,81.76,91.56,81.74C91.63,81.71,92.51,80.27,93.15,79.12C93.77,78,93.98,77.63,94.16,77.27C94.27,77.07,94.44,76.77,94.54,76.62C94.63,76.47,94.71,76.27,94.71,76.18C94.72,76.09,94.76,76.02,94.81,76.02C94.86,76.02,94.92,75.93,94.95,75.81C94.98,75.7,95.05,75.52,95.09,75.42C95.13,75.32,95.23,75.04,95.32,74.82C95.41,74.59,95.52,74.32,95.56,74.21C95.6,74.11,95.7,73.88,95.78,73.69C95.86,73.5,95.93,73.27,95.93,73.17C95.93,73.07,95.95,72.96,95.99,72.93C96.06,72.86,96.24,72.39,96.45,71.71C96.53,71.46,96.65,71.11,96.72,70.95C96.79,70.78,96.85,70.56,96.85,70.46C96.85,70.37,96.89,70.26,96.95,70.23C97,70.2,97.04,70.06,97.04,69.92C97.04,69.78,97.08,69.63,97.12,69.58C97.22,69.48,97.4,68.82,97.47,68.31C97.5,68.12,97.56,67.96,97.6,67.96C97.65,67.96,97.68,67.87,97.68,67.76C97.68,67.65,97.73,67.43,97.78,67.27C97.88,66.94,98.23,65.42,98.35,64.77C98.56,63.59,98.64,63.12,98.93,61.25C99.12,60.05,99.16,59.65,99.5,55.51C99.69,53.18,99.63,46.08,99.41,43.52C99.32,42.58,99.21,41.39,99.17,40.88C99.12,40.37,99.02,39.52,98.93,38.98C98.85,38.45,98.75,37.76,98.7,37.45C98.62,36.94,98.54,36.44,98.38,35.51C98.35,35.3,98.21,34.7,98.09,34.17C97.96,33.63,97.82,33.01,97.77,32.78C97.71,32.55,97.63,32.28,97.59,32.18C97.55,32.07,97.45,31.67,97.36,31.27C97.27,30.88,97.16,30.53,97.12,30.5C97.07,30.48,97.04,30.35,97.04,30.23C97.04,30.1,97,29.93,96.96,29.84C96.91,29.75,96.8,29.43,96.71,29.12C96.62,28.82,96.47,28.34,96.37,28.05C96.27,27.78,96.13,27.34,96.05,27.08C95.97,26.83,95.86,26.5,95.79,26.34C95.72,26.19,95.59,25.86,95.5,25.6C95.41,25.35,95.3,25.1,95.26,25.05C95.22,25,95.18,24.87,95.18,24.75C95.18,24.64,95.14,24.52,95.09,24.49C95.04,24.46,95,24.36,95,24.27C95,24.17,94.94,24.05,94.86,23.98C94.79,23.92,94.72,23.79,94.72,23.69C94.72,23.6,94.68,23.52,94.63,23.52C94.58,23.52,94.54,23.43,94.54,23.33C94.54,23.23,94.47,23.12,94.4,23.09C94.32,23.06,94.26,22.97,94.25,22.88C94.25,22.8,94.17,22.61,94.07,22.45C93.98,22.3,93.8,21.99,93.68,21.76C93.56,21.53,93.34,21.13,93.2,20.88C93.06,20.62,92.79,20.14,92.61,19.82C92.11,18.91,91.2,17.5,91.1,17.5C91.05,17.5,91.02,17.45,91.02,17.38C91.02,17.31,90.9,17.12,90.77,16.96C90.62,16.8,90.3,16.41,90.05,16.09C89.79,15.77,89.56,15.49,89.53,15.46C89.5,15.44,89.28,15.17,89.03,14.86C88.24,13.88,86.24,11.87,85.46,11.28C85.29,11.14,85.01,10.92,84.86,10.77C84.7,10.63,84.42,10.41,84.23,10.28C84.04,10.15,83.89,10.02,83.89,9.98C83.89,9.94,83.83,9.91,83.75,9.91C83.67,9.91,83.61,9.88,83.61,9.84C83.61,9.79,83.5,9.69,83.36,9.61C83.21,9.52,82.96,9.32,82.79,9.17C82.61,9.01,82.31,8.79,82.11,8.68C81.92,8.57,81.76,8.45,81.76,8.42C81.76,8.38,81.66,8.33,81.53,8.3C81.4,8.26,81.3,8.2,81.3,8.15C81.3,8.1,81.26,8.05,81.21,8.05C81.16,8.05,80.94,7.94,80.72,7.79C80.5,7.64,80.03,7.37,79.68,7.18C79.32,6.98,78.88,6.73,78.7,6.62C78.53,6.52,78.23,6.35,78.05,6.26C77.88,6.17,77.52,5.98,77.27,5.84C77.02,5.69,76.7,5.52,76.57,5.46C76.45,5.41,76.09,5.24,75.79,5.09C75.48,4.95,75.02,4.76,74.77,4.67C74.52,4.58,74.27,4.47,74.22,4.43C74.17,4.39,74.04,4.35,73.92,4.35C73.81,4.35,73.69,4.32,73.66,4.27C73.64,4.22,73.5,4.16,73.36,4.12C73.21,4.09,72.73,3.93,72.27,3.76C71.81,3.59,71.18,3.37,70.88,3.28C70.57,3.19,70.29,3.08,70.23,3.04C70.19,3,70.01,2.96,69.84,2.96C69.68,2.96,69.54,2.93,69.54,2.89C69.54,2.81,68.93,2.62,68.38,2.54C68.23,2.51,68.02,2.45,67.92,2.41C67.73,2.33,67.45,2.26,66.25,1.99C65.89,1.91,65.43,1.81,65.23,1.76C64.46,1.59,63.92,1.49,62.68,1.3C62.36,1.25,61.67,1.14,61.16,1.07C60.15,0.91,59.33,0.82,56.57,0.6C54.86,0.46,47.56,0.34,46.16,0.43", + + "rounded_rect" to + "M50,0L88,0 C94.4,0 100,5.4 100 12 L100,88 C100,94.6 94.6 100 88 100 " + + "L12,100 C5.4,100 0,94.6 0,88 L0 12 C0 5.4 5.4 0 12 0 L50,0 Z", + + "squircle" to + "M41.8,0h16.4C72.1,0 77.6,1.5 83,4.4c5.4,2.9 9.7,7.2 12.6,12.6" + + "c2.9,5.4 4.4,11 4.4,24.7v16.4c0,13.7 -1.5,19.3 -4.4,24.7" + + "c-2.9,5.4 -7.2,9.7 -12.6,12.6c-5.4,2.9 -11,4.4 -24.7,4.4H41.8" + + "c-13.8,0 -19.3,-1.5 -24.7,-4.4c-5.4,-2.9 -9.7,-7.1 -12.7,-12.5" + + "S0,72 0,58.3V41.9c0,-13.8 1.5,-19.3 4.4,-24.7S11.6,7.4 17,4.5" + + "S28.1,0 41.8,0z", + + "tapered_rect" to + "M20,0 80,0 100,20 100,80 80,100 20,100 0,80 0,20 20,0 Z", + + "teardrop" to + "M50,0 C77.6,0 100,22.4 100,50 L100,88 C100,94.6 94.6,100 88,100 " + + "L50,100 C22.4 100 0 77.6 0 50C0 22.4 22.4 0 50 0 Z", + + "vessel" to + "M12.97,0 C8.41,0 4.14,2.55 2.21,6.68 -1.03,13.61 -0.71,21.78 " + + "3.16,28.46 4.89,31.46 4.89,35.2 3.16,38.2 -1.05,45.48 -1.05,54.52 " + + "3.16,61.8 4.89,64.8 4.89,68.54 3.16,71.54 -0.71,78.22 -1.03,86.39 " + + "2.21,93.32 4.14,97.45 8.41,100 12.97,100 21.38,100 78.62,100 87.03,100 " + + "91.59,100 95.85,97.45 97.79,93.32 101.02,86.39 100.71,78.22 96.84,71.54 " + + "95.1,68.54 95.1,64.8 96.84,61.8 101.05,54.52 101.05,45.48 96.84,38.2 " + + "95.1,35.2 95.1,31.46 96.84,28.46 100.71,21.78 101.02,13.61 97.79,6.68 " + + "95.85,2.55 91.59,0 87.03,0 78.62,0 21.38,0 12.97,0 Z", + + "rounded_hexagon" to + "M4.8 33V67c0 5.8 3 11 8 13.7l29.4 17c4.9 2.7 11 2.7 15.9 0l29.4 -17" + + "c4.9 -2.7 8 -8 8 -13.7V33c0 -5.8 -3 -11 -8 -13.7l-29.4 -17" + + "c-4.9 -2.7 -11 -2.7 -15.9 0l-29.7 17C7.8 22.2 4.8 27.5 4.8 33z", + ) + + val ALL_KEYS: List = PATH_BY_KEY.keys.toList() + + private val shapeCache = LinkedHashMap() + + fun isKnownKey(key: String): Boolean = PATH_BY_KEY.containsKey(key) + + fun pathForKey(key: String): String = + PATH_BY_KEY[key] ?: PATH_BY_KEY[DEFAULT_KEY]!! + + fun shapeForKey(key: String): Shape { + shapeCache[key]?.let { return it } + + val pathData = pathForKey(key) + val shape = PathShape(pathData) + shapeCache[key] = shape + return shape + } + + private class PathShape(pathData: String) : Shape { + private val basePath: android.graphics.Path = try { + PathParser.createPathFromPathData(pathData) + } catch (_: RuntimeException) { + PathParser.createPathFromPathData(PATH_BY_KEY[DEFAULT_KEY]!!) + } + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + val scaled = android.graphics.Path(basePath) + val matrix = Matrix() + matrix.setScale(size.width / 100f, size.height / 100f) + scaled.transform(matrix) + return Outline.Generic(scaled.asComposePath()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 6a86bd88c455..3297411e2507 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -127,6 +127,8 @@ import com.android.systemui.qs.ui.composable.QuickSettingsShade import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow @Composable fun TileLazyGrid( @@ -221,15 +223,26 @@ fun ContentScope.Tile( null } + val classicStyle = rememberQSPanelStyle() + if (tile.spec.spec == "sound" && !iconOnly) { + if (classicStyle) return@trace QSTileRingerSlider() return@trace } + val iconShapeKey = rememberQSTileIconShapeKey() + val labelHide = classicStyle && rememberQSTileLabelHide() + val tileHeight = if (!classicStyle || labelHide) { + CommonTileDefaults.TileHeight + } else { + CommonTileDefaults.TileHeight + 8.dp + } + val shapeMode = if (useMinimalStyle) 0 else rememberTileShapeMode() val wantCircle = !useMinimalStyle && shapeMode == 4 && iconOnly val tileShape = - if (wantCircle) { + if (wantCircle && !classicStyle) { CircleShape } else if (useMinimalStyle) { val useMinimalInvert = rememberAxMinimalInvert() @@ -255,8 +268,8 @@ fun ContentScope.Tile( contentRevealModifier = Modifier } - val outerShape = if (wantCircle) RoundedCornerShape(0.dp) else tileShape - val outerColor: () -> Color = if (wantCircle) { { Color.Transparent } } else { { animatedColor } } + val outerShape = if (wantCircle && !classicStyle) RoundedCornerShape(0.dp) else tileShape + val outerColor: () -> Color = if (wantCircle || classicStyle) { { Color.Transparent } } else { { animatedColor } } val focusBorderColor = MaterialTheme.colorScheme.secondary TileExpandable( @@ -264,6 +277,7 @@ fun ContentScope.Tile( shape = outerShape, squishiness = squishiness, hapticsViewModel = hapticsViewModel, + classicStyle = classicStyle, modifier = modifier .then(surfaceRevealModifier) @@ -271,7 +285,7 @@ fun ContentScope.Tile( Modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) } .fillMaxWidth() - .height(CommonTileDefaults.TileHeight) + .height(tileHeight) .thenIf(currentBounceableInfo != null) { Modifier.bounceable( currentBounceableInfo!!.bounceable, @@ -345,22 +359,25 @@ fun ContentScope.Tile( } } - if (wantCircle) { + if (wantCircle || classicStyle) { val interaction = remember { MutableInteractionSource() } Box(Modifier.fillMaxSize()) { Box( modifier = Modifier - .size(CommonTileDefaults.TileHeight) + .size(tileHeight) .align(Alignment.Center) - .clip(CircleShape) - .drawBehind { - val brush = colors.iconBackgroundGradient - if (brush != null) { - drawRect(brush = brush) - } else { - drawRect(color = animatedColor) - } + .thenIf(!classicStyle) { + Modifier + .clip(CircleShape) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + } } .indication(interaction, LocalIndication.current) .tileCombinedClickable( @@ -374,14 +391,28 @@ fun ContentScope.Tile( .tileTestTag(iconOnly), ) { val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } - SmallTileContent( - iconProvider = iconProvider, - color = colors.icon, - modifier = - Modifier.align(Alignment.Center).bounceScale { - contentBounceable.iconBounceScale - }, - ) + if (!classicStyle) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + modifier = + Modifier.align(Alignment.Center).bounceScale { + contentBounceable.iconBounceScale + }, + ) + } else { + ClassicTileContent( + label = uiState.label, + iconProvider = iconProvider, + iconShapeKey = iconShapeKey, + colors = colors, + labelHide = labelHide, + modifier = + Modifier.align(Alignment.Center).bounceScale { + contentBounceable.iconBounceScale + }, + ) + } } } } else { @@ -392,6 +423,7 @@ fun ContentScope.Tile( accessibilityUiState = uiState.accessibilityUiState, iconOnly = iconOnly, isDualTarget = isDualTarget, + classicStyle = classicStyle, modifier = contentRevealModifier, colors = colors, ) { @@ -464,12 +496,18 @@ private fun TileExpandable( shape: Shape, squishiness: () -> Float, hapticsViewModel: TileHapticsViewModel?, + classicStyle: Boolean, modifier: Modifier = Modifier, content: @Composable (Expandable) -> Unit, ) { Expandable( controller = rememberExpandableController(color = color, shape = shape), - modifier = modifier.clip(shape).verticalSquish(squishiness), + modifier = modifier + .verticalSquish(squishiness) + .thenIf(!classicStyle) { + Modifier + .clip(shape) + }, useModifierBasedImplementation = true, ) { content(hapticsViewModel?.createStateAwareExpandable(it) ?: it) @@ -483,6 +521,7 @@ fun TileContainer( accessibilityUiState: AccessibilityUiState, iconOnly: Boolean, isDualTarget: Boolean, + classicStyle: Boolean, interactionSource: MutableInteractionSource?, modifier: Modifier = Modifier, colors: TileColors, @@ -502,7 +541,7 @@ fun TileContainer( interactionSource = interactionSource, ) .tileTestTag(iconOnly) - .thenIf(!isDualTarget || iconOnly) { + .thenIf(!classicStyle && (!isDualTarget || iconOnly)) { Modifier .drawBehind { val brush = colors.iconBackgroundGradient @@ -891,6 +930,126 @@ private fun rememberGradientCustomColors(): Pair { return start to end } +@Composable +fun rememberQSPanelStyle(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readPanelStyleEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_PANEL_STYLE, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var classicStyleEnabled by remember { mutableStateOf(readPanelStyleEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + classicStyleEnabled = readPanelStyleEnabled() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_PANEL_STYLE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return classicStyleEnabled +} + +@Composable +fun rememberQSTileLabelHide(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readLabelHideEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_LABEL_HIDE, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var labelHideEnabled by remember { mutableStateOf(readLabelHideEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + labelHideEnabled = readLabelHideEnabled() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_LABEL_HIDE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return labelHideEnabled +} + +@Composable +fun rememberQSTileIconShapeKey(): String { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readValue(): String { + return try { + Settings.System.getStringForUser( + contentResolver, Settings.System.QS_TILE_ICON_SHAPE, + UserHandle.USER_CURRENT + ) ?: "circle" + } catch (_: Throwable) { + "circle" + } + } + + var value by remember { mutableStateOf(readValue()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + value = readValue() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_ICON_SHAPE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return value +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt index 8cf7803f7227..9e061232d264 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import androidx.compose.runtime.getValue import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS @@ -58,8 +59,11 @@ constructor( ) override fun splitIntoPages(tiles: List, rows: Int): List> { + val largeTilesSpan = columnsWithMediaViewModel.largeSpan + val largeTiles by iconTilesViewModel.largeTilesState + return splitInRows( - tiles.map { SizedTileImpl(it, widthOf(it.spec)) }, + tiles.map { SizedTileImpl(it, if (largeTiles.contains(it.spec)) largeTilesSpan else 1) }, columnsWithMediaViewModel.columns, ) .chunked(rows) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt index 4f6057409f65..756399b77e41 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt @@ -71,9 +71,18 @@ constructor( initialValue = false, ) + private val classicStyle by + hydrator.hydratedStateOf( + traceName = "classicStyle", + source = largeTileSpanInteractor.classicStyle, + initialValue = false, + ) + val largeSpan: Int get() = - if (useExtraLargeTiles) { + if (classicStyle) { + 1 + } else if (useExtraLargeTiles) { if (columns > maxSpan) columns / 2 else columns } else { largeTileSpanInteractor.defaultTileMaxWidth From c4517572cfe654d6983686977f2c284b0a9496b0 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 26 Mar 2026 10:33:17 +0000 Subject: [PATCH 1163/1315] SystemUI: Add gradient in ringer tile Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../common/ringer/RingerSliderInterfaces.kt | 5 +++++ .../systemui/common/ringer/RingerSliderWidget.kt | 14 ++++++++++++-- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 8 +++++++- .../qs/tiles/impl/ringer/QSTileRingerDefaults.kt | 6 ++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt index ab25fa25115e..01e5b193c4d2 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt @@ -17,12 +17,17 @@ package com.android.systemui.common.ringer import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.Dp interface RingerSliderTheme { @get:Composable val activeBg: Color + @get:Composable + val activeBgBrush: Brush? + get() = null + @get:Composable val neutralBg: Color diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt index 5f40ab67868f..c8f662039279 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt @@ -176,10 +176,20 @@ fun RingerSliderWidget( when { isDozing -> Color.Transparent isDndEnabled -> theme.dndBg - else -> theme.activeBg + else -> Color.Transparent }, thumbShape ) + .then( + if (!isDozing && !isDndEnabled) { + val brush = theme.activeBgBrush + if (brush != null) { + Modifier.background(brush, thumbShape) + } else { + Modifier.background(theme.activeBg, thumbShape) + } + } else Modifier + ) .then( when { isDozing -> @@ -187,7 +197,7 @@ fun RingerSliderWidget( isDndEnabled -> Modifier.border(2.dp, theme.dndBg, thumbShape) else -> - Modifier.border(2.dp, theme.activeBg, thumbShape) + Modifier.border(2.dp, theme.activeBgBrush ?: SolidColor(theme.activeBg), thumbShape) } ), contentAlignment = Alignment.Center diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 3297411e2507..2629ecb2bc1a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -224,7 +224,7 @@ fun ContentScope.Tile( } val classicStyle = rememberQSPanelStyle() - + if (tile.spec.spec == "sound" && !iconOnly) { if (classicStyle) return@trace QSTileRingerSlider() @@ -1050,6 +1050,12 @@ fun rememberQSTileIconShapeKey(): String { return value } +@Composable + internal fun rememberQsTileBackgroundBrush(): Brush? { + val enabled = rememberQsGradient() + return TileDefaults.qsTileBackgroundBrush(enabled) + } + private object TileDefaults { val ActiveIconCornerRadius = 16.dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt index 15f1baa7a8c8..413794cf5a2a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt @@ -18,17 +18,23 @@ package com.android.systemui.qs.tiles.impl.ringer import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.common.ringer.RingerSliderDimens import com.android.systemui.common.ringer.RingerSliderTheme import com.android.systemui.qs.panels.ui.compose.infinitegrid.CustomColorScheme +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQsGradient +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQsTileBackgroundBrush class QSTileRingerTheme( ) : RingerSliderTheme { override val activeBg: Color @Composable get() = MaterialTheme.colorScheme.primary + + override val activeBgBrush: Brush? + @Composable get() = rememberQsTileBackgroundBrush() override val neutralBg: Color @Composable get() = CustomColorScheme.current.qsTileColor From 75a6669b01d911c195fa5c723cbfd42074e8b6d3 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 28 Mar 2026 15:28:31 +0530 Subject: [PATCH 1164/1315] SystemUI: Fix classic tile specs in some instances * Treat classic tiles as icon only so that we apply color and bounceable animation correctly. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/qs/panels/ui/compose/QuickQuickSettings.kt | 6 ++++-- .../qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt | 6 ++++-- .../systemui/qs/panels/ui/compose/selection/Selection.kt | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index ab97fe28dc51..ad86eecc8f4c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -36,6 +36,7 @@ import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQSPanelStyle import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel import com.android.systemui.qs.shared.ui.QuickSettings.Elements.toElementKey @@ -57,6 +58,7 @@ fun ContentScope.QuickQuickSettings( val useModifiedSpacing = remember { Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 } + val classicStyle = rememberQSPanelStyle() Box(modifier = modifier) { GridAnchor() @@ -72,7 +74,7 @@ fun ContentScope.QuickQuickSettings( ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, - iconOnly = sizedTile.isIcon, + iconOnly = classicStyle || sizedTile.isIcon, squishiness = { squishiness }, coroutineScope = scope, tileHapticsViewModelFactoryProvider = @@ -111,7 +113,7 @@ fun ContentScope.QuickQuickSettings( Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, - iconOnly = it.isIcon, + iconOnly = classicStyle || it.isIcon, squishiness = { squishiness }, coroutineScope = scope, bounceableInfo = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 52ad368b4118..9d62b87cd69e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -103,6 +103,8 @@ constructor( val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + val classicStyle = rememberQSPanelStyle() + if (QSMaterialExpressiveTiles.isEnabled) { ButtonGroupGrid( sizedTiles = sizedTiles, @@ -114,7 +116,7 @@ constructor( ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, - iconOnly = iconTilesViewModel.isIconTile(sizedTile.tile.spec), + iconOnly = classicStyle || iconTilesViewModel.isIconTile(sizedTile.tile.spec), squishiness = { squishiness }, tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, coroutineScope = scope, @@ -154,7 +156,7 @@ constructor( Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, - iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), + iconOnly = classicStyle || iconTilesViewModel.isIconTile(it.tile.spec), squishiness = { squishiness }, tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, coroutineScope = scope, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 801fa29fe830..3df79a6ac3d9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -76,6 +76,7 @@ import androidx.compose.ui.zIndex import com.android.compose.modifiers.thenIf import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.ActiveTileCornerRadius import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQSPanelStyle import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberTileShapeMode import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize @@ -198,6 +199,7 @@ private fun Modifier.selectionBorder( selectionAlpha: () -> Float = { 0f } ): Modifier { val shapeMode = rememberTileShapeMode() + val panelStyle = rememberQSPanelStyle() val borderRadiusPx = with(LocalDensity.current) { when (shapeMode) { 1 -> InactiveCornerRadius.toPx() @@ -214,7 +216,7 @@ private fun Modifier.selectionBorder( val borderWidthPx = selectionBorderWidth.toPx() val alpha = selectionAlpha() - if (shapeMode == 4 && iconOnly) { + if (panelStyle || (shapeMode == 4 && iconOnly)) { val w = this.size.width val h = this.size.height val radius = (min(w, h) - borderWidthPx) / 2f From 9fad38464fcc30e623ca3379fa23c7f38f4a4023 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 31 Mar 2026 10:03:27 +0000 Subject: [PATCH 1165/1315] SystemUI: Improve dual tone qs fallback color Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 02e9a29a42fe..ba8ba3c73951 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -47,7 +47,7 @@ @android:color/system_neutral1_100 @android:color/system_neutral1_200 @android:color/system_surface_dim_light - #00000000 + @android:color/system_neutral1_50 #F5F5F5 From 99728c4b2c88de91accfdec48d47ed0cd04118bd Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 3 Apr 2026 09:26:54 +0000 Subject: [PATCH 1166/1315] SystemUI: Fix QS classic tile style jitter during panel expansion Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/infinitegrid/CommonTile.kt | 16 +++----- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 39 ++++++++++--------- .../panels/ui/viewmodel/QSColumnsViewModel.kt | 18 ++++----- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 2beb83f8f855..05dbe71ead0a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -136,16 +136,12 @@ fun ClassicTileContent( val animatedColor by animateColorAsState(colors.background, label = "QSTileCircleBgColor") - val tileHeight = if (labelHide) { - CommonTileDefaults.TileHeight - } else { - CommonTileDefaults.TileHeight - 8.dp - } - - val iconSize = if (labelHide) { - CommonTileDefaults.IconSize - } else { - CommonTileDefaults.LargeTileIconSize + val (tileHeight, iconSize) = remember(labelHide) { + if (labelHide) { + CommonTileDefaults.TileHeight to CommonTileDefaults.IconSize + } else { + (CommonTileDefaults.TileHeight - 8.dp) to CommonTileDefaults.LargeTileIconSize + } } Column( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 2629ecb2bc1a..bf065d73a77c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -45,6 +45,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyGridState @@ -233,10 +234,12 @@ fun ContentScope.Tile( val iconShapeKey = rememberQSTileIconShapeKey() val labelHide = classicStyle && rememberQSTileLabelHide() - val tileHeight = if (!classicStyle || labelHide) { - CommonTileDefaults.TileHeight - } else { - CommonTileDefaults.TileHeight + 8.dp + val tileHeight = remember(classicStyle, labelHide) { + if (!classicStyle || labelHide) { + CommonTileDefaults.TileHeight + } else { + CommonTileDefaults.TileHeight + 8.dp + } } val shapeMode = if (useMinimalStyle) 0 else rememberTileShapeMode() @@ -362,11 +365,15 @@ fun ContentScope.Tile( if (wantCircle || classicStyle) { val interaction = remember { MutableInteractionSource() } - Box(Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), + ) { Box( + contentAlignment = Alignment.Center, modifier = Modifier .size(tileHeight) - .align(Alignment.Center) .thenIf(!classicStyle) { Modifier .clip(CircleShape) @@ -392,13 +399,12 @@ fun ContentScope.Tile( ) { val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } if (!classicStyle) { - SmallTileContent( + SmallTileContent( iconProvider = iconProvider, color = colors.icon, - modifier = - Modifier.align(Alignment.Center).bounceScale { - contentBounceable.iconBounceScale - }, + modifier = Modifier.bounceScale { + contentBounceable.iconBounceScale + }, ) } else { ClassicTileContent( @@ -407,10 +413,9 @@ fun ContentScope.Tile( iconShapeKey = iconShapeKey, colors = colors, labelHide = labelHide, - modifier = - Modifier.align(Alignment.Center).bounceScale { - contentBounceable.iconBounceScale - }, + modifier = Modifier.bounceScale { + contentBounceable.iconBounceScale + }, ) } } @@ -951,9 +956,7 @@ fun rememberQSPanelStyle(): Boolean { DisposableEffect(contentResolver) { val observer = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { - context.mainExecutor.execute { - classicStyleEnabled = readPanelStyleEnabled() - } + classicStyleEnabled = readPanelStyleEnabled() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt index 756399b77e41..1ed68f95db47 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt @@ -78,15 +78,15 @@ constructor( initialValue = false, ) - val largeSpan: Int - get() = - if (classicStyle) { - 1 - } else if (useExtraLargeTiles) { - if (columns > maxSpan) columns / 2 else columns - } else { - largeTileSpanInteractor.defaultTileMaxWidth - } + val largeSpan: Int by derivedStateOf { + if (classicStyle) { + 1 + } else if (useExtraLargeTiles) { + if (columns > maxSpan) columns / 2 else columns + } else { + largeTileSpanInteractor.defaultTileMaxWidth + } + } private val mediaInRowInLandscapeViewModel = if (mediaLocation != null && mediaUiBehavior != null) { From 5823fbfffa28cddd11e8db002bb354d596e7b8a1 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 5 Apr 2026 13:33:09 +0530 Subject: [PATCH 1167/1315] SystemUI: Add more classic icon shapes Ref: https://github.com/BootleggersROM/packages_overlays_Shishufied/tree/queso/QSTiles * Regenerate path and adapt for our implementation. * Add separate outline tile color to make it distinct. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../ui/compose/infinitegrid/CommonTile.kt | 53 ++++++++++++++++--- .../ui/compose/infinitegrid/EditTile.kt | 1 + .../compose/infinitegrid/QSTileIconShapes.kt | 38 +++++++++++-- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 10 ++++ 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 05dbe71ead0a..c52bbce16ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -17,10 +17,13 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.content.Context +import android.graphics.Matrix +import android.graphics.Path import android.graphics.drawable.Animatable import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable import android.text.TextUtils +import android.util.PathParser import androidx.annotation.VisibleForTesting import androidx.compose.animation.animateColorAsState import androidx.compose.animation.graphics.res.animatedVectorResource @@ -69,6 +72,7 @@ import androidx.compose.ui.graphics.ColorProducer import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter @@ -130,11 +134,26 @@ fun ClassicTileContent( labelHide: Boolean, modifier: Modifier = Modifier, ) { + val isNoBackground = iconShapeKey in QSTileIconShapes.NO_BACKGROUND_KEYS val iconShape = remember(iconShapeKey) { QSTileIconShapes.shapeForKey(iconShapeKey) } + val overlayPathData = remember(iconShapeKey) { + QSTileIconShapes.OVERLAY_BY_KEY[iconShapeKey] + } + val overlayPath = remember(overlayPathData) { + overlayPathData?.let { pathData -> + try { + PathParser.createPathFromPathData(pathData) + } catch (_: RuntimeException) { + null + } + } + } + val animatedColor by animateColorAsState(colors.background, label = "QSTileCircleBgColor") + val animatedOutlineColor by animateColorAsState(colors.outline, label = "QSTileOutlineColor") val (tileHeight, iconSize) = remember(labelHide) { if (labelHide) { @@ -152,19 +171,37 @@ fun ClassicTileContent( Box( modifier = Modifier .size(tileHeight) - .clip(iconShape) - .drawBehind { - val brush = colors.iconBackgroundGradient - if (brush != null) { - drawRect(brush = brush) - } else { - drawRect(color = animatedColor) + .thenIf(!isNoBackground) { + Modifier + .clip(iconShape) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + } + } + .thenIf(overlayPath != null) { + Modifier.drawWithContent { + drawContent() + overlayPath?.let { path -> + val scaledPath = Path(path) + val matrix = Matrix() + matrix.setScale(size.width / 100f, size.height / 100f) + scaledPath.transform(matrix) + drawPath( + path = scaledPath.asComposePath(), + color = animatedOutlineColor, + ) + } } }, ) { SmallTileContent( iconProvider = iconProvider, - color = colors.icon, + color = if (!isNoBackground) colors.icon else animatedOutlineColor, size = { iconSize }, modifier = Modifier.align(Alignment.Center), ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index 025af09e395f..691b0b0909e3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -1458,6 +1458,7 @@ private object EditModeTileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, + outline = MaterialTheme.colorScheme.onSurface, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt index d7af5af8b868..9fad79d40220 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/QSTileIconShapes.kt @@ -3,16 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ - package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.graphics.Matrix import android.util.PathParser import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.asAndroidPath import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection @@ -171,6 +169,37 @@ object QSTileIconShapes { "M4.8 33V67c0 5.8 3 11 8 13.7l29.4 17c4.9 2.7 11 2.7 15.9 0l29.4 -17" + "c4.9 -2.7 8 -8 8 -13.7V33c0 -5.8 -3 -11 -8 -13.7l-29.4 -17" + "c-4.9 -2.7 -11 -2.7 -15.9 0l-29.7 17C7.8 22.2 4.8 27.5 4.8 33z", + + "pokesign" to + "M50,0C22.8125,0 0.75,22.0417 0.75,49.25c0,27.2083 22.0417,49.25 49.25,49.25c27.1875,0 49.25,-22.0417 49.25,-49.25C99.25,22.0417 77.1875,0 50,0zM50,88.8333c-21.8542,0 -39.5833,-17.7292 -39.5833,-39.5833C10.4167,27.375 28.1458,9.6458 50,9.6458c21.8542,0 39.5833,17.7292 39.5833,39.5833C89.5833,71.1042 71.8542,88.8333 50,88.8333z M49.8125,38.8542c5.2917,0 9.625,3.9583 10.3125,9.0625h20.25C79.6667,31.7292 66.3542,18.8125 50,18.8125S20.3333,31.7292 19.625,47.9167h19.875C40.1875,42.8125 44.5208,38.8542 49.8125,38.8542z M49.8125,59.75c-4.7917,0 -8.7917,-3.25 -10.0208,-7.6667H19.7083C21.1458,67.5625 34.1458,79.6875 50,79.6875c15.8542,0 28.8542,-12.125 30.2917,-27.6042h-20.4583C58.6042,56.5 54.6042,59.75 49.8125,59.75z", + + "ninja" to + "M44.7292,50c0,-2.8333 2.25,-5.125 5.0417,-5.25V1.7708l-0.3542,-0.5208l-17.7292,28.375L3.2292,47.75h0.8958l26.5208,20.8333l19.1458,29.5833V55.25C46.9792,55.125 44.7292,52.8333 44.7292,50z M31.2917,68.1458l-26.2292,-20.6458l27.0625,-17.1875l17.3333,-27.6042l0.3125,0.4792l0,-2.8125l-0.3542,-0.5417l-18.2708,29.25l-30.6042,19.4375l3.3125,0l26.2083,20.5833l19.7292,30.4792l0,-2.875z M68.9375,30.4583l-19.1458,-28.6875v42.9792c0.0833,0 0.1458,-0.0208 0.2083,-0.0208c2.8958,0 5.2708,2.3542 5.2708,5.2708S52.9167,55.2708 50,55.2708c-0.0833,0 -0.1458,-0.0208 -0.2083,-0.0208v42.9167L50,98.4792l20.1667,-31.6875l26.1667,-19.0625L68.9375,30.4583z M69.4792,29.8958l-19.7083,-29.5208l0,2.8125l18.5,27.7083l26.6875,16.8958l-25.25,18.375l-19.7292,30.875l-0.2083,-0.3125l0,2.875l0.2292,0.3333l20.75,-32.6042l26.9792,-19.6458z M50,56.8333c-3.7708,0 -6.8333,-3.0625 -6.8333,-6.8333c0,-3.7708 3.0625,-6.8333 6.8333,-6.8333c3.7708,0 6.8333,3.0625 6.8333,6.8333C56.8333,53.7708 53.7708,56.8333 50,56.8333zM50,46.2917c-2.0417,0 -3.7083,1.6667 -3.7083,3.7083c0,2.0417 1.6667,3.7083 3.7083,3.7083c2.0417,0 3.7083,-1.6667 3.7083,-3.7083C53.7083,47.9583 52.0417,46.2917 50,46.2917z", + + "just_icons" to + "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0", + + "outline_style" to + "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0", + + "dotted_circle" to + "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0", + + "squaremedo" to + "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0", + ) + + val NO_BACKGROUND_KEYS: Set = setOf("just_icons", "outline_style", "dotted_circle", "squaremedo") + + val OVERLAY_BY_KEY: Map = mapOf( + "outline_style" to + "M50,0 A50,50 0,1,1 50,100 A50,50 0,1,1 50,0 M50,2.2 A47.8,47.8 0,1,0 50,97.8 A47.8,47.8 0,1,0 50,2.2Z", + + "dotted_circle" to + "M95.1042,40.75l3.2292,-0.6458c0,0 0.375,2.4583 0.7292,4.8958c0.125,2.4792 0.25,4.9583 0.25,4.9583h-3.2917c0,0 -0.125,-2.3125 -0.2292,-4.625C95.4375,43.0417 95.1042,40.75 95.1042,40.75z M90.375,27.8542l2.8958,-1.5833c0,0 0.2917,0.5417 0.6875,1.3958c0.3958,0.8333 0.9375,1.9583 1.4792,3.0625c0.4167,1.1667 0.8542,2.3333 1.1667,3.2083c0.3125,0.875 0.5208,1.4583 0.5208,1.4583l-3.1458,0.9792c0,0 -0.1875,-0.5417 -0.4792,-1.3542c-0.2917,-0.8125 -0.6875,-1.8958 -1.0833,-2.9792c-0.5,-1.0417 -1,-2.0833 -1.375,-2.875C90.6458,28.375 90.375,27.8542 90.375,27.8542z M82.0417,16.9375L84.3542,14.5833c0,0 0.4792,0.3958 1.0833,1.1042c0.625,0.6875 1.4583,1.6042 2.2917,2.5208c0.8125,0.9375 1.5,1.9583 2.0833,2.7083c0.5625,0.75 0.9167,1.25 0.9167,1.25l-2.7292,1.8542c0,0 -0.3542,-0.4583 -0.875,-1.1667c-0.5417,-0.6875 -1.1875,-1.6458 -1.9375,-2.5208c-0.7708,-0.8542 -1.5625,-1.7083 -2.1458,-2.3542C82.5,17.3125 82.0417,16.9375 82.0417,16.9375z M70.8958,8.9583l1.5,-2.9375c0,0 0.5833,0.25 1.375,0.7083c0.7917,0.4792 1.8542,1.125 2.9167,1.7708c1.0833,0.6042 2.0208,1.4375 2.7917,1.9583c0.75,0.5625 1.25,0.9167 1.25,0.9167l-2.0625,2.5833c0,0 -0.4583,-0.3542 -1.1667,-0.875c-0.7083,-0.5 -1.5833,-1.2708 -2.6042,-1.8333c-0.9792,-0.6042 -1.9792,-1.2083 -2.7083,-1.6458C71.4167,9.1667 70.8958,8.9583 70.8958,8.9583z M57.875,4.6042l0.5625,-3.25c0,0 0.6042,0.1042 1.5208,0.25c0.8958,0.2292 2.1042,0.5417 3.3125,0.8542c1.2083,0.2708 2.375,0.7083 3.25,1.0417c0.875,0.3125 1.4583,0.5417 1.4583,0.5417l-1.2083,3.0625c0,0 -0.5417,-0.1875 -1.3542,-0.5c-0.8125,-0.2917 -1.8958,-0.7292 -3.0208,-0.9792c-1.125,-0.2917 -2.25,-0.5833 -3.0833,-0.7917C58.4583,4.7083 57.875,4.6042 57.875,4.6042z M44.1667,4.2917l-0.4167,-3.2708c0,0 0.6042,-0.125 1.5417,-0.1875c0.9167,-0.0417 2.1667,-0.1042 3.3958,-0.1667c1.2292,-0.1042 2.4792,0 3.3958,0.0417c0.9167,0.0417 1.5417,0.0833 1.5417,0.0833l-0.25,3.2917c0,0 -0.5833,-0.0208 -1.4375,-0.0833c-0.875,-0.0417 -2.0208,-0.1458 -3.1667,-0.0417c-1.1667,0.0625 -2.3125,0.125 -3.1667,0.1667C44.7292,4.1667 44.1667,4.2917 44.1667,4.2917z M30.9583,8.0208l-1.3542,-3c0,0 0.5417,-0.2917 1.4167,-0.6458c0.875,-0.3125 2.0417,-0.75 3.2083,-1.1667c1.1458,-0.4792 2.375,-0.7083 3.2708,-0.9583c0.8958,-0.2292 1.5,-0.3958 1.5,-0.3958l0.7292,3.2083c0,0 -0.5625,0.1458 -1.3958,0.3542c-0.8333,0.2292 -1.9792,0.4375 -3.0417,0.8958c-1.0833,0.3958 -2.1667,0.7917 -2.9792,1.0833C31.4583,7.7292 30.9583,8.0208 30.9583,8.0208z M19.4583,15.4792L17.2708,13.0208c0,0 0.4583,-0.4167 1.1458,-1.0417c0.7292,-0.5833 1.7292,-1.3125 2.7083,-2.0625c0.9792,-0.7708 2.0417,-1.4167 2.8333,-1.8958c0.7917,-0.4792 1.3333,-0.8125 1.3333,-0.8125l1.6458,2.8542c0,0 -0.5,0.2917 -1.2292,0.75c-0.75,0.4375 -1.75,1.0417 -2.6458,1.7708c-0.9167,0.7083 -1.8542,1.375 -2.5417,1.9167C19.875,15.1042 19.4583,15.4792 19.4583,15.4792z M10.6458,26l-2.8125,-1.7083c0,0 1.2292,-2.1667 2.7917,-4.0833c0.7292,-1 1.4792,-1.9792 2.1458,-2.6458c0.625,-0.6875 1.0417,-1.1458 1.0417,-1.1458l2.4167,2.25c0,0 -0.3958,0.4167 -0.9792,1.0625c-0.6042,0.625 -1.3125,1.5417 -2,2.4792C11.7917,24 10.6458,26 10.6458,26z M5.3333,38.6458l-3.1875,-0.8125c0,0 0.1458,-0.6042 0.3958,-1.5c0.1875,-0.9167 0.6458,-2.0625 1.0625,-3.2292c0.7708,-2.3542 1.9167,-4.5625 1.9167,-4.5625l2.9583,1.4375c0,0 -1.0833,2.0417 -1.7917,4.25c-0.3958,1.0833 -0.8333,2.1667 -1,3.0208C5.4792,38.0833 5.3333,38.6458 5.3333,38.6458z M4,52.3125l-3.2917,0.1667c0,0 -0.0208,-0.625 -0.0833,-1.5417C0.5417,50 0.6458,48.7708 0.7083,47.5208c0.0208,-2.4792 0.4792,-4.9167 0.4792,-4.9167l3.25,0.4792c0,0 -0.4375,2.2708 -0.4583,4.6042c-0.0417,1.1667 -0.1667,2.3125 -0.0625,3.1667C3.9792,51.7292 4,52.3125 4,52.3125z M6.7292,65.75l-3.1042,1.125c0,0 -0.9375,-2.2917 -1.4583,-4.7292c-0.3125,-1.2083 -0.6458,-2.3958 -0.75,-3.3333c-0.1458,-0.9167 -0.2292,-1.5417 -0.2292,-1.5417l3.25,-0.4792c0,0 0.0833,0.5625 0.2083,1.4375c0.0833,0.875 0.4167,1.9792 0.6875,3.1042C5.8542,63.6042 6.7292,65.75 6.7292,65.75z M13.3125,77.7917l-2.625,2c0,0 -0.375,-0.5 -0.9167,-1.25c-0.5833,-0.7292 -1.2292,-1.7708 -1.875,-2.8542c-1.375,-2.0833 -2.375,-4.3542 -2.375,-4.3542l2.9583,-1.4375c0,0 0.9375,2.125 2.2083,4.0625c0.5833,1 1.1875,1.9792 1.75,2.6458C12.9583,77.3125 13.3125,77.7917 13.3125,77.7917z M23.125,87.3542l-1.9167,2.6667c0,0 -0.5,-0.375 -1.25,-0.9167c-0.75,-0.5417 -1.7292,-1.2917 -2.625,-2.1667c-0.9167,-0.8333 -1.8542,-1.6458 -2.5,-2.3125c-0.625,-0.6875 -1.0417,-1.1458 -1.0417,-1.1458l2.4167,-2.25c0,0 0.3958,0.4375 0.9792,1.0625c0.6042,0.625 1.4792,1.375 2.3333,2.1458c0.8333,0.8125 1.75,1.5208 2.4583,2.0208C22.6667,87 23.125,87.3542 23.125,87.3542z M35.3542,93.6042l-1.0417,3.125c0,0 -0.5833,-0.2083 -1.4583,-0.5208c-0.875,-0.3333 -2.0625,-0.6875 -3.1667,-1.2708c-1.125,-0.5417 -2.2292,-1.0833 -3.0625,-1.4792c-0.8125,-0.4375 -1.3333,-0.7917 -1.3333,-0.7917l1.6458,-2.8542c0,0 0.4792,0.3125 1.25,0.7292c0.7917,0.375 1.8333,0.875 2.875,1.375c1.0208,0.5417 2.1458,0.875 2.9375,1.1875C34.8125,93.4167 35.3542,93.6042 35.3542,93.6042z M48.875,95.9583l-0.0833,3.2917c0,0 -0.625,-0.0208 -1.5417,-0.0833c-0.9167,-0.0833 -2.1667,-0.0417 -3.3958,-0.2708c-1.2292,-0.1875 -2.4583,-0.375 -3.375,-0.5208c-0.4583,-0.0625 -0.8333,-0.1667 -1.1042,-0.2292c-0.2708,-0.0625 -0.4167,-0.1042 -0.4167,-0.1042l0.7292,-3.2083c0,0 0.1458,0.0417 0.3958,0.1042c0.25,0.0625 0.5833,0.1667 1.0208,0.2083c0.8542,0.125 2,0.3125 3.1458,0.4792c1.1458,0.2292 2.3125,0.1875 3.1667,0.2708C48.2917,95.9375 48.875,95.9583 48.875,95.9583z M62.4792,94.25l0.8958,3.1667c0,0 -0.6042,0.1458 -1.5,0.3958c-0.8958,0.2292 -2.0833,0.5833 -3.3333,0.7083c-1.2292,0.1875 -2.4583,0.375 -3.375,0.5208c-0.9167,0.0833 -1.5417,0.1042 -1.5417,0.1042l-0.25,-3.2917c0,0 0.5833,0 1.4375,-0.0833c0.8542,-0.125 2,-0.3125 3.1458,-0.4792c1.1458,-0.125 2.2708,-0.4583 3.1042,-0.6667S62.4792,94.25 62.4792,94.25z M74.9792,88.5833l1.7917,2.7708c0,0 -0.5208,0.3125 -1.3125,0.8125c-0.8125,0.4583 -1.8333,1.1875 -2.9583,1.6875c-1.125,0.5417 -2.2292,1.0833 -3.0625,1.4792c-0.8542,0.375 -1.4375,0.5625 -1.4375,0.5625l-1.2083,-3.0625c0,0 0.5625,-0.1667 1.3542,-0.5208c0.7917,-0.375 1.8333,-0.875 2.875,-1.375c1.0625,-0.4583 2.0208,-1.125 2.7708,-1.5625C74.4792,88.8958 74.9792,88.5833 74.9792,88.5833z M85.2708,79.5l2.5208,2.125c0,0 -0.4167,0.4583 -1.0417,1.1458c-0.6458,0.6667 -1.4167,1.6458 -2.3333,2.4792c-0.9167,0.8333 -1.8333,1.6667 -2.5208,2.2917c-0.6667,0.6458 -1.1875,0.9792 -1.1875,0.9792l-2.0625,-2.5833c0,0 0.5,-0.3125 1.125,-0.9167c0.6458,-0.5833 1.5,-1.3542 2.3542,-2.1458c0.8542,-0.7917 1.5833,-1.6875 2.1667,-2.3125C84.875,79.9375 85.2708,79.5 85.2708,79.5z M92.4375,67.7917l3.0417,1.2708c0,0 -1.0625,2.25 -2.1458,4.4583c-0.6458,1.0625 -1.2917,2.125 -1.7708,2.9167c-0.4792,0.7917 -0.8333,1.3125 -0.8333,1.3125l-2.7292,-1.8542c0,0 0.3333,-0.4792 0.7708,-1.2292c0.4583,-0.7292 1.0417,-1.7292 1.6458,-2.7083C91.4375,69.875 92.4375,67.7917 92.4375,67.7917z M95.7917,54.4792l3.2708,0.3125c0,0 -0.0625,0.6042 -0.2083,1.5417c-0.1458,0.9167 -0.3333,2.1458 -0.5208,3.375c-0.6042,2.3958 -1.2083,4.8125 -1.2083,4.8125l-3.1458,-0.9792c0,0 0.5625,-2.25 1.125,-4.4792c0.1667,-1.1458 0.3542,-2.2917 0.4792,-3.1458C95.7292,55.0417 95.7917,54.4792 95.7917,54.4792z", + + "squaremedo" to + "M16.5,66.0833l17.3958,17.4167l2.2083,0l-19.625,-19.625z M2.625,50l12.3125,-12.3125l0,-2.2083l-14.5208,14.5208l14.5208,14.5208l0,-2.2083z M50,97.375l-12.3125,-12.3125l-2.2083,0l14.5208,14.5208l14.5208,-14.5208l-2.2083,0z M63.875,83.5l2.2083,0l17.4167,-17.4167l0,-2.2083z M97.375,50l-12.3125,12.3125l0,2.2083l14.5208,-14.5208l-14.5208,-14.5208l0,2.2083z M33.9167,16.5l-17.4167,17.4167l0,2.2083l19.625,-19.625z M50,2.625l12.3125,12.3125l2.2083,0l-14.5208,-14.5208l-14.5208,14.5208l2.2083,0z M83.5,33.9167l-17.4167,-17.4167l-2.2083,0l19.625,19.625z M60.9792,16.5l2.8958,0l-1.5625,-1.5625l-2.8958,0z M40.5833,14.9375l-2.8958,0l-1.5625,1.5625l2.8958,0z M83.5,60.5833l0,3.2917l1.5625,-1.5625l0,-3.2917z M85.0625,40.5833l0,-2.8958l-1.5625,-1.5625l0,2.8958z M85.0625,64.5208l-1.5625,1.5625l0,17.4167l-17.4167,0l-1.5625,1.5625l20.5417,0z M83.5,16.5l0,17.4167l1.5625,1.5625l0,-20.5417l-20.5417,0l1.5625,1.5625z M39.4167,83.5l-3.2917,0l1.5625,1.5625l3.2917,0z M59.0208,85.0625l3.2917,0l1.5625,-1.5625l-3.2917,0z M16.5,39.0208l0,-2.8958l-1.5625,1.5625l0,2.8958z M16.5,16.5l17.4167,0l1.5625,-1.5625l-20.5417,0l0,20.5417l1.5625,-1.5625z M16.5,83.5l0,-17.4167l-1.5625,-1.5625l0,20.5417l20.5208,0l-1.5625,-1.5625z M14.9375,59.0208l0,3.2917l1.5625,1.5625l0,-3.2917z M36.125,83.5l-2.2083,0l1.5625,1.5625l2.2083,0z M63.875,16.5l2.2083,0l-1.5625,-1.5625l-2.2083,0z M83.5,36.125l1.5625,1.5625l0,-2.2083l-1.5625,-1.5625z M36.125,16.5l1.5625,-1.5625l-2.2083,0l-1.5625,1.5625z M63.875,83.5l-1.5625,1.5625l2.2083,0l1.5625,-1.5625z M16.5,63.875l-1.5625,-1.5625l0,2.2083l1.5625,1.5625z M83.5,63.875l0,2.2083l1.5625,-1.5625l0,-2.2083z M16.5,36.125l0,-2.2083l-1.5625,1.5625l0,2.2083z M16.5,59.6875l0,0.8958l22.9167,22.9167l0.8958,0z M59.6875,83.5l0.8958,0l22.9167,-22.9167l0,-0.8958z M50,93.1667l-8.125,-8.125l-0.8958,0l9.0208,9.0208l9.0208,-9.0208l-0.8958,0z M6.8333,50l8.125,-8.125l0,-1.3125l-9.2292,9.2292l9.2292,9.2292l0,-0.8958z M40.3125,16.5l-1.3125,0l-22.5,22.5l0,1.3125z M93.1667,50l-8.125,8.125l0,0.8958l9.2292,-9.2292l-9.2292,-9.2292l0,1.3125z M83.5,40.3125l0,-1.3125l-22.5,-22.5l-1.3125,0z M50,6.8333l8.125,8.125l1.3125,0l-9.4167,-9.4167l-9.4167,9.4167l1.3125,0z M59.6875,16.5l1.3125,0l-1.5625,-1.5625l-1.3125,0z M85.0625,41.875l0,-1.3125l-1.5625,-1.5625l0,1.3125z M41.875,14.9375l-1.3125,0l-1.5625,1.5625l1.3125,0z M16.5,40.3125l0,-1.3125l-1.5625,1.5625l0,1.3125z M14.9375,58.125l0,0.8958l1.5625,1.5625l0,-0.8958z M58.125,85.0625l0.8958,0l1.5625,-1.5625l-0.8958,0z M85.0625,58.125l-1.5625,1.5625l0,0.8958l1.5625,-1.5625z M40.3125,83.5l-0.8958,0l1.5625,1.5625l0.8958,0z M7.9375,49.7917l7,-7.0208l0,-0.8958l-8.125,8.125l8.125,8.125l0,-1.3125z M83.5,41.2292l0,-0.8958l-23.8125,-23.8125l-0.8958,0z M50,7.7292l7.2083,7.2083l0.8958,0l-8.125,-8.125l-8.125,8.125l0.8958,0z M41.2292,16.5l-0.8958,0l-23.8125,23.8125l0,0.8958z M50,91.875l-6.8125,-6.8125l-1.3125,0l8.125,8.125l8.125,-8.125l-1.3125,0z M85.0625,41.875l0,0.8958l7.0208,7.0208l-7.0208,7.0208l0,1.3125l8.125,-8.125z M58.375,83.5l1.3125,0l23.8125,-23.8125l0,-1.3125z M16.5,58.375l0,1.3125l23.8125,23.8125l1.3125,0z M56.8125,85.0625l1.3125,0l1.5625,-1.5625l-1.3125,0z M41.625,83.5l-1.3125,0l1.5625,1.5625l1.3125,0z M14.9375,56.8125l0,1.3125l1.5625,1.5625l0,-1.3125z M85.0625,42.7917l0,-0.8958l-1.5625,-1.5625l0,0.8958z M83.5,58.375l0,1.3125l1.5625,-1.5625l0,-1.3125z M58.7708,16.5l0.8958,0l-1.5625,-1.5625l-0.8958,0z M16.5,41.2292l0,-0.8958l-1.5625,1.5625l0,0.8958z M42.7917,14.9375l-0.8958,0l-1.5625,1.5625l0.8958,0z", ) val ALL_KEYS: List = PATH_BY_KEY.keys.toList() @@ -183,6 +212,9 @@ object QSTileIconShapes { PATH_BY_KEY[key] ?: PATH_BY_KEY[DEFAULT_KEY]!! fun shapeForKey(key: String): Shape { + if (key in NO_BACKGROUND_KEYS) { + return RectangleShape + } shapeCache[key]?.let { return it } val pathData = pathForKey(key) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index bf065d73a77c..47b2a45d1963 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -652,6 +652,7 @@ data class TileColors( val secondaryLabel: Color, val icon: Color, val iconBackgroundGradient: Brush? = null, + val outline: Color, ) @Composable @@ -1075,6 +1076,7 @@ private object TileDefaults { secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, iconBackgroundGradient = gradient, + outline = MaterialTheme.colorScheme.primary, ) } @@ -1093,6 +1095,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + outline = MaterialTheme.colorScheme.primary, ) } else { TileColors( @@ -1102,6 +1105,7 @@ private object TileDefaults { secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onPrimary, iconBackgroundGradient = gradient, + outline = MaterialTheme.colorScheme.primary, ) } } @@ -1119,6 +1123,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, + outline = MaterialTheme.colorScheme.primary, ) } else { TileColors( @@ -1127,6 +1132,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, + outline = MaterialTheme.colorScheme.primary, ) } } @@ -1140,6 +1146,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, + outline = MaterialTheme.colorScheme.onSurface, ) @Composable @@ -1153,6 +1160,7 @@ private object TileDefaults { label = onSurfaceVariantColor, secondaryLabel = onSurfaceVariantColor, icon = onSurfaceVariantColor, + outline = onSurfaceVariantColor, ) } @@ -1165,6 +1173,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + outline = MaterialTheme.colorScheme.primary, ) @Composable @@ -1176,6 +1185,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, + outline = MaterialTheme.colorScheme.primary, ) @Composable From 1b20b5856d09935fbe298e4130cee74a55cd1bb1 Mon Sep 17 00:00:00 2001 From: Adithya R Date: Sun, 12 Apr 2026 20:46:56 +0000 Subject: [PATCH 1168/1315] SystemUI: Tune new biometric dialog UI Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/dimens.xml | 2 +- packages/SystemUI/res/values/integers.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 02d0ab2478df..79023ae5d931 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1246,7 +1246,7 @@ 100dp 160dp 136dp - 28dp + 16dp 280dp 350dp diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index fad4d4ff8abe..5157601f3b32 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -15,7 +15,7 @@ ~ limitations under the License --> - 8388611 + 1 3 2 @@ -48,4 +48,4 @@ 3 4 - \ No newline at end of file + From 2bfba61e2aeb457aa0154589c37266db9761eab5 Mon Sep 17 00:00:00 2001 From: Danny Baumann Date: Tue, 3 Mar 2015 10:43:28 +0100 Subject: [PATCH 1169/1315] Allow sending vendor- or device-specific commands to the camera HAL. Change-Id: I2aaa9e526b6f1a35d45e96b6d23e3db972d82733 Signed-off-by: Joey Huab Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/hardware/Camera.java | 14 ++++++++++++++ core/jni/android_hardware_Camera.cpp | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 765d31794982..c1b220f9ae95 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -1801,6 +1801,20 @@ private void updateAppOpsPlayAudio() { } } + /** + * Send a vendor-specific camera command + * + * @hide + */ + public final void sendVendorCommand(int cmd, int arg1, int arg2) { + if (cmd < 1000) { + throw new IllegalArgumentException("Command numbers must be at least 1000"); + } + _sendVendorCommand(cmd, arg1, arg2); + } + + private native final void _sendVendorCommand(int cmd, int arg1, int arg2); + /** * Callback interface for zoom changes during a smooth zoom operation. * diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 786fc1183e7f..99fa0928417d 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -1236,6 +1236,18 @@ static int32_t android_hardware_Camera_getAudioRestriction( return ret; } +static void android_hardware_Camera_sendVendorCommand(JNIEnv *env, jobject thiz, + jint cmd, jint arg1, jint arg2) +{ + ALOGV("sendVendorCommand"); + sp camera = get_native_camera(env, thiz, NULL); + if (camera == 0) return; + + if (camera->sendCommand(cmd, arg1, arg2) != NO_ERROR) { + jniThrowRuntimeException(env, "sending vendor command failed"); + } +} + //------------------------------------------------- static const JNINativeMethod camMethods[] = { @@ -1282,6 +1294,7 @@ static const JNINativeMethod camMethods[] = { (void *)android_hardware_Camera_enableFocusMoveCallback}, {"setAudioRestriction", "(I)V", (void *)android_hardware_Camera_setAudioRestriction}, {"getAudioRestriction", "()I", (void *)android_hardware_Camera_getAudioRestriction}, + {"_sendVendorCommand", "(III)V", (void *)android_hardware_Camera_sendVendorCommand}, }; struct field { From bc090e868ba777e7df13eeac26cab8e3b71b636c Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 28 Apr 2026 19:03:47 +0000 Subject: [PATCH 1170/1315] SystemUI: Tune some clock layout Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res-keyguard/layout/keyguard_clock_center.xml | 1 - .../res-keyguard/layout/keyguard_clock_ios.xml | 1 + .../res-keyguard/layout/keyguard_clock_nos1.xml | 2 +- .../res-keyguard/layout/keyguard_clock_nos2.xml | 11 ++++------- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml index 687e9d6e89ac..4049853d7a7f 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml @@ -53,7 +53,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:alpha="0.9" - android:layout_marginTop="8dp" android:fontFamily="@*android:string/config_clockFontFamily" android:format12Hour="hh:mm" android:format24Hour="HH:mm" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml index 589fc86a7a51..5987e0d5beff 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml @@ -31,6 +31,7 @@ android:layout_height="wrap_content" android:alpha="0.9" android:fontFamily="@*android:string/config_clockFontFamily" + android:layout_marginTop="-8dp" android:format12Hour="hh:mm" android:format24Hour="HH:mm" android:gravity="center" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml index 2232ba083884..26d238364be9 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml @@ -54,7 +54,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:alpha="0.9" - android:layout_marginTop="9dp" + android:layout_marginTop="7dp" android:fontFamily="@font/subway" android:includeFontPadding="false" android:format12Hour="hmm" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml index 35a0dd4c20ff..6650c59897d1 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml @@ -56,7 +56,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:layout_marginTop="-4dp" + android:layout_marginTop="-12dp" android:gravity="center"> @@ -64,7 +64,6 @@ android:id="@+id/clock_hour" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:alpha="0.9" android:fontFamily="@font/ntype82" android:format12Hour="h" android:format24Hour="H" @@ -73,27 +72,25 @@ android:tag="text1" android:textAlignment="center" android:textColor="@android:color/white" - android:textSize="150dp" /> + android:textSize="100dp" /> + android:textSize="94dp" /> + android:textSize="100dp" /> From 4b5548e61f390a1241f01af33237cc1922b4f5c7 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:11:43 +0800 Subject: [PATCH 1171/1315] services: fixing bt device enabling mouse input issue https: //github.com/AxionAOSP/issue_tracker/issues/158 Change-Id: I237dda9f71e5145d3b9963c1c5b03a7912af278a Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/input/InputManagerService.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index d71710aa53df..e20e7b9b4d80 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -35,6 +35,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; @@ -248,6 +249,9 @@ public class InputManagerService extends IInputManager.Stub private final ArrayList mTempInputDevicesChangedListenersToNotify = new ArrayList<>(); // handler thread only + @GuardedBy("mInputDevicesLock") + private final SparseBooleanArray mSuppressedBluetoothPointers = new SparseBooleanArray(); + // State for vibrator tokens. private final Object mVibratorLock = new Object(); private final Map mVibratorTokens = new ArrayMap<>(); @@ -2476,6 +2480,68 @@ private void notifyInputDevicesChanged(InputDevice[] inputDevices) { } mInputDevices = inputDevices; + mHandler.post(this::evaluateBluetoothPointerSuppression); + } + } + + private void evaluateBluetoothPointerSuppression() { + final InputDevice[] devices; + synchronized (mInputDevicesLock) { + devices = mInputDevices; + } + synchronized (mInputDevicesLock) { + for (int i = mSuppressedBluetoothPointers.size() - 1; i >= 0; i--) { + final int trackedId = mSuppressedBluetoothPointers.keyAt(i); + boolean stillPresent = false; + for (InputDevice device : devices) { + if (device.getId() == trackedId) { + stillPresent = true; + break; + } + } + if (!stillPresent) { + mSuppressedBluetoothPointers.removeAt(i); + } + } + } + for (InputDevice device : devices) { + final int id = device.getId(); + if ((device.getSources() & InputDevice.SOURCE_MOUSE) != InputDevice.SOURCE_MOUSE) { + continue; + } + final String btAddress = mNative.getBluetoothAddress(id); + if (btAddress == null) { + continue; + } + if (isLikelyRealBluetoothPointer(btAddress)) { + continue; + } + synchronized (mInputDevicesLock) { + if (mSuppressedBluetoothPointers.get(id)) { + continue; + } + mSuppressedBluetoothPointers.put(id, true); + } + mNative.disableInputDevice(id); + } + } + + private boolean isLikelyRealBluetoothPointer(String btAddress) { + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null) { + return true; + } + try { + final BluetoothDevice device = adapter.getRemoteDevice(btAddress); + final BluetoothClass btClass = device.getBluetoothClass(); + if (btClass == null) { + return true; + } + final int deviceClass = btClass.getDeviceClass(); + return (deviceClass & BluetoothClass.Device.PERIPHERAL_POINTING) + == BluetoothClass.Device.PERIPHERAL_POINTING; + } catch (IllegalArgumentException e) { + return true; } } From d39a86194b8267a0e38587e326d73fc5a4e14c0b Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 30 Apr 2026 22:13:53 +0000 Subject: [PATCH 1172/1315] SystemUI: Fix pulse rendering behind UDFPS and lockscreen affordances Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/statusbar/phone/CentralSurfacesImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 0f0e22227d35..e8cc75b1ef85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1202,7 +1202,7 @@ private void attachCustomOverlays() { new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - overlay.addView(mPulseViewController.getPulseView(), + root.addView(mPulseViewController.getPulseView(), scrimBehindIndex + 1, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); From 859cb29ab23fb44ae0ac2e9b3911c2a90f484ba0 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 1 May 2026 17:06:19 +0530 Subject: [PATCH 1173/1315] SystemUI: DynamicBar: Fix notification event not dismissable Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt index 7a931885f236..2d2ac26242d9 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarExpandedPanel.kt @@ -156,8 +156,7 @@ constructor( WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or if (isCurrentlyExpanded) 0 - else (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + else WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE val params = WindowManager.LayoutParams( @@ -203,12 +202,10 @@ constructor( val params = view.layoutParams as? WindowManager.LayoutParams ?: return@ensureMainThread if (expanded) { params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() - params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() params.height = WindowManager.LayoutParams.MATCH_PARENT windowManager.updateViewLayout(view, params) } else { params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE windowManager.updateViewLayout(view, params) val runnable = Runnable { val v = overlayView ?: return@Runnable From 704e3cb71454c704fec724e2854535fa9262dc66 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 10:54:28 +0800 Subject: [PATCH 1174/1315] SystemUI: DynamicBar: Reducing cpu usage Change-Id: Id091a6747c1a95f22b95ec09880704c6971ed94e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../data/source/AppTrackingIslandManager.kt | 29 ++++- .../ui/AxDynamicBarChipViewModel.kt | 28 ++-- .../ui/compose/AxDynamicBarChip.kt | 120 +++++++++--------- .../ui/compose/PillIslandContent.kt | 64 +++++++++- 4 files changed, 150 insertions(+), 91 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt index 76c811d16b8a..f48b91031b1c 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/data/source/AppTrackingIslandManager.kt @@ -36,13 +36,15 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont @Volatile private var currentForegroundPkg: String? = null private fun emitEvent() { - _appSwitchEvent.value = + val event = if (recentApps.isNotEmpty()) IslandEvent.AppSwitch( recentApps = recentApps.toList(), previousApp = _appSwitchEvent.value?.previousApp, ) else null + logEvent("emit", event) + _appSwitchEvent.value = event } private val listener = @@ -59,6 +61,12 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont return } if (!hasLauncherIntent(pkg)) return + if (pkg == currentForegroundPkg) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "skip duplicate front pkg=$pkg task=${taskInfo.taskId}") + } + return + } val leavingPkg = currentForegroundPkg currentForegroundPkg = pkg @@ -107,13 +115,15 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont _appSwitchEvent.value?.previousApp } - _appSwitchEvent.value = + val event = if (recentApps.isNotEmpty()) IslandEvent.AppSwitch( recentApps = recentApps.toList(), previousApp = newPreviousApp, ) else null + logEvent("front pkg=$pkg leaving=$leavingPkg", event) + _appSwitchEvent.value = event } } @@ -144,6 +154,9 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont } fun clear() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "clear") + } _appSwitchEvent.value = null } @@ -188,5 +201,15 @@ class AppTrackingIslandManager @Inject constructor(@Application private val cont } catch (_: Exception) { null } -} + private fun logEvent(reason: String, event: IslandEvent.AppSwitch?) { + if (!Log.isLoggable(TAG, Log.DEBUG)) return + Log.d( + TAG, + "$reason emit=${event != null} " + + "count=${event?.recentApps?.size ?: 0} " + + "top=${event?.recentApps?.firstOrNull()?.packageName} " + + "prev=${event?.previousApp?.packageName}", + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt index 178f9c03ed5e..9e08eb9eac83 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/AxDynamicBarChipViewModel.kt @@ -14,19 +14,13 @@ import com.android.systemui.statusbar.policy.BatteryController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -84,6 +78,7 @@ constructor( allEvents = uiState.events, ) } + .distinctUntilChanged() .stateIn(applicationScope, SharingStarted.Lazily, null) val isEnabled: StateFlow = interactor.settings.isEnabled @@ -112,25 +107,19 @@ constructor( KeyguardBatteryInfo(0, false, false, false, null), ) - // Re-compute charging string whenever battery info changes + // Re-compute charging string only when battery state changes. Polling this while idle is costly + // because KeyguardIndicationController formats through Resources on the main thread. val batteryString: StateFlow = keyguardBatteryInfo - .map { it.isCharging } - .distinctUntilChanged() - .flatMapLatest { charging -> - if (charging) { - flow { - while (true) { - emit(formatChargingString(keyguardIndicationController.powerChargingString)) - delay(BATTERY_STRING_REFRESH_MS) - } - } + .map { + if (it.isCharging) { + formatChargingString(keyguardIndicationController.powerChargingString) } else { - flowOf(formatChargingString(keyguardIndicationController.powerChargingString)) + "" } } .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.Eagerly, "") + .stateIn(applicationScope, SharingStarted.Lazily, "") val isOnKeyguard: StateFlow = interactor.isOnKeyguard @@ -195,7 +184,6 @@ constructor( companion object { private const val LOW_UDFPS_THRESHOLD = 0.93f - private const val BATTERY_STRING_REFRESH_MS = 2_000L } private fun formatChargingString(text: String?): String { diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt index 5b556e40264e..e878a465aa22 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarChip.kt @@ -2,14 +2,7 @@ package com.android.systemui.axdynamicbar.ui.compose import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.runtime.LaunchedEffect -import kotlin.math.abs import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.animation.fadeIn @@ -34,7 +27,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -76,7 +68,6 @@ import com.android.systemui.axdynamicbar.shared.TsBadge import com.android.systemui.axdynamicbar.shared.chipAccentColorFor import com.android.systemui.axdynamicbar.shared.chipContentColorOn import com.android.systemui.axdynamicbar.shared.chipProgressFor -import com.android.systemui.axdynamicbar.shared.iconKeyFor import com.android.systemui.axdynamicbar.shared.textKeyFor import com.android.systemui.axdynamicbar.shared.toScaledBitmap import com.android.systemui.axdynamicbar.ui.AxDynamicBarChipViewModel @@ -186,32 +177,33 @@ fun AxDynamicBarChip( onClick = null, defaultMinSize = false, ) { _ -> - AnimatedContent( - targetState = ChipDisplay(displayEvent, isAlert), - transitionSpec = { - (fadeIn(motionScheme.defaultEffectsSpec()) + scaleIn(initialScale = 0.92f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith - (fadeOut(motionScheme.fastEffectsSpec()) + scaleOut(targetScale = 0.92f, animationSpec = motionScheme.fastSpatialSpec())) using - SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) - }, - contentKey = { if (it.isAlert) "alert" else it.event::class.simpleName }, - label = "chip_event", - ) { display -> - val rawAccent = chipAccentColorFor(display.event) - val accent by animateColorAsState(rawAccent, MaterialTheme.motionScheme.fastEffectsSpec(), label = "accent") - val contentColor by animateColorAsState( - chipContentColorOn(rawAccent), MaterialTheme.motionScheme.fastEffectsSpec(), label = "content", - ) - val rawProgress = chipProgressFor(display.event) - val progressTarget = rawProgress ?: 0f - val progressAnim = remember { Animatable(progressTarget) } - LaunchedEffect(progressTarget) { - if (abs(progressTarget - progressAnim.value) > 0.05f) { - progressAnim.animateTo(progressTarget, tween(300, easing = FastOutSlowInEasing)) - } else { - progressAnim.snapTo(progressTarget) - } - } - val progress = if (rawProgress != null) progressAnim.value else null + AnimatedContent( + targetState = chipDisplayKey(displayEvent, isAlert), + transitionSpec = { + (( + fadeIn(motionScheme.defaultEffectsSpec()) + + scaleIn( + initialScale = 0.92f, + animationSpec = motionScheme.defaultSpatialSpec(), + ) + ) togetherWith ( + fadeOut(motionScheme.fastEffectsSpec()) + + scaleOut( + targetScale = 0.92f, + animationSpec = motionScheme.fastSpatialSpec(), + ) + )) + .using(sizeTransform = null) + }, + label = "chip_event", + ) { + val event = displayEvent + val rawAccent = chipAccentColorFor(event) + val accent by animateColorAsState(rawAccent, MaterialTheme.motionScheme.fastEffectsSpec(), label = "accent") + val contentColor by animateColorAsState( + chipContentColorOn(rawAccent), MaterialTheme.motionScheme.fastEffectsSpec(), label = "content", + ) + val progress = chipProgressFor(event) Box( modifier = Modifier.fillMaxHeight(), @@ -223,7 +215,6 @@ fun AxDynamicBarChip( .widthIn(max = 100.dp) .clip(ChipShape) .background(accent) - .animateContentSize(motionScheme.defaultSpatialSpec()) .then( if (progress != null) { val trackColor = lerp(accent, contentColor, 0.2f) @@ -263,19 +254,16 @@ fun AxDynamicBarChip( color = contentColor.copy(alpha = AlphaTertiary), ) } - if (display.isAlert && display.event is IslandEvent.Notification) { + if (isAlert && event is IslandEvent.Notification) { + val notif = event AnimatedContent( - targetState = display.event, + targetState = notif.sbn.key, transitionSpec = { (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith fadeOut(motionScheme.fastEffectsSpec())) .using(sizeTransform = null) }, - contentKey = { - (it as? IslandEvent.Notification)?.sbn?.key - }, label = "alert_content", - ) { event -> - val notif = event as IslandEvent.Notification + ) { Row(verticalAlignment = Alignment.CenterVertically) { notif.appIcon?.let { icon -> Image( @@ -298,8 +286,8 @@ fun AxDynamicBarChip( ) } } - } else if (display.event is IslandEvent.Sports && (display.event as IslandEvent.Sports).team2Name.isNotEmpty()) { - val sport = display.event as IslandEvent.Sports + } else if (event is IslandEvent.Sports && event.team2Name.isNotEmpty()) { + val sport = event StatusBarSportsTeamBadge(sport.team1Name, sport.team1Icon, contentColor) Spacer(Modifier.width(SpaceXs)) Text( @@ -313,33 +301,27 @@ fun AxDynamicBarChip( StatusBarSportsTeamBadge(sport.team2Name, sport.team2Icon, contentColor) } else { AnimatedContent( - targetState = display.event, + targetState = chipIconKey(event), transitionSpec = { - (fadeIn(motionScheme.defaultEffectsSpec()) + - scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith - (fadeOut(motionScheme.fastEffectsSpec()) + - scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using - SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) }, - contentKey = { iconKeyFor(it) }, label = "chip_icon", - ) { event -> - PillEventIcon(event, tint = contentColor) + ) { + PillEventIcon(event, tint = contentColor, animated = false) } Spacer(Modifier.width(SpaceXs)) AnimatedContent( - targetState = display.event, + targetState = textKeyFor(event), transitionSpec = { - (fadeIn(motionScheme.defaultEffectsSpec()) + - scaleIn(initialScale = 0.85f, animationSpec = motionScheme.defaultSpatialSpec())) togetherWith - (fadeOut(motionScheme.fastEffectsSpec()) + - scaleOut(targetScale = 0.85f, animationSpec = motionScheme.fastSpatialSpec())) using - SizeTransform(clip = false, sizeAnimationSpec = { _, _ -> motionScheme.defaultSpatialSpec() }) + (fadeIn(motionScheme.defaultEffectsSpec()) togetherWith + fadeOut(motionScheme.fastEffectsSpec())) + .using(sizeTransform = null) }, - contentKey = { textKeyFor(it) }, label = "chip_text", modifier = Modifier.weight(1f, fill = false).widthIn(max = chipTextMaxWidth), - ) { event -> + ) { PillEventText( event, Modifier.widthIn(max = chipTextMaxWidth), @@ -401,5 +383,17 @@ private fun StatusBarSportsTeamBadge(name: String, icon: Drawable?, contentColor } } -private data class ChipDisplay(val event: IslandEvent, val isAlert: Boolean) +private fun chipDisplayKey(event: IslandEvent, isAlert: Boolean): String = + if (isAlert) "alert:${event.id}" else event.id + +private fun chipIconKey(event: IslandEvent): Any = + when (event) { + is IslandEvent.AppSwitch -> { + val app = event.previousApp ?: event.recentApps.firstOrNull() + app?.taskId ?: app?.packageName ?: event.id + } + is IslandEvent.Media -> event.albumArt ?: event.packageName + is IslandEvent.Notification -> event.sbn.key + else -> event.id + } diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index b5ac90eb1bf0..2adc2abe31bd 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -80,7 +80,15 @@ import java.lang.Math.toRadians import kotlinx.coroutines.delay @Composable -internal fun PillEventIcon(event: IslandEvent, tint: Color? = null) { +internal fun PillEventIcon( + event: IslandEvent, + tint: Color? = null, + animated: Boolean = true, +) { + if (!animated) { + StaticPillEventIcon(event, tint) + return + } when (event) { is IslandEvent.AudioRecording -> AudioRecordingPillIcon(event, tint) is IslandEvent.Media -> MediaPillIcon(event) @@ -107,7 +115,48 @@ internal fun PillEventIcon(event: IslandEvent, tint: Color? = null) { } @Composable -private fun AospChipPillIcon(event: IslandEvent.AospChip, tint: Color? = null) { +private fun StaticPillEventIcon(event: IslandEvent, tint: Color? = null) { + when (event) { + is IslandEvent.Media -> MediaPillIcon(event, animated = false) + is IslandEvent.Notification -> NotificationPillIcon(event) + is IslandEvent.AppSwitch -> AppSwitchPillIcon(event) + is IslandEvent.AospChip -> AospChipPillIcon(event, tint, animated = false) + is IslandEvent.PromotedOngoing -> + if (event.appIcon != null) { + Image( + bitmap = event.appIcon.toScaledBitmap(16.dp), + contentDescription = null, + modifier = Modifier.size(16.dp).clip(ShapeXs), + contentScale = ContentScale.Crop, + ) + } else { + Icon( + Icons.Filled.Notifications, + null, + tint = tint ?: BlueAccent, + modifier = Modifier.size(SizeBadge), + ) + } + else -> { + val style = eventStyleFor(event) + style.icon?.let { + Icon( + it, + null, + tint = tint ?: style.accent, + modifier = Modifier.size(SizeBadge), + ) + } + } + } +} + +@Composable +private fun AospChipPillIcon( + event: IslandEvent.AospChip, + tint: Color? = null, + animated: Boolean = true, +) { val color = tint ?: aospChipAccent(event.active) val context = LocalContext.current val isCountdown = event.active.content is OngoingActivityChipModel.Content.Countdown @@ -164,6 +213,7 @@ private fun AospChipPillIcon(event: IslandEvent.AospChip, tint: Color? = null) { val isScreenRec = event.active.key == "ScreenRecord" when { + !animated -> iconContent() isCall -> { val transition = rememberInfiniteTransition(label = "aosp_call_shake") val shake by transition.animateFloat( @@ -275,7 +325,7 @@ private fun AnimatedTrophyIcon(color: Color) { } @Composable -private fun MediaPillIcon(event: IslandEvent.Media) { +private fun MediaPillIcon(event: IslandEvent.Media, animated: Boolean = true) { event.albumArt?.let { art -> Image( bitmap = art.toScaledBitmap(16.dp), @@ -289,7 +339,12 @@ private fun MediaPillIcon(event: IslandEvent.Media) { Modifier.size(16.dp).clip(CircleShape).background(OrangeAccent.copy(alpha = 0.42f)), contentAlignment = Alignment.Center, ) { - WaveformAnimation(OrangeAccent, Modifier.size(10.dp), isAnimating = event.isPlaying, barCount = 3) + WaveformAnimation( + OrangeAccent, + Modifier.size(10.dp), + isAnimating = animated && event.isPlaying, + barCount = 3, + ) } } @@ -1362,4 +1417,3 @@ fun WaveformAnimation(color: Color, modifier: Modifier = Modifier.size(34.dp, 20 } } } - From 0fa3602b97344b805822956a3bb568d3fa91144e Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 11:11:54 +0800 Subject: [PATCH 1175/1315] core: Adding aod screen off animation Change-Id: I1c798bb0c29558c97fcf381d973189ce30af4a1e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/os/PowerManager.java | 10 ++++++++- .../systemui/LauncherProxyService.java | 9 +++++++- .../data/repository/KeyguardRepository.kt | 17 ++++++++++++++ .../repository/LightRevealScrimRepository.kt | 22 ++++++++++++++++++- .../domain/interactor/PowerInteractor.kt | 7 ++++++ .../power/shared/model/WakeSleepReason.kt | 5 ++++- .../NotificationPanelViewController.java | 4 +++- .../systemui/shade/PulsingGestureListener.kt | 5 +++-- .../systemui/shade/QQSGestureListener.kt | 6 ++++- .../UnlockedScreenOffAnimationController.kt | 21 ++++++++++++++---- 10 files changed, 94 insertions(+), 12 deletions(-) diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 10a2941abd7e..8f276d00b97b 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -597,9 +597,15 @@ public static String userActivityEventToString(@UserActivityEvent int userActivi public static final int GO_TO_SLEEP_REASON_UNKNOWN = 14; /** + * Go to sleep reason code: Going to sleep due to a touch tap or double tap. * @hide */ - public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_UNKNOWN; + public static final int GO_TO_SLEEP_REASON_TOUCH = 15; + + /** + * @hide + */ + public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_TOUCH; /** * @hide @@ -620,6 +626,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { case GO_TO_SLEEP_REASON_QUIESCENT: return "quiescent"; case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button"; case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout"; + case GO_TO_SLEEP_REASON_TOUCH: return "touch"; case GO_TO_SLEEP_REASON_UNKNOWN: return "unknown"; default: return Integer.toString(sleepReason); } @@ -733,6 +740,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { GO_TO_SLEEP_REASON_QUIESCENT, GO_TO_SLEEP_REASON_SLEEP_BUTTON, GO_TO_SLEEP_REASON_TIMEOUT, + GO_TO_SLEEP_REASON_TOUCH, GO_TO_SLEEP_REASON_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java index 5bf2f4b7e689..c35e8cc8ea57 100644 --- a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java @@ -101,6 +101,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState.SysUiStateCallback; @@ -190,6 +191,7 @@ public class LauncherProxyService implements CallbackController mUnfoldTransitionProgressForwarder; private final UiEventLogger mUiEventLogger; @@ -529,8 +531,11 @@ public void toggleQuickSettingsPanel() { public void onSleepEvent(MotionEvent event) { verifyCallerAndClearCallingIdentity("onSleepEvent", () -> { mHandler.post(() -> { + mPowerInteractor.setLastTouchToSleepPosition( + event.getX(), event.getY()); mContext.getSystemService(PowerManager.class) - .goToSleep(event.getEventTime()); + .goToSleep(event.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); event.recycle(); }); }); @@ -804,6 +809,7 @@ public LauncherProxyService(Context context, UserTracker userTracker, UserManager userManager, WakefulnessLifecycle wakefulnessLifecycle, + PowerInteractor powerInteractor, UiEventLogger uiEventLogger, DisplayTracker displayTracker, KeyguardUnlockAnimationController sysuiUnlockAnimationController, @@ -846,6 +852,7 @@ public LauncherProxyService(Context context, mShadeModeInteractor = shadeModeInteractor; mShadeDisplayPolicy = shadeDisplayPolicy; mUserTracker = userTracker; + mPowerInteractor = powerInteractor; mConnectionBackoffAttempts = 0; int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 4f45d44e9c0c..c96084d5a05e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -189,6 +189,10 @@ interface KeyguardRepository { val lastDozeTapToWakePosition: StateFlow + val lastTouchToSleepPosition: StateFlow + + fun setLastTouchToSleepPosition(position: Point) + /** Last point that [KeyguardRootView] was tapped */ val lastRootViewTapPosition: MutableStateFlow @@ -284,6 +288,8 @@ interface KeyguardRepository { fun setLastDozeTapToWakePosition(position: Point) + fun clearLastTouchToSleepPosition() + fun setIsDozing(isDozing: Boolean) fun dozeTimeTick() @@ -437,6 +443,17 @@ constructor( _lastDozeTapToWakePosition.value = position } + private val _lastTouchToSleepPosition = MutableStateFlow(null) + override val lastTouchToSleepPosition = _lastTouchToSleepPosition.asStateFlow() + + override fun setLastTouchToSleepPosition(position: Point) { + _lastTouchToSleepPosition.value = position + } + + override fun clearLastTouchToSleepPosition() { + _lastTouchToSleepPosition.value = null + } + override val lastRootViewTapPosition: MutableStateFlow = MutableStateFlow(null) override val ambientIndicationVisible: MutableStateFlow = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index ad7c25e67b45..bd930f90eba3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakeSleepReason.TAP +import com.android.systemui.power.shared.model.WakeSleepReason.TOUCH_TO_SLEEP import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.CircleReveal @@ -46,7 +47,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -134,17 +137,34 @@ constructor( /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */ private val nonBiometricRevealEffect: Flow = - powerRepository.wakefulness.flatMapLatest { wakefulnessModel -> + combine( + powerRepository.wakefulness, + keyguardRepository.lastTouchToSleepPosition, + ) { wakefulnessModel, touchSleepPos -> + Pair(wakefulnessModel, touchSleepPos) + }.flatMapLatest { (wakefulnessModel, touchSleepPos) -> when { wakefulnessModel.isAwakeOrAsleepFrom(WakeSleepReason.POWER_BUTTON) -> powerButtonRevealEffect wakefulnessModel.isAwakeFrom(TAP) -> tapRevealEffect + wakefulnessModel.isAwakeOrAsleepFrom(TOUCH_TO_SLEEP) && touchSleepPos != null -> + flowOf(constructCircleRevealFromPoint(touchSleepPos)) else -> flowOf(LiftReveal) } } private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f) + init { + backgroundScope.launch { + powerRepository.wakefulness.collect { model -> + if (model.isAwake()) { + keyguardRepository.clearLastTouchToSleepPosition() + } + } + } + } + override val wallpaperSupportsAmbientMode: MutableStateFlow = MutableStateFlow(false) override val useDarkWallpaperScrim: StateFlow = diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index cef410908b74..7222a5ca7e1e 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -17,11 +17,13 @@ package com.android.systemui.power.domain.interactor +import android.graphics.Point import android.os.PowerManager import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -50,6 +52,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val statusBarStateController: StatusBarStateController, private val cameraGestureHelper: Provider, + private val keyguardRepository: KeyguardRepository, ) { /** Whether the screen is on or off. */ val isInteractive: StateFlow = repository.isInteractive @@ -93,6 +96,10 @@ constructor( fun onUserTouch(noChangeLights: Boolean = false) = repository.userTouch(noChangeLights = noChangeLights) + fun setLastTouchToSleepPosition(x: Float, y: Float) { + keyguardRepository.setLastTouchToSleepPosition(Point(x.toInt(), y.toInt())) + } + /** * Wakes up the device if the device was dozing. * diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index c57b53bab442..cd390cffd85c 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -60,7 +60,9 @@ enum class WakeSleepReason( FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD), /** Device goes to sleep because it timed out. */ - TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT), + + TOUCH_TO_SLEEP(isTouch = true, PowerManager.GO_TO_SLEEP_REASON_TOUCH); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -84,6 +86,7 @@ enum class WakeSleepReason( PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON -> SLEEP_BUTTON PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD + PowerManager.GO_TO_SLEEP_REASON_TOUCH -> TOUCH_TO_SLEEP else -> OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 36c1be6ca05c..3be8f7420414 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -835,7 +835,9 @@ public void onViewDetachedFromWindow(View v) {} @Override public boolean onDoubleTap(MotionEvent e) { if (mPowerManager != null) { - mPowerManager.goToSleep(e.getEventTime()); + mPowerInteractor.setLastTouchToSleepPosition(e.getX(), e.getY()); + mPowerManager.goToSleep(e.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); } return true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index 402d406516da..04f6965f7141 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -140,10 +140,10 @@ constructor( return false } - return onDoubleTapEvent() + return onDoubleTapEvent(e.x, e.y) } - fun onDoubleTapEvent(): Boolean { + fun onDoubleTapEvent(x: Float = 0f, y: Float = 0f): Boolean { // React to the [MotionEvent.ACTION_UP] event after double tap is detected. Falsing // checks MUST be on the ACTION_UP event. if ( @@ -153,6 +153,7 @@ constructor( !falsingManager.isFalseDoubleTap ) { if (doubleTapVibrate) wakeVibrate() + dozeInteractor.setLastTapToWakePosition(Point(x.toInt(), y.toInt())) powerInteractor.wakeUpIfDozing("PULSING_DOUBLE_TAP", PowerManager.WAKE_REASON_TAP) return true } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt index 70c62a58fc7b..a948777ad786 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.CentralSurfaces import lineageos.providers.LineageSettings @@ -40,6 +41,7 @@ class QQSGestureListener @Inject constructor( private val statusBarStateController: StatusBarStateController, private val selectedUserInteractor: SelectedUserInteractor, private val centralSurfaces: CentralSurfaces, + private val powerInteractor: PowerInteractor, ) : GestureDetector.SimpleOnGestureListener() { private var doubleTapToSleepEnabled = false @@ -85,7 +87,9 @@ class QQSGestureListener @Inject constructor( !centralSurfaces.isBouncerShowing()) && !falsingManager.isFalseDoubleTap ) { - powerManager.goToSleep(e.getEventTime()) + powerInteractor.setLastTouchToSleepPosition(e.x, e.y) + powerManager.goToSleep(e.eventTime, + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0) return true } else if (!statusBarStateController.isDozing && lockscreenDT2SEnabled && diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index aeab291699d1..f8425817ce7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.domain.interactor.DisplayStateInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.res.R import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor @@ -31,6 +32,7 @@ import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.statusbar.PowerButtonReveal import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarStateControllerImpl @@ -132,10 +134,12 @@ constructor( } override fun onAnimationStart(animation: Animator) { - if (dozeParameters.get().isMinModeActive()) { - lightRevealScrim.revealEffect = LiftReveal - } else { - lightRevealScrim.revealEffect = revealEffect + if (!ambientAod()) { + if (dozeParameters.get().isMinModeActive()) { + lightRevealScrim.revealEffect = LiftReveal + } else { + lightRevealScrim.revealEffect = revealEffect + } } interactionJankMonitor.begin( notifShadeWindowControllerLazy.get().windowRootView, @@ -298,6 +302,15 @@ constructor( lightRevealAnimator.setDuration(LIGHT_REVEAL_ANIMATION_DURATION_MINMODE) } + if (wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) { + val powerButtonY = context.resources.getDimensionPixelSize( + R.dimen.physical_power_button_center_screen_location_y + ).toFloat() + revealEffect = PowerButtonReveal(powerButtonY) + } else { + revealEffect = LiftReveal + } + // Start the animation on the next frame. startAnimation() is called after // PhoneWindowManager makes a binder call to System UI on // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is From 29fe0fce4edea35fa73f0bc1d7cc8758d589eba7 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 6 Dec 2024 10:30:23 +0000 Subject: [PATCH 1176/1315] SystemUI: Use accent ripple for lockscreen widget click action Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/lockscreen_widget_background_circle.xml | 2 +- .../res/drawable/lockscreen_widget_background_square.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml index 1d2a38200621..61b50c3ff512 100644 --- a/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml @@ -1,6 +1,6 @@ diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml index 1af71a4b567c..51c2851a724c 100644 --- a/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml @@ -1,6 +1,6 @@ From 971776fa3a36d7c23d302bb513bfae43e54c259f Mon Sep 17 00:00:00 2001 From: Mashopy Date: Sat, 3 Jan 2026 01:30:42 +0100 Subject: [PATCH 1177/1315] SystemUI: Reverse MediaTek udfps dimlayer changes The current impl for udfps framework dimming mostly targets OPlus Qualcomm devices, making it unsuitable for MediaTek devices lacking LHBM supports. Due to how GHBM is handled by MediaTek on BSP (HWC + RenderEngine + SysUI), it causes multiple issues on them, notably the infamous flashbang behavior with GHBM as MediaTek HWC expect multiple DimLayers that dynamically parse alpha based on the current brightness value and then order renderengine to dither it to not trigger a flashbang. This behavior has been mostly reversed from PRIZE MtkSystemUI apk from LAVA and tested on Nothing Phone (2a). TODO: Test on non-PRIZE devices and possibly let other devices change the alpha smoother point. Change-Id: I907d9075c79ab4af5b82d3781291d97c1eab17b1 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../SystemUI/res/values/lineage_config.xml | 6 + .../biometrics/MtkUdfpsScrimController.java | 95 +++++++++ .../systemui/biometrics/UdfpsController.java | 48 +++++ .../biometrics/UdfpsControllerOverlay.kt | 184 +++++++++++++++++- 4 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/MtkUdfpsScrimController.java diff --git a/packages/SystemUI/res/values/lineage_config.xml b/packages/SystemUI/res/values/lineage_config.xml index ccb178f5d782..5844a3dd131a 100644 --- a/packages/SystemUI/res/values/lineage_config.xml +++ b/packages/SystemUI/res/values/lineage_config.xml @@ -57,6 +57,12 @@ 4095,0 + + false + + + OnScreenFingerprintDimLayer + false diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/MtkUdfpsScrimController.java b/packages/SystemUI/src/com/android/systemui/biometrics/MtkUdfpsScrimController.java new file mode 100644 index 000000000000..0252ad14bd44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/MtkUdfpsScrimController.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2026 The LineageOS project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import android.content.Context; +import android.provider.Settings; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; + +/** + * Controller for managing the UDFPS HBM (High Brightness Mode) Scrim on MediaTek devices lacking LHBM support. + * + * Logic reversed from Smali: Lcom/android/systemui/biometrics/PriUdfpsScrimController; from LAVA + */ +public class MtkUdfpsScrimController { + private static MtkUdfpsScrimController mInstance; + + // Accessed directly by UdfpsController + public View mScrimView; + public WindowManager mWindowManager; + + private final Context mContext; + + public MtkUdfpsScrimController(Context context) { + mContext = context; + } + + public static MtkUdfpsScrimController getInstance(Context context) { + if (mInstance == null) { + mInstance = new MtkUdfpsScrimController(context); + } + return mInstance; + } + + /** + * Calculates the alpha (transparency) for the HBM overlay based on screen brightness. + * The formula ensures the sensor gets a consistent amount of light. + * + * @param brightness Current system brightness (0-255) + * @return Alpha value (0.0f to 1.0f) + */ + public float calculateAlpha(int brightness) { + float alpha = 1.0f - (brightness / 255.0f); + + if (brightness < 25) { + // Make alpha 5% more transparent when brightness is under 25. + alpha = alpha * 0.95f; + } + + Log.d("MtkUdfpsScrimController", "Requested Brightness value: " + brightness); + Log.d("MtkUdfpsScrimController", "Alpha Value: " + alpha); + + // Ensure we stay in bounds + return Math.max(0.0f, Math.min(1.0f, alpha)); + } + + public int getSystemBrightness() { + if (mContext == null) { + return 127; + } + + // Try Float first + float brightFloat = Settings.System.getFloat( + mContext.getContentResolver(), + "screen_brightness_float", + -1.0f + ); + + if (brightFloat >= 0.0f) { + return (int) (brightFloat * 255.0f); + } + + // Fallback + return Settings.System.getInt( + mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS, + 127 + ); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 19d23bfcfb63..b2bf16b08142 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -79,6 +79,7 @@ import com.android.systemui.Flags; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.MtkUdfpsScrimController; import com.android.systemui.biometrics.dagger.BiometricsBackground; import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; @@ -241,6 +242,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { private boolean mAttemptedToDismissKeyguard; private final Set mCallbacks = new HashSet<>(); + private boolean mUseMtkGhbmDimming; + private UdfpsAnimation mUdfpsAnimation; private boolean mKeyguardCallbackRegistered = false; @@ -884,8 +887,12 @@ public UdfpsController(@NonNull @Main Context context, mDisableSmartPixels = mContext.getResources().getBoolean(com.android.systemui.res.R.bool.config_disableSmartPixelsOnUDFPS); + mUseMtkGhbmDimming = mContext.getResources().getBoolean( + com.android.systemui.res.R.bool.config_udfpsMtkGhbmDimming); + if (com.android.internal.util.matrixx.Utils.isPackageInstalled(mContext, "com.matrixx.udfps.animations")) { + updateUdfpsAnimation(); mConfigurationController.addCallback(mConfigurationListener); } @@ -1036,6 +1043,15 @@ private void hideUdfpsOverlay() { if (oldView != null) { onFingerUp(mOverlay.getRequestId(), oldView); } + + // Ensure HBM views are removed from WindowManager + if (mUseMtkGhbmDimming) { + View hbmView = mOverlay.getHbmView(); + if (hbmView != null) { + hbmView.setVisibility(View.GONE); + } + } + final boolean removed = mOverlay.hide(); mKeyguardViewManager.hideAlternateBouncer(true); hideUdfpsAnimation(); @@ -1268,6 +1284,30 @@ private void onFingerDown( mOnFingerDown = true; mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, pointerId, x, y, minor, major, orientation, time, gestureStart, isAod); + + if (mUseMtkGhbmDimming && mOverlay != null) { + View hbmView = mOverlay.getHbmView(); // Ensure UdfpsControllerOverlay exposes this + WindowManager.LayoutParams hbmParams = mOverlay.getHbmLayoutParamsFull(); + + if (hbmView != null && hbmParams != null) { + MtkUdfpsScrimController mtkController = MtkUdfpsScrimController.getInstance(mContext); + + int brightness = mtkController.getSystemBrightness(); + float alpha = mtkController.calculateAlpha(brightness); + + Log.d(TAG, "UDFPS Scrim: Brightness=" + brightness + " CalculatedAlpha=" + alpha); + + if (hbmView.getVisibility() != View.VISIBLE) { + hbmView.setVisibility(View.VISIBLE); + } + + if (Math.abs(hbmParams.alpha - alpha) > 0.001f) { + hbmParams.alpha = alpha; + mWindowManager.updateViewLayout(hbmView, hbmParams); + } + } + } + Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); final View view = mOverlay.getTouchOverlay(); @@ -1336,6 +1376,14 @@ private void onFingerUp( hideUdfpsAnimation(); } mOnFingerDown = false; + + if (mUseMtkGhbmDimming && mOverlay != null) { + View hbmView = mOverlay.getHbmView(); + if (hbmView != null) { + hbmView.setVisibility(View.GONE); + } + } + unconfigureDisplay(view); cancelAodSendFingerUpAction(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index a80a96dc53f4..ffdb2741125e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui.biometrics import android.annotation.SuppressLint import android.annotation.UiThread import android.content.Context +import android.graphics.Color import android.graphics.PixelFormat import android.graphics.Rect import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP @@ -131,8 +132,54 @@ constructor( null } - private val coreLayoutParams = - WindowManager.LayoutParams( + private val useMtkGhbmDimming = context.resources.getBoolean( + com.android.systemui.res.R.bool.config_udfpsMtkGhbmDimming + ) + + private val hbmDimLayerName = context.resources.getString( + com.android.systemui.res.R.string.config_udfpsHbmDimLayer + ) + + val hbmView: View? = if (useMtkGhbmDimming) { + View(context).apply { + setBackgroundColor(Color.BLACK) + visibility = View.INVISIBLE + } + } else null + + private val dimView: View? = if (useMtkGhbmDimming) { + View(context).apply { + setBackgroundColor(Color.TRANSPARENT) + } + } else null + + private var isAddDimView: Boolean = false + + private val coreLayoutParams: WindowManager.LayoutParams = if (useMtkGhbmDimming) { + WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ).apply { + title = TAG + fitInsetsTypes = 0 + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + flags = (WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + // Avoid announcing window title. + accessibilityTitle = " " + + inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + } + } else { + WindowManager.LayoutParams( WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 0 /* flags set in computeLayoutParams() */, PixelFormat.TRANSLUCENT, @@ -151,6 +198,92 @@ constructor( accessibilityTitle = " " inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY } + } + + // HBM Params (The high brightness layer) + private val hbmLayoutParams: WindowManager.LayoutParams? = if (useMtkGhbmDimming) { + WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ).apply { + title = hbmDimLayerName + fitInsetsTypes = 0 + alpha = 0.1f + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + flags = (WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + // Avoid announcing window title. + accessibilityTitle = " " + + inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + } + } else { + null + } + + // HBM Full Params + val hbmLayoutParamsFull: WindowManager.LayoutParams? = if (useMtkGhbmDimming) { + WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ).apply { + title = hbmDimLayerName + fitInsetsTypes = 0 + alpha = 0.1f + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + flags = (WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + // Avoid announcing window title. + accessibilityTitle = " " + + inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + } + } else { + null + } + + // Dim Params + private val dimLayoutParams: WindowManager.LayoutParams? = if (useMtkGhbmDimming) { + WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ).apply { + title = "UdfpsDim" + fitInsetsTypes = 0 + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + flags = (WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + accessibilityTitle = " " + + inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + } + } else { + null + } /** If the overlay is currently showing. */ val isShowing: Boolean @@ -266,7 +399,35 @@ constructor( if (Build.IS_DEBUGGABLE) { Log.d(TAG, "adding view=$view") } - windowManager.addView(view, coreLayoutParams.updateDimensions(animation)) + + if (useMtkGhbmDimming) { + hbmLayoutParams?.updateDimensions(animation) + hbmLayoutParamsFull?.updateDimensions(animation) + dimLayoutParams?.updateDimensions(animation) + + try { + windowManager.addView(hbmView, hbmLayoutParams) + windowManager.addView(dimView, dimLayoutParams) + isAddDimView = true + } catch (e: Exception) { + Log.e(TAG, "Failed to add vendor HBM/Dim views", e) + } + + windowManager.addView(view, coreLayoutParams.updateDimensions(animation)) + + if (requestReason == REASON_ENROLL_FIND_SENSOR || requestReason == REASON_ENROLL_ENROLLING) { + val child = view.findViewById(R.id.udfps_enroll_accessibility_view) + child?.let { + val lp = it.layoutParams + lp.width = sensorBounds.width() + lp.height = sensorBounds.height() + it.layoutParams = lp + it.requestLayout() + } + } + } else { + windowManager.addView(view, coreLayoutParams.updateDimensions(animation)) + } } if (powerInteractor.detailedWakefulness.value.isAwake()) { // Device is awake, so we add the view immediately. @@ -297,6 +458,12 @@ constructor( // no need to update any layouts. Instead the correct params will be used when the // view is eventually added. windowManager.updateViewLayout(it, coreLayoutParams.updateDimensions(null)) + + if (useMtkGhbmDimming && isAddDimView) { + hbmLayoutParamsFull?.updateDimensions(null) + windowManager.updateViewLayout(hbmView, hbmLayoutParams) + windowManager.updateViewLayout(dimView, dimLayoutParams) + } } } } @@ -320,6 +487,17 @@ constructor( } windowManager.removeView(this) } + + if (useMtkGhbmDimming && isAddDimView) { + try { + windowManager.removeView(hbmView) + windowManager.removeView(dimView) + } catch (e: Exception) { + Log.w(TAG, "Failed to remove HBM/Dim views", e) + } + isAddDimView = false + } + Trace.setCounter("UdfpsAddView", 0) setOnTouchListener(null) setOnHoverListener(null) From b080d1a953582068ef33b71f955686f4bf6e3390 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Tue, 12 May 2026 19:19:48 +0000 Subject: [PATCH 1178/1315] Revert "SystemUI: Smart Pixels [1/2]" Revert "SystemUI: Add Smart Pixels tile" This reverts commit b758a29bfee79f0805abfe780e7f3be32918086d. Revert "SystemUI: Allow devices to disable Smart Pixels on UDFPS" This reverts commit a7299651f9b3483e9988763369efeb4583f772fd. Revert "SystemUI: Smart Pixels [1/2]" This reverts commit 9c7d14073cba4b10283f82145880fd2b48fda64f. Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 24 -- core/res/res/values/matrixx_config.xml | 3 - core/res/res/values/matrixx_symbols.xml | 3 - packages/SystemUI/AndroidManifest.xml | 5 - .../res/drawable/ic_qs_smart_pixels.xml | 8 - packages/SystemUI/res/values/config.xml | 2 +- packages/SystemUI/res/values/cr_config.xml | 3 - .../SystemUI/res/values/matrixx_strings.xml | 4 - .../systemui/biometrics/UdfpsController.java | 65 ----- .../dagger/SystemUICoreStartableModule.kt | 7 - .../android/systemui/lineage/LineageModule.kt | 24 -- .../systemui/qs/tiles/SmartPixelsTile.java | 183 ------------- .../android/systemui/smartpixels/Grids.java | 148 ----------- .../smartpixels/SmartPixelsReceiver.java | 168 ------------ .../smartpixels/SmartPixelsService.java | 244 ------------------ 15 files changed, 1 insertion(+), 890 deletions(-) delete mode 100644 packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java delete mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/Grids.java delete mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsReceiver.java delete mode 100644 packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsService.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 64c0ca92c991..40db0cfe50fe 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7645,30 +7645,6 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String FORCE_FULLSCREEN_CUTOUT_APPS = "force_full_screen_cutout_apps"; - /** - * Whether to enable Smart Pixels - * @hide - */ - public static final String SMART_PIXELS_ENABLE = "smart_pixels_enable"; - - /** - * Smart Pixels pattern - * @hide - */ - public static final String SMART_PIXELS_PATTERN = "smart_pixels_pattern"; - - /** - * Smart Pixels Shift Timeout - * @hide - */ - public static final String SMART_PIXELS_SHIFT_TIMEOUT = "smart_pixels_shift_timeout"; - - /** - * Whether Smart Pixels should enable on power saver mode - * @hide - */ - public static final String SMART_PIXELS_ON_POWER_SAVE = "smart_pixels_on_power_save"; - /** * Defines the screen-off animation to display * @hide diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index b3b1ccab1b2f..95cf9c014d64 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -96,9 +96,6 @@ false false - - false - false diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index f73b72b4e922..28b9d172e4a8 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -82,9 +82,6 @@ - - - diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 411859e5db98..5cf52aab703a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -612,11 +612,6 @@ - - - diff --git a/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml b/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml deleted file mode 100644 index 525321baff96..000000000000 --- a/packages/SystemUI/res/drawable/ic_qs_smart_pixels.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 4e1506fea174..37e207f2c9ef 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume,five_g,vpn_tethering,sleep_mode + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume,five_g,vpn_tethering,sleep_mode diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index 5d1601462582..bd06529490dc 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -15,9 +15,6 @@ 3000 6000 - - false - /sys/class/thermal/thermal_zone0/temp diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index e0ffa984b2c3..a4b5976efced 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -147,10 +147,6 @@ Volume panel - - Smart Pixels - Auto-enabled Smart Pixels - Error loading weather data No weather data diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index b2bf16b08142..f15e6655f8b2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -247,11 +247,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { private UdfpsAnimation mUdfpsAnimation; private boolean mKeyguardCallbackRegistered = false; - private boolean mDisableSmartPixels; - private boolean mSmartPixelsFlag; - private boolean mSmartPixelsEnabled; - private boolean mSmartPixelsOnPowerSave; - @VisibleForTesting public static final VibrationAttributes UDFPS_VIBRATION_ATTRIBUTES = new VibrationAttributes.Builder() @@ -273,9 +268,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override public void onScreenTurnedOn() { - if (mDisableSmartPixels) { - isSmartPixelsEnabled(); - } mScreenOn = true; if (mAodInterruptRunnable != null) { mAodInterruptRunnable.run(); @@ -885,8 +877,6 @@ public UdfpsController(@NonNull @Main Context context, mWakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver); } - mDisableSmartPixels = mContext.getResources().getBoolean(com.android.systemui.res.R.bool.config_disableSmartPixelsOnUDFPS); - mUseMtkGhbmDimming = mContext.getResources().getBoolean( com.android.systemui.res.R.bool.config_udfpsMtkGhbmDimming); @@ -909,47 +899,6 @@ private void updateUdfpsAnimation() { } } - private void isSmartPixelsEnabled() { - if (!mSmartPixelsFlag) { - mSmartPixelsEnabled = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, - 0, mContext.getUserId()) != 0; - Log.i(TAG, "SmartPixels: SmartPixels enabled - " + mSmartPixelsEnabled); - mSmartPixelsOnPowerSave = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, mContext.getUserId()) != 0; - Log.i(TAG, "SmartPixels: SmartPixels on Power Save enabled - " + mSmartPixelsOnPowerSave); - } - } - - private void disableSmartPixels() { - Log.i(TAG, "SmartPixels: Disable SmartPixels"); - if (mSmartPixelsEnabled) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ENABLE, - 0, mContext.getUserId()); - } - if (mSmartPixelsOnPowerSave) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, mContext.getUserId()); - } - } - - private void enableSmartPixels() { - Log.i(TAG, "SmartPixels: Enable SmartPixels"); - if (mSmartPixelsEnabled) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ENABLE, - 1, mContext.getUserId()); - } - if (mSmartPixelsOnPowerSave) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 1, mContext.getUserId()); - } - } - /** * If a11y touchExplorationEnabled, play haptic to signal UDFPS scanning started. */ @@ -1258,12 +1207,6 @@ private void onFingerDown( + " current: " + mOverlay.getRequestId()); return; } - if (mDisableSmartPixels) { - if (!mSmartPixelsFlag && (mSmartPixelsEnabled || mSmartPixelsOnPowerSave)) { - disableSmartPixels(); - } - mSmartPixelsFlag = true; - } if (isOptical()) { mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE); } @@ -1357,14 +1300,6 @@ private void onFingerUp( mExecution.assertIsMainThread(); mActivePointerId = MotionEvent.INVALID_POINTER_ID; mAcquiredReceived = false; - - if (mDisableSmartPixels) { - if (mSmartPixelsFlag && (mSmartPixelsEnabled || mSmartPixelsOnPowerSave)) { - enableSmartPixels(); - } - mSmartPixelsFlag = false; - } - if (mOnFingerDown) { mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x, y, minor, major, orientation, time, gestureStart, isAod); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 054d4c6333db..fdb645146b86 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -54,7 +54,6 @@ import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerR import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable import com.android.systemui.shortcut.ShortcutKeyDispatcher -import com.android.systemui.smartpixels.SmartPixelsReceiver import com.android.systemui.statusbar.ImmersiveModeConfirmation import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import com.android.systemui.statusbar.gesture.GesturePointerEventListener @@ -338,12 +337,6 @@ abstract class SystemUICoreStartableModule { keyGestureEventInitializer: SysUIKeyGestureEventInitializer ): CoreStartable - /** Inject into SmartPixelsReceiver. */ - @Binds - @IntoMap - @ClassKey(SmartPixelsReceiver::class) - abstract fun bindSmartPixelsReceiver(sysui: SmartPixelsReceiver): CoreStartable - @Binds @IntoMap @ClassKey(UsbModePickerDialogDelegate::class) diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 2e7abedc5b1d..dbc6fcb8913e 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -38,7 +38,6 @@ import com.android.systemui.qs.tiles.ReadingModeTile import com.android.systemui.qs.tiles.RefreshRateTile import com.android.systemui.qs.tiles.ScreenshotTile import com.android.systemui.qs.tiles.SleepModeTile -import com.android.systemui.qs.tiles.SmartPixelsTile import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile @@ -167,12 +166,6 @@ interface LineageModule { @StringKey(SleepModeTile.TILE_SPEC) fun bindSleepModeTile(sleepModeTile: SleepModeTile): QSTileImpl<*> - /** Inject SmartPixelsTile into tileMap in QSModule */ - @Binds - @IntoMap - @StringKey(SmartPixelsTile.TILE_SPEC) - fun bindSmartPixelsTile(smartPixelsTile: SmartPixelsTile): QSTileImpl<*> - /** Inject SoundTile into tileMap in QSModule */ @Binds @IntoMap @@ -505,23 +498,6 @@ interface LineageModule { category = TileCategory.UTILITIES, ) - @Provides - @IntoMap - @StringKey(SmartPixelsTile.TILE_SPEC) - fun provideSmartPixelsTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = - QSTileConfig( - tileSpec = TileSpec.create(SmartPixelsTile.TILE_SPEC), - uiConfig = - QSTileUIConfig.Resource( - iconRes = R.drawable.ic_qs_smart_pixels, - labelRes = R.string.quick_settings_smart_pixels - ), - instanceId = uiEventLogger.getNewInstanceId(), - category = TileCategory.DISPLAY, - ) - - @Provides - @IntoMap @StringKey(SoundTile.TILE_SPEC) fun provideSoundConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java deleted file mode 100644 index 8755bd217fa3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SmartPixelsTile.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2018 CarbonROM - * Copyright (C) 2018 Adin Kwok (adinkwok) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.tiles; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.os.Looper; -import android.os.PowerManager; -import android.os.UserHandle; -import android.provider.Settings; -import android.service.quicksettings.Tile; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.animation.Expandable; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSTile.BooleanState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.QsEventLogger; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.res.R; -import com.android.systemui.statusbar.policy.BatteryController; - -import javax.inject.Inject; - -public class SmartPixelsTile extends QSTileImpl implements - BatteryController.BatteryStateChangeCallback { - - public static final String TILE_SPEC = "smartpixels"; - - - private static final Intent SMART_PIXELS_SETTINGS = new Intent("android.settings.SMART_PIXELS_SETTINGS"); - - private final BatteryController mBatteryController; - - private boolean mSmartPixelsEnable; - private boolean mSmartPixelsOnPowerSave; - private boolean mLowPowerMode; - private boolean mListening; - - @Nullable - private Icon mIcon = null; - - @Inject - public SmartPixelsTile( - QSHost host, - QsEventLogger uiEventLogger, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - BatteryController batteryController) { - super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - - mBatteryController = batteryController; - } - - @Override - public BooleanState newTileState() { - return new BooleanState(); - } - - @Override - public void handleSetListening(boolean listening) { - if (listening) { - mBatteryController.addCallback(this); - } else { - mBatteryController.removeCallback(this); - } - } - - @Override - public boolean isAvailable() { - return mContext.getResources(). - getBoolean(com.android.internal.R.bool.config_supportSmartPixels); - } - - @Override - public void handleClick(@Nullable Expandable expandable) { - mSmartPixelsEnable = (Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, - 0, UserHandle.USER_CURRENT) == 1); - mSmartPixelsOnPowerSave = (Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, UserHandle.USER_CURRENT) == 1); - if (mLowPowerMode && mSmartPixelsOnPowerSave) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, UserHandle.USER_CURRENT); - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ENABLE, - 0, UserHandle.USER_CURRENT); - } else if (!mSmartPixelsEnable) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ENABLE, - 1, UserHandle.USER_CURRENT); - } else { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SMART_PIXELS_ENABLE, - 0, UserHandle.USER_CURRENT); - } - refreshState(); - } - - @Override - public Intent getLongClickIntent() { - return SMART_PIXELS_SETTINGS; - } - - @Override - protected void handleUpdateState(BooleanState state, Object arg) { - mSmartPixelsEnable = (Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ENABLE, - 0, UserHandle.USER_CURRENT) == 1); - mSmartPixelsOnPowerSave = (Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, UserHandle.USER_CURRENT) == 1); - if (mIcon == null) { - mIcon = maybeLoadResourceIcon(R.drawable.ic_qs_smart_pixels); - } - state.icon = mIcon; - if (mLowPowerMode && mSmartPixelsOnPowerSave) { - state.label = mContext.getString(R.string.quick_settings_smart_pixels_on_power_save); - state.value = true; - } else if (mSmartPixelsEnable) { - state.label = mContext.getString(R.string.quick_settings_smart_pixels); - state.value = true; - } else { - state.label = mContext.getString(R.string.quick_settings_smart_pixels); - state.value = false; - } - state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_smart_pixels); - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.MATRIXX; - } - - @Override - public void onBatteryLevelChanged(int level, boolean plugged, boolean charging) { - // yurt - } - - @Override - public void onPowerSaveChanged(boolean active) { - mLowPowerMode = active; - refreshState(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixels/Grids.java b/packages/SystemUI/src/com/android/systemui/smartpixels/Grids.java deleted file mode 100644 index e29381c38485..000000000000 --- a/packages/SystemUI/src/com/android/systemui/smartpixels/Grids.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2015, Sergii Pylypenko - * (c) 2018, Joe Maples - * (c) 2018, CarbonROM - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of screen-dimmer-pixel-filter nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -package com.android.systemui.smartpixels; - -public class Grids { - - public static final int GridSize = 64; - public static final int GridSideSize = 8; - - public static String[] PatternNames = new String[] { - "12%", - "25%", - "38%", - "50%", - "62%", - "75%", - "88%", - }; - - public static byte[][] Patterns = new byte[][] { - { - 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 0, - 0, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, 0, 0, 0, - }, - { - 1, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 1, 0, - 0, 1, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 1, 0, - 0, 1, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 1, - }, - { - 1, 0, 0, 0, 1, 0, 0, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 0, 0, 1, 0, 0, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 1, 0, 0, 0, 1, 0, 0, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 0, 0, 1, 0, 0, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - }, - { - 1, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, 1, 0, 1, - }, - { - 0, 1, 1, 1, 0, 1, 1, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 1, 1, 0, 1, 1, 1, 0, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 0, 1, 1, 1, 0, 1, 1, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - 1, 1, 0, 1, 1, 1, 0, 1, - 1, 0, 1, 0, 1, 0, 1, 0, - }, - { - 0, 1, 1, 1, 0, 1, 1, 1, - 1, 1, 0, 1, 1, 1, 0, 1, - 1, 0, 1, 1, 1, 0, 1, 1, - 1, 1, 1, 0, 1, 1, 1, 0, - 0, 1, 1, 1, 0, 1, 1, 1, - 1, 1, 0, 1, 1, 1, 0, 1, - 1, 0, 1, 1, 1, 0, 1, 1, - 1, 1, 1, 0, 1, 1, 1, 0, - }, - { - 0, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 0, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 0, 1, - 1, 0, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 0, 1, 1, - 1, 1, 0, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 0, - 1, 1, 1, 1, 0, 1, 1, 1, - }, - }; - - // Indexes to shift screen pattern in both vertical and horizontal directions - public static byte[] GridShift = new byte[] { - 0, 1, 8, 9, 2, 3, 10, 11, - 4, 5, 12, 13, 6, 7, 14, 15, - 16, 17, 24, 25, 18, 19, 26, 27, - 20, 21, 28, 29, 22, 23, 30, 31, - 32, 33, 40, 41, 34, 35, 42, 43, - 36, 37, 44, 45, 38, 39, 46, 47, - 48, 49, 56, 57, 50, 51, 58, 59, - 52, 53, 60, 61, 54, 55, 62, 63, - }; - - public static int[] ShiftTimeouts = new int[] { // In milliseconds - 15 * 1000, - 30 * 1000, - 60 * 1000, - 2 * 60 * 1000, - 5 * 60 * 1000, - 10 * 60 * 1000, - 20 * 60 * 1000, - 30 * 60 * 1000, - 60 * 60 * 1000, - }; - -} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsReceiver.java b/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsReceiver.java deleted file mode 100644 index d00c3c788d4c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsReceiver.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2018 CarbonROM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.smartpixels; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.ContentObserver; -import android.os.Handler; -import android.os.PowerManager; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.Log; - -import com.android.systemui.CoreStartable; -import com.android.systemui.dagger.SysUISingleton; - -import javax.inject.Inject; - -@SysUISingleton -public class SmartPixelsReceiver extends BroadcastReceiver implements CoreStartable { - private static final String TAG = "SmartPixelsReceiver"; - - private Context mContext; - private Handler mHandler = new Handler(); - private ContentResolver mResolver; - private PowerManager mPowerManager; - private SettingsObserver mSettingsObserver; - private Intent mSmartPixelsService; - private IntentFilter mFilter; - - private boolean mEnabled; - private boolean mOnPowerSave; - private boolean mPowerSave; - private boolean mServiceRunning = false; - private boolean mRegisteredReceiver = false; - - @Inject - public SmartPixelsReceiver(Context context) { - mContext = context; - } - - @Override - public void start() { - if (!mContext.getResources(). - getBoolean(com.android.internal.R.bool.config_supportSmartPixels)) - return; - - mSmartPixelsService = new Intent(mContext, - com.android.systemui.smartpixels.SmartPixelsService.class); - - mFilter = new IntentFilter(); - mFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); - mFilter.addAction(Intent.ACTION_USER_FOREGROUND); - - initiateSettingsObserver(); - } - - private void registerReceiver() { - mContext.registerReceiver(this, mFilter, Context.RECEIVER_NOT_EXPORTED); - mRegisteredReceiver = true; - } - - private void unregisterReceiver() { - mContext.unregisterReceiver(this); - mRegisteredReceiver = false; - } - - private void initiateSettingsObserver() { - mResolver = mContext.getContentResolver(); - mSettingsObserver = new SettingsObserver(mHandler); - mSettingsObserver.observe(); - mSettingsObserver.update(); - } - - private class SettingsObserver extends ContentObserver { - SettingsObserver(Handler handler) { - super(handler); - mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - } - - void observe() { - mResolver.registerContentObserver(Settings.System.getUriFor( - Settings.System.SMART_PIXELS_ENABLE), - false, this, UserHandle.USER_ALL); - mResolver.registerContentObserver(Settings.System.getUriFor( - Settings.System.SMART_PIXELS_ON_POWER_SAVE), - false, this, UserHandle.USER_ALL); - mResolver.registerContentObserver(Settings.System.getUriFor( - Settings.System.SMART_PIXELS_PATTERN), - false, this, UserHandle.USER_ALL); - mResolver.registerContentObserver(Settings.System.getUriFor( - Settings.System.SMART_PIXELS_SHIFT_TIMEOUT), - false, this, UserHandle.USER_ALL); - } - - @Override - public void onChange(boolean selfChange) { - update(); - } - - public void update() { - mEnabled = (Settings.System.getIntForUser( - mResolver, Settings.System.SMART_PIXELS_ENABLE, - 0, UserHandle.USER_CURRENT) == 1); - mOnPowerSave = (Settings.System.getIntForUser( - mResolver, Settings.System.SMART_PIXELS_ON_POWER_SAVE, - 0, UserHandle.USER_CURRENT) == 1); - mPowerSave = mPowerManager.isPowerSaveMode(); - - if (mEnabled || mOnPowerSave) { - if (!mRegisteredReceiver) - registerReceiver(); - } else if (mRegisteredReceiver) { - unregisterReceiver(); - } - - if (!mEnabled && mOnPowerSave) { - if (mPowerSave && !mServiceRunning) { - mContext.startService(mSmartPixelsService); - mServiceRunning = true; - Log.d(TAG, "Started Smart Pixels Service by Power Save enable"); - } else if (!mPowerSave && mServiceRunning) { - mContext.stopService(mSmartPixelsService); - mServiceRunning = false; - Log.d(TAG, "Stopped Smart Pixels Service by Power Save disable"); - } else if (mPowerSave && mServiceRunning) { - mContext.stopService(mSmartPixelsService); - mContext.startService(mSmartPixelsService); - Log.d(TAG, "Restarted Smart Pixels Service by Power Save enable"); - } - } else if (mEnabled && !mServiceRunning) { - mContext.startService(mSmartPixelsService); - mServiceRunning = true; - Log.d(TAG, "Started Smart Pixels Service by enable"); - } else if (!mEnabled && mServiceRunning) { - mContext.stopService(mSmartPixelsService); - mServiceRunning = false; - Log.d(TAG, "Stopped Smart Pixels Service by disable"); - } else if (mEnabled && mServiceRunning) { - mContext.stopService(mSmartPixelsService); - mContext.startService(mSmartPixelsService); - Log.d(TAG, "Restarted Smart Pixels Service"); - } - } - } - - @Override - public void onReceive(final Context context, Intent intent) { - mSettingsObserver.update(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsService.java b/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsService.java deleted file mode 100644 index 759e889b3170..000000000000 --- a/packages/SystemUI/src/com/android/systemui/smartpixels/SmartPixelsService.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2015, Sergii Pylypenko - * (c) 2018, Joe Maples - * (c) 2018, Adin Kwok - * (c) 2018, CarbonROM - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of screen-dimmer-pixel-filter nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -package com.android.systemui.smartpixels; - -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; - -import android.Manifest; -import android.app.Service; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.graphics.Point; -import android.graphics.Shader; -import android.graphics.drawable.BitmapDrawable; -import android.os.Handler; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.View; -import android.view.WindowManager; -import android.widget.ImageView; - -import com.android.systemui.res.R; - -public class SmartPixelsService extends Service { - public static final String LOG = "SmartPixelsService"; - - private WindowManager windowManager; - private ImageView view = null; - private Bitmap bmp; - - private boolean destroyed = false; - public static boolean running = false; - - private int startCounter = 0; - private Context mContext; - - // Pixel Filter Settings - private int mPattern = 3; - private int mShiftTimeout = 4; - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - running = true; - mContext = this; - updateSettings(); - Log.d(LOG, "Service started"); - startFilter(); - } - - public void startFilter() { - if (view != null) { - return; - } - - windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - - view = new ImageView(this); - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getRealMetrics(metrics); - bmp = Bitmap.createBitmap(Grids.GridSideSize, Grids.GridSideSize, Bitmap.Config.ARGB_4444); - - updatePattern(); - BitmapDrawable draw = new BitmapDrawable(bmp); - draw.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); - draw.setFilterBitmap(false); - draw.setAntiAlias(false); - draw.setTargetDensity(metrics.densityDpi); - - view.setBackground(draw); - - WindowManager.LayoutParams params = getLayoutParams(); - params.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY; - try { - windowManager.addView(view, params); - } catch (Exception e) { - running = false; - view = null; - return; - } - - startCounter++; - final int handlerStartCounter = startCounter; - final Handler handler = new Handler(); - final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - handler.postDelayed(new Runnable() { - @Override - public void run() { - if (view == null || destroyed || handlerStartCounter != startCounter) { - return; - } else if (pm.isInteractive()) { - updatePattern(); - view.invalidate(); - } - if (!destroyed) { - handler.postDelayed(this, Grids.ShiftTimeouts[mShiftTimeout]); - } - } - }, Grids.ShiftTimeouts[mShiftTimeout]); - } - - public void stopFilter() { - if (view == null) { - return; - } - - startCounter++; - - windowManager.removeView(view); - view = null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - destroyed = true; - stopFilter(); - Log.d(LOG, "Service stopped"); - running = false; - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Log.d(LOG, "Screen orientation changed, updating window layout"); - WindowManager.LayoutParams params = getLayoutParams(); - windowManager.updateViewLayout(view, params); - } - - private WindowManager.LayoutParams getLayoutParams() - { - Point displaySize = new Point(); - windowManager.getDefaultDisplay().getRealSize(displaySize); - Point windowSize = new Point(); - windowManager.getDefaultDisplay().getRealSize(windowSize); - Resources res = getResources(); - int mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height); - displaySize.x += displaySize.x - windowSize.x + (mStatusBarHeight * 2); - displaySize.y += displaySize.y - windowSize.y + (mStatusBarHeight * 2); - - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - displaySize.x, - displaySize.y, - 0, - 0, - WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | - WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN | - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | - WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, - PixelFormat.TRANSPARENT - ); - - // Use the rounded corners overlay to hide it from screenshots. See 132c9f514. - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY; - - params.dimAmount = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; - params.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE; - return params; - } - - private int getShift() { - long shift = (System.currentTimeMillis() / Grids.ShiftTimeouts[mShiftTimeout]) % Grids.GridSize; - return Grids.GridShift[(int)shift]; - } - - private void updatePattern() { - int shift = getShift(); - int shiftX = shift % Grids.GridSideSize; - int shiftY = shift / Grids.GridSideSize; - for (int i = 0; i < Grids.GridSize; i++) { - int x = (i + shiftX) % Grids.GridSideSize; - int y = ((i / Grids.GridSideSize) + shiftY) % Grids.GridSideSize; - int color = (Grids.Patterns[mPattern][i] == 0) ? Color.TRANSPARENT : Color.BLACK; - bmp.setPixel(x, y, color); - } - } - - private void updateSettings() { - mPattern = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_PATTERN, - 5, UserHandle.USER_CURRENT); - mShiftTimeout = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.SMART_PIXELS_SHIFT_TIMEOUT, - 4, UserHandle.USER_CURRENT); - } -} From 06500fa69c5a0c59b5531350e52aa3d82897872f Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:08:43 +0800 Subject: [PATCH 1179/1315] SystemUI: Add smart pixels support Change-Id: I003b0ef78309bbf18f0c7bf3d877102108f9931f Co-authored-by: Saikrishna1504 Co-authored-by: Joe Maples Co-authored-by: Sergii Pylypenko Co-authored-by: Adin Kwok Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/provider/Settings.java | 12 ++ .../res/drawable/qs_smart_pixels_icon_off.xml | 11 ++ .../res/drawable/qs_smart_pixels_icon_on.xml | 20 +++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/matrixx_strings.xml | 7 + .../dagger/SystemUICoreStartableModule.kt | 8 + .../android/systemui/lineage/LineageModule.kt | 24 +++ .../smartpixel/domain/SmartPixelSettings.kt | 89 ++++++++++ .../smartpixel/ui/SmartPixelDialogDelegate.kt | 154 ++++++++++++++++++ .../smartpixel/ui/SmartPixelManager.kt | 31 ++++ .../smartpixel/ui/SmartPixelOverlay.kt | 97 +++++++++++ .../systemui/smartpixel/ui/SmartPixelTile.kt | 150 +++++++++++++++++ .../systemui/smartpixel/ui/SmartPixelView.kt | 53 ++++++ .../smartpixel/ui/SmartPixelViewController.kt | 67 ++++++++ 14 files changed, 724 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/qs_smart_pixels_icon_off.xml create mode 100644 packages/SystemUI/res/drawable/qs_smart_pixels_icon_on.xml create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/domain/SmartPixelSettings.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelDialogDelegate.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelOverlay.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelTile.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelView.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelViewController.kt diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 40db0cfe50fe..afb7929eefc7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14758,6 +14758,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Readable public static final String PI_SNAPCHAT_SPOOF = "pi_snapchat_spoof"; + /** + * Whether to enable Smart Pixels + * @hide + */ + public static final String SMART_PIXEL_FILTER_ENABLED = "smart_pixel_filter_enabled"; + + /** + * Smart Pixels percentage + * @hide + */ + public static final String SMART_PIXEL_FILTER_PERCENT = "smart_pixel_filter_percent"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/res/drawable/qs_smart_pixels_icon_off.xml b/packages/SystemUI/res/drawable/qs_smart_pixels_icon_off.xml new file mode 100644 index 000000000000..1a7a98189f57 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_smart_pixels_icon_off.xml @@ -0,0 +1,11 @@ + + + + diff --git a/packages/SystemUI/res/drawable/qs_smart_pixels_icon_on.xml b/packages/SystemUI/res/drawable/qs_smart_pixels_icon_on.xml new file mode 100644 index 000000000000..fa110b9c3549 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_smart_pixels_icon_on.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 37e207f2c9ef..dca011ab89d0 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume,five_g,vpn_tethering,sleep_mode + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume,five_g,vpn_tethering,sleep_mode,smart_pixels diff --git a/packages/SystemUI/res/values/matrixx_strings.xml b/packages/SystemUI/res/values/matrixx_strings.xml index a4b5976efced..ac0e75137a0e 100644 --- a/packages/SystemUI/res/values/matrixx_strings.xml +++ b/packages/SystemUI/res/values/matrixx_strings.xml @@ -321,4 +321,11 @@ Screenshot + + + Smart Pixels + On + Off + Pixel percentage + %d%% diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index fdb645146b86..482cf797f3e9 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -54,6 +54,7 @@ import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerR import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable import com.android.systemui.shortcut.ShortcutKeyDispatcher +import com.android.systemui.smartpixel.ui.SmartPixelManager import com.android.systemui.statusbar.ImmersiveModeConfirmation import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsRefiner import com.android.systemui.statusbar.gesture.GesturePointerEventListener @@ -360,4 +361,11 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(AxAppLockerHelper::class) abstract fun bindAxAppLockerHelper(impl: AxAppLockerHelper): CoreStartable + + @Binds + @IntoMap + @ClassKey(SmartPixelManager::class) + abstract fun bindSmartPixelManager( + impl: SmartPixelManager + ): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index dbc6fcb8913e..1de8bc24d15b 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -49,6 +49,7 @@ import com.android.systemui.qs.tiles.WeatherTile import com.android.systemui.qs.tiles.base.shared.model.QSTileConfig import com.android.systemui.qs.tiles.base.shared.model.QSTileUIConfig import com.android.systemui.res.R +import com.android.systemui.smartpixel.ui.SmartPixelTile import dagger.Binds import dagger.Module @@ -166,6 +167,12 @@ interface LineageModule { @StringKey(SleepModeTile.TILE_SPEC) fun bindSleepModeTile(sleepModeTile: SleepModeTile): QSTileImpl<*> + /** Inject SmartPixelTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(SmartPixelTile.TILE_SPEC) + abstract fun bindSmartPixelTile(tile: SmartPixelTile): QSTileImpl<*> + /** Inject SoundTile into tileMap in QSModule */ @Binds @IntoMap @@ -498,6 +505,23 @@ interface LineageModule { category = TileCategory.UTILITIES, ) + @Provides + @IntoMap + @StringKey(SmartPixelTile.TILE_SPEC) + fun provideSmartPixelsTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(SmartPixelTile.TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_smart_pixels_icon_off, + labelRes = R.string.quick_settings_smart_pixels_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.DISPLAY, + ) + + @Provides + @IntoMap @StringKey(SoundTile.TILE_SPEC) fun provideSoundConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/domain/SmartPixelSettings.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/domain/SmartPixelSettings.kt new file mode 100644 index 000000000000..7e70977d9960 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/domain/SmartPixelSettings.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.domain + +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.SecureSettings +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class SmartPixelSettings @Inject constructor( + private val secureSettings: SecureSettings, + @Main private val mainExecutor: Executor, +) { + companion object { + const val KEY_ENABLED = Settings.Secure.SMART_PIXEL_FILTER_ENABLED + const val KEY_PERCENT = Settings.Secure.SMART_PIXEL_FILTER_PERCENT + + const val DEFAULT_PERCENT = 25 + const val MIN_PERCENT = 10 + const val MAX_PERCENT = 75 + } + + private val _isEnabled = MutableStateFlow(false) + val isEnabled: StateFlow = _isEnabled.asStateFlow() + + private val _percent = MutableStateFlow(DEFAULT_PERCENT) + val percent: StateFlow = _percent.asStateFlow() + + private val settingsObserver = + object : ContentObserver(mainExecutor, 0) { + override fun onChange(selfChange: Boolean) { + refresh() + } + } + + private var initialized = false + + fun init() { + if (initialized) return + initialized = true + refresh() + secureSettings.registerContentObserverForUserSync( + KEY_ENABLED, false, settingsObserver, UserHandle.USER_ALL, + ) + secureSettings.registerContentObserverForUserSync( + KEY_PERCENT, false, settingsObserver, UserHandle.USER_ALL, + ) + } + + fun setEnabled(enabled: Boolean) { + secureSettings.putIntForUser(KEY_ENABLED, if (enabled) 1 else 0, UserHandle.USER_CURRENT) + } + + fun setPercent(value: Int) { + secureSettings.putIntForUser( + KEY_PERCENT, value.coerceIn(MIN_PERCENT, MAX_PERCENT), UserHandle.USER_CURRENT, + ) + } + + private fun refresh() { + _isEnabled.value = + secureSettings.getIntForUser(KEY_ENABLED, 0, UserHandle.USER_CURRENT) == 1 + _percent.value = + secureSettings.getIntForUser(KEY_PERCENT, DEFAULT_PERCENT, UserHandle.USER_CURRENT) + .coerceIn(MIN_PERCENT, MAX_PERCENT) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelDialogDelegate.kt new file mode 100644 index 000000000000..e1f8c06bf5f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelDialogDelegate.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.compose.PlatformButton +import com.android.compose.theme.PlatformTheme +import com.android.systemui.smartpixel.domain.SmartPixelSettings +import com.android.systemui.smartpixel.domain.SmartPixelSettings.Companion.MAX_PERCENT +import com.android.systemui.smartpixel.domain.SmartPixelSettings.Companion.MIN_PERCENT +import com.android.systemui.dialog.ui.composable.AlertDialogContent +import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import javax.inject.Inject + +class SmartPixelDialogDelegate @Inject constructor( + private val sysuiDialogFactory: SystemUIDialogFactory, + private val settings: SmartPixelSettings, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, +) { + + fun createDialog(): SystemUIDialog = + sysuiDialogFactory.create(context = shadeDialogContextInteractor.context) { + SmartPixelDialogContent(it) + } + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + @Composable + private fun SmartPixelDialogContent(dialog: SystemUIDialog) { + val isCurrentlyInDarkTheme = isSystemInDarkTheme() + val cachedDarkTheme = remember { isCurrentlyInDarkTheme } + PlatformTheme(isDarkTheme = cachedDarkTheme) { + AlertDialogContent( + title = { Text(stringResource(R.string.quick_settings_smart_pixels_label)) }, + content = { SmartPixelControls() }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.quick_settings_done)) + } + }, + contentBottomPadding = 8.dp, + ) + } + } + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + @Composable + private fun SmartPixelControls() { + val isEnabled by settings.isEnabled.collectAsState() + val currentPercent by settings.percent.collectAsState() + var sliderValue by remember(currentPercent) { mutableFloatStateOf(currentPercent.toFloat()) } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource( + if (isEnabled) R.string.quick_settings_smart_pixels_on + else R.string.quick_settings_smart_pixels_off, + ), + style = MaterialTheme.typography.bodyLarge, + ) + Switch( + checked = isEnabled, + onCheckedChange = { settings.setEnabled(it) }, + thumbContent = { + Icon( + imageVector = if (isEnabled) Icons.Filled.Check else Icons.Filled.Clear, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + ) + } + + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.smart_pixels_percent_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringResource( + R.string.smart_pixels_percent_format, + sliderValue.toInt(), + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Slider( + value = sliderValue, + onValueChange = { sliderValue = it }, + onValueChangeFinished = { settings.setPercent(sliderValue.toInt()) }, + valueRange = MIN_PERCENT.toFloat()..MAX_PERCENT.toFloat(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + enabled = isEnabled, + ) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelManager.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelManager.kt new file mode 100644 index 000000000000..0a23cd2b8406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelManager.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class SmartPixelManager @Inject constructor( + private val overlay: SmartPixelOverlay, +) : CoreStartable { + + override fun start() { + overlay.init() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelOverlay.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelOverlay.kt new file mode 100644 index 000000000000..307966c0b1ce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelOverlay.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import android.content.Context +import android.graphics.PixelFormat +import android.view.WindowManager +import android.view.WindowManager.LayoutParams +import com.android.systemui.smartpixel.domain.SmartPixelSettings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn + +@SysUISingleton +class SmartPixelOverlay @Inject constructor( + @Application private val context: Context, + private val windowManager: WindowManager, + @Application private val applicationScope: CoroutineScope, + private val settings: SmartPixelSettings, +) { + private var filterView: SmartPixelView? = null + private var viewController: SmartPixelViewController? = null + + fun init() { + settings.init() + + combine( + settings.isEnabled, + settings.percent, + ) { enabled, percent -> + if (enabled) { + showOverlay() + viewController?.updateConfig(percent) + } else { + hideOverlay() + } + }.launchIn(applicationScope) + } + + private fun showOverlay() { + if (filterView != null) return + + val view = SmartPixelView(context) + val controller = SmartPixelViewController(view) + + val params = WindowManager.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + LayoutParams.FLAG_NOT_FOCUSABLE or + LayoutParams.FLAG_NOT_TOUCHABLE or + LayoutParams.FLAG_LAYOUT_IN_SCREEN or + LayoutParams.FLAG_LAYOUT_NO_LIMITS or + LayoutParams.FLAG_HARDWARE_ACCELERATED or + LayoutParams.FLAG_SHOW_WHEN_LOCKED, + PixelFormat.TRANSLUCENT, + ) + params.title = "SmartPixelFilter" + params.layoutInDisplayCutoutMode = + LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + params.privateFlags = params.privateFlags or + LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + + windowManager.addView(view, params) + controller.init() + controller.updateConfig(settings.percent.value) + + filterView = view + viewController = controller + } + + private fun hideOverlay() { + viewController?.destroy() + filterView?.let { view -> + windowManager.removeViewImmediate(view) + } + filterView = null + viewController = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelTile.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelTile.kt new file mode 100644 index 000000000000..a6055ff6368a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelTile.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.service.quicksettings.Tile +import com.google.android.material.materialswitch.MaterialSwitch +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.nano.MetricsProto.MetricsEvent +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.smartpixel.domain.SmartPixelSettings +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.qs.QSTile.BooleanState +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QsEventLogger +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.settings.SecureSettings +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Provider + +class SmartPixelTile @Inject constructor( + host: QSHost, + uiEventLogger: QsEventLogger, + @Background backgroundLooper: Looper, + @Main mainHandler: Handler, + falsingManager: FalsingManager, + metricsLogger: MetricsLogger, + statusBarStateController: StatusBarStateController, + activityStarter: ActivityStarter, + qsLogger: QSLogger, + private val secureSettings: SecureSettings, + private val keyguardStateController: KeyguardStateController, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogDelegateProvider: Provider, + @Main private val mainExecutor: Executor, +) : QSTileImpl( + host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, + metricsLogger, statusBarStateController, activityStarter, qsLogger, +) { + companion object { + const val TILE_SPEC = "smart_pixels" + private const val INTERACTION_JANK_TAG = "smart_pixels" + } + + override fun newTileState(): BooleanState { + val state = BooleanState() + state.handlesLongClick = true + return state + } + + override fun handleClick(expandable: Expandable?) { + val newState = !mState.value + secureSettings.putIntForUser( + SmartPixelSettings.KEY_ENABLED, + if (newState) 1 else 0, + UserHandle.USER_CURRENT, + ) + refreshState(newState) + } + + override fun handleLongClick(expandable: Expandable?) { + val animateFromExpandable = expandable != null && !keyguardStateController.isShowing + + val runnable = Runnable { + val dialog: SystemUIDialog = dialogDelegateProvider.get().createDialog() + if (animateFromExpandable) { + val controller = expandable?.dialogTransitionController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, + ) + ) + controller?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show() + } else { + dialog.show() + } + } + + mainExecutor.execute { + mActivityStarter.executeRunnableDismissingKeyguard( + runnable, + null, + true, + true, + false, + ) + } + } + + override fun getLongClickIntent(): Intent? = null + + override fun getTileLabel(): CharSequence = + mContext.getString(R.string.quick_settings_smart_pixels_label) + + override fun handleUpdateState(state: BooleanState, arg: Any?) { + val enabled = if (arg is Boolean) { + arg + } else { + secureSettings.getIntForUser( + SmartPixelSettings.KEY_ENABLED, 0, UserHandle.USER_CURRENT, + ) == 1 + } + state.value = enabled + state.label = mContext.getString(R.string.quick_settings_smart_pixels_label) + state.secondaryLabel = mContext.getString( + if (enabled) R.string.quick_settings_smart_pixels_on + else R.string.quick_settings_smart_pixels_off, + ) + state.contentDescription = state.label + state.expandedAccessibilityClassName = MaterialSwitch::class.java.name + state.state = if (enabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + state.icon = ResourceIcon.get( + if (enabled) R.drawable.qs_smart_pixels_icon_on + else R.drawable.qs_smart_pixels_icon_off + ) + } + + override fun isAvailable(): Boolean = true + + override fun getMetricsCategory(): Int = MetricsEvent.QS_PANEL +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelView.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelView.kt new file mode 100644 index 000000000000..b208f2ecbe6c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelView.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Shader +import android.view.View + +class SmartPixelView(context: Context) : View(context) { + + private val patternPaint = Paint() + private var patternBitmap: Bitmap? = null + + init { + setLayerType(LAYER_TYPE_HARDWARE, null) + } + + fun updatePattern(bitmap: Bitmap) { + patternBitmap?.recycle() + patternBitmap = bitmap + patternPaint.shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + invalidate() + } + + override fun onDraw(canvas: Canvas) { + if (patternBitmap == null) return + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), patternPaint) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + patternBitmap?.recycle() + patternBitmap = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelViewController.kt b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelViewController.kt new file mode 100644 index 000000000000..e22b73f439f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartpixel/ui/SmartPixelViewController.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartpixel.ui + +import android.graphics.Bitmap +import android.graphics.Color +import com.android.systemui.smartpixel.domain.SmartPixelSettings.Companion.MAX_PERCENT +import com.android.systemui.smartpixel.domain.SmartPixelSettings.Companion.MIN_PERCENT +import com.android.systemui.util.ViewController + +class SmartPixelViewController( + view: SmartPixelView, +) : ViewController(view) { + + companion object { + private const val TILE_SIZE = 8 + private const val TOTAL_PIXELS = TILE_SIZE * TILE_SIZE + + private val BAYER_MATRIX = intArrayOf( + 0, 32, 8, 40, 2, 34, 10, 42, + 48, 16, 56, 24, 50, 18, 58, 26, + 12, 44, 4, 36, 14, 46, 6, 38, + 60, 28, 52, 20, 62, 30, 54, 22, + 3, 35, 11, 43, 1, 33, 9, 41, + 51, 19, 59, 27, 49, 17, 57, 25, + 15, 47, 7, 39, 13, 45, 5, 37, + 63, 31, 55, 23, 61, 29, 53, 21, + ) + } + + fun updateConfig(percent: Int) { + rebuildPattern(percent.coerceIn(MIN_PERCENT, MAX_PERCENT)) + } + + private fun rebuildPattern(percent: Int) { + val threshold = (TOTAL_PIXELS * percent / 100f).toInt().coerceIn(1, TOTAL_PIXELS - 1) + val bmp = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888) + + for (y in 0 until TILE_SIZE) { + for (x in 0 until TILE_SIZE) { + if (BAYER_MATRIX[y * TILE_SIZE + x] < threshold) { + bmp.setPixel(x, y, Color.BLACK) + } + } + } + + mView.updatePattern(bmp) + } + + override fun onViewAttached() {} + + override fun onViewDetached() {} +} From 97829dadc951db674e531f3ae021b7b5b45cc6ef Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 29 Oct 2024 16:54:30 +0800 Subject: [PATCH 1180/1315] SystemUI: Send load-up hint on finger down * this prepares the HWUI for incoming ripple animation Signed-off-by: minaripenguin Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/biometrics/UdfpsController.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index f15e6655f8b2..4b9260725080 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -1207,6 +1207,13 @@ private void onFingerDown( + " current: " + mOverlay.getRequestId()); return; } + + final View view = mOverlay.getTouchOverlay(); + + if (view != null && view.getViewRootImpl() != null) { + view.getViewRootImpl().notifyRendererOfExpensiveFrame(); + } + if (isOptical()) { mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE); } @@ -1253,7 +1260,6 @@ private void onFingerDown( Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); - final View view = mOverlay.getTouchOverlay(); if (isOptical() && view instanceof UdfpsTouchOverlay udfpsView) { if (mIgnoreRefreshRate) { dispatchOnUiReady(requestId); @@ -1262,6 +1268,10 @@ private void onFingerDown( } } + if (view != null && view.getViewRootImpl() != null) { + view.getViewRootImpl().notifyRendererOfExpensiveFrame(); + } + if (isOptical()) { for (Callback cb : mCallbacks) { cb.onFingerDown(); From 3b9d69bd053ac87dd94c47398365d28ebafa9b12 Mon Sep 17 00:00:00 2001 From: John Galt Date: Wed, 11 Dec 2024 09:24:05 -0500 Subject: [PATCH 1181/1315] UdfpsController: add LAUNCH boost Based on logic from https://github.com/AOSPA/android_frameworks_base/commit/94cb933df8fa0cf2eb55c78e72f14b30f690e066 which used qcom perf framework opposed to cleaner libperfmgr. Massively improves performance on OP12, should improve performance on virtually any device with libperfmgr and a powerhint that has significant LAUNCH boosts. Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/systemui/biometrics/UdfpsController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 4b9260725080..573acf5a49c8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -53,6 +53,7 @@ import android.os.Handler; import android.os.Looper; import android.os.PowerManager; +import android.os.PowerManagerInternal; import android.os.Trace; import android.os.VibrationAttributes; import android.os.VibrationEffect; @@ -75,6 +76,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.UserActivityNotifier; +import com.android.server.LocalServices; import com.android.systemui.Dumpable; import com.android.systemui.Flags; import com.android.systemui.animation.ActivityTransitionAnimator; @@ -241,6 +243,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { private boolean mOnFingerDown; private boolean mAttemptedToDismissKeyguard; private final Set mCallbacks = new HashSet<>(); + PowerManagerInternal mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); private boolean mUseMtkGhbmDimming; @@ -1210,6 +1213,10 @@ private void onFingerDown( final View view = mOverlay.getTouchOverlay(); + if (mPowerManagerInternal != null) { + mPowerManagerInternal.setPowerMode(PowerManagerInternal.MODE_LAUNCH, true); + } + if (view != null && view.getViewRootImpl() != null) { view.getViewRootImpl().notifyRendererOfExpensiveFrame(); } @@ -1331,6 +1338,10 @@ private void onFingerUp( unconfigureDisplay(view); cancelAodSendFingerUpAction(); + if (mPowerManagerInternal != null) { + mPowerManagerInternal.setPowerMode(PowerManagerInternal.MODE_LAUNCH, false); + } + } public boolean isAnimationEnabled() { From aba8cd35a8f6ffa797251db0359f607347e072b9 Mon Sep 17 00:00:00 2001 From: ShevT Date: Thu, 15 Dec 2022 15:23:45 +0300 Subject: [PATCH 1182/1315] SystemUI: Allow devices to disable Smart Pixels on UDFPS With SmartPixels enabled, UDFPS does not work well. The higher the percentage of pixels to be disabled, the worse UDFPS works. Fix this by disabling SmartPixels when UDFPS is working. Change-Id: Ic478aa5d3a541d1ce533cfce7dacfd8ddec99ad0 Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res/values/cr_config.xml | 3 ++ .../systemui/biometrics/UdfpsController.java | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/packages/SystemUI/res/values/cr_config.xml b/packages/SystemUI/res/values/cr_config.xml index bd06529490dc..288bee62c90e 100644 --- a/packages/SystemUI/res/values/cr_config.xml +++ b/packages/SystemUI/res/values/cr_config.xml @@ -29,4 +29,7 @@ -1 + + + false diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 573acf5a49c8..9f06170c46a6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -250,6 +250,10 @@ public class UdfpsController implements DozeReceiver, Dumpable { private UdfpsAnimation mUdfpsAnimation; private boolean mKeyguardCallbackRegistered = false; + private boolean mDisableSmartPixels; + private boolean mSmartPixelsFlag; + private boolean mSmartPixelsEnabled; + @VisibleForTesting public static final VibrationAttributes UDFPS_VIBRATION_ATTRIBUTES = new VibrationAttributes.Builder() @@ -271,6 +275,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override public void onScreenTurnedOn() { + if (mDisableSmartPixels) { + isSmartPixelsEnabled(); + } mScreenOn = true; if (mAodInterruptRunnable != null) { mAodInterruptRunnable.run(); @@ -880,6 +887,8 @@ public UdfpsController(@NonNull @Main Context context, mWakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver); } + mDisableSmartPixels = mContext.getResources().getBoolean(com.android.systemui.res.R.bool.config_disableSmartPixelsOnUDFPS); + mUseMtkGhbmDimming = mContext.getResources().getBoolean( com.android.systemui.res.R.bool.config_udfpsMtkGhbmDimming); @@ -902,6 +911,30 @@ private void updateUdfpsAnimation() { } } + private void isSmartPixelsEnabled() { + if (!mSmartPixelsFlag) { + mSmartPixelsEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.SMART_PIXEL_FILTER_ENABLED, + 0, mContext.getUserId()) != 0; + } + } + + private void disableSmartPixels() { + if (mSmartPixelsEnabled) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.SMART_PIXEL_FILTER_ENABLED, + 0, mContext.getUserId()); + } + } + + private void enableSmartPixels() { + if (mSmartPixelsEnabled) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.SMART_PIXEL_FILTER_ENABLED, + 1, mContext.getUserId()); + } + } + /** * If a11y touchExplorationEnabled, play haptic to signal UDFPS scanning started. */ @@ -1221,6 +1254,13 @@ private void onFingerDown( view.getViewRootImpl().notifyRendererOfExpensiveFrame(); } + if (mDisableSmartPixels) { + if (!mSmartPixelsFlag && mSmartPixelsEnabled) { + disableSmartPixels(); + } + mSmartPixelsFlag = true; + } + if (isOptical()) { mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE); } @@ -1317,6 +1357,14 @@ private void onFingerUp( mExecution.assertIsMainThread(); mActivePointerId = MotionEvent.INVALID_POINTER_ID; mAcquiredReceived = false; + + if (mDisableSmartPixels) { + if (mSmartPixelsFlag && mSmartPixelsEnabled) { + enableSmartPixels(); + } + mSmartPixelsFlag = false; + } + if (mOnFingerDown) { mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x, y, minor, major, orientation, time, gestureStart, isAod); From fa71e3c520834b542a6437f85c96ed9e47685f5c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:14:37 +0800 Subject: [PATCH 1183/1315] services: Optimizing wallpaper controller traversals during simpleperf when performing app exit animation, setWallpaperZoomOut was performing full window tree scans on every frame of the zoom animation to find the maximum zoom level.this is wasteful. we should only triggers wallpaper token updates if the global maximum zoom level actually changes. Change-Id: I3bbe1293ab8dbeaacfea6a51f5e27b45b66ef357 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/wm/WallpaperController.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index ec6d226c8523..8ad9f85af86a 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -81,6 +81,7 @@ class WallpaperController { private WindowState mPrevWallpaperTarget = null; private float mLastWallpaperZoomOut = 0; + private WindowState mMaxZoomOutWindow = null; // Whether COMMAND_FREEZE was dispatched. private boolean mLastFrozen = false; @@ -185,6 +186,7 @@ private boolean isBackNavigationTarget(WindowState w) { if (!windowState.mIsWallpaper && Float.compare(windowState.mWallpaperZoomOut, mLastWallpaperZoomOut) > 0) { mLastWallpaperZoomOut = windowState.mWallpaperZoomOut; + mMaxZoomOutWindow = windowState; } }; @@ -528,11 +530,21 @@ void setWindowWallpaperPosition( void setWallpaperZoomOut(WindowState window, float zoom) { if (Float.compare(window.mWallpaperZoomOut, zoom) != 0) { + float oldMaxZoom = mLastWallpaperZoomOut; window.mWallpaperZoomOut = zoom; - computeLastWallpaperZoomOut(); - for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) { - final WallpaperWindowToken token = mWallpaperTokens.get(i); - token.updateWallpaperOffset(); + + if (Float.compare(zoom, mLastWallpaperZoomOut) > 0) { + mLastWallpaperZoomOut = zoom; + mMaxZoomOutWindow = window; + } else if (window == mMaxZoomOutWindow || Float.compare(oldMaxZoom, mLastWallpaperZoomOut) == 0) { + computeLastWallpaperZoomOut(); + } + + if (Float.compare(oldMaxZoom, mLastWallpaperZoomOut) != 0) { + for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) { + final WallpaperWindowToken token = mWallpaperTokens.get(i); + token.updateWallpaperOffset(); + } } } } @@ -796,7 +808,8 @@ void adjustWallpaperWindows() { } final boolean visibleRequested = - mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested(); + (mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested()) + || (mDisplayContent.mTransitionController.inTransition() && isWallpaperVisible()); updateWallpaperTokens(visibleRequested, mService.mFlags.mAodTransition ? mDisplayContent.isKeyguardLockedOrAodShowing() @@ -974,6 +987,7 @@ WindowState getTopVisibleWallpaper() { */ private void computeLastWallpaperZoomOut() { mLastWallpaperZoomOut = 0; + mMaxZoomOutWindow = null; mDisplayContent.forAllWindows(mComputeMaxZoomOutFunction, true); } From e4f4a591e7c90081305031d56b414202f62678c5 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:10:37 +0800 Subject: [PATCH 1184/1315] services: Optimizing wallpaper zoom Change-Id: I1549ec25043b749d86d3a806e83f6004090cd96d Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/Session.java | 28 ++++++++++++++-- .../server/wm/WallpaperController.java | 32 ++++++++++--------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index d591ec3e4c90..23b9d035073b 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -65,6 +65,7 @@ import android.os.UserHandle; import android.permission.PermissionManager; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.view.IWindow; @@ -129,6 +130,9 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private String mRelayoutTag; private final InsetsSourceControl.Array mDummyControls = new InsetsSourceControl.Array(); final boolean mSetsUnrestrictedKeepClearAreas; + private final ArrayMap mPendingWallpaperZoomOut = new ArrayMap<>(); + private final Runnable mApplyPendingWallpaperZoomOut = this::applyPendingWallpaperZoomOut; + private boolean mApplyWallpaperZoomOutPending; public Session(WindowManagerService service, IWindowSessionCallback callback) { this(service, callback, Binder.getCallingPid(), Binder.getCallingUid()); @@ -603,10 +607,30 @@ public void setWallpaperZoomOut(IBinder window, float zoom) { + zoom); } synchronized (mService.mGlobalLock) { + mPendingWallpaperZoomOut.put(window, zoom); + if (!mApplyWallpaperZoomOutPending) { + mApplyWallpaperZoomOutPending = true; + mService.mH.post(mApplyPendingWallpaperZoomOut); + } + } + } + + private void applyPendingWallpaperZoomOut() { + synchronized (mService.mGlobalLock) { + mApplyWallpaperZoomOutPending = false; + final int size = mPendingWallpaperZoomOut.size(); + if (size == 0) { + return; + } + final ArrayMap pending = new ArrayMap<>(mPendingWallpaperZoomOut); + mPendingWallpaperZoomOut.clear(); final long ident = Binder.clearCallingIdentity(); try { - actionOnWallpaper(window, (wpController, windowState) -> - wpController.setWallpaperZoomOut(windowState, zoom)); + for (int i = 0; i < size; i++) { + final float zoom = pending.valueAt(i); + actionOnWallpaper(pending.keyAt(i), (wpController, windowState) -> + wpController.setWallpaperZoomOut(windowState, zoom)); + } } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 8ad9f85af86a..04bcfa35dd1d 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -80,6 +80,7 @@ class WallpaperController { // to another, and this is the previous wallpaper target. private WindowState mPrevWallpaperTarget = null; + private static final float WALLPAPER_ZOOM_EPSILON = 0.003f; private float mLastWallpaperZoomOut = 0; private WindowState mMaxZoomOutWindow = null; @@ -529,22 +530,23 @@ void setWindowWallpaperPosition( } void setWallpaperZoomOut(WindowState window, float zoom) { - if (Float.compare(window.mWallpaperZoomOut, zoom) != 0) { - float oldMaxZoom = mLastWallpaperZoomOut; - window.mWallpaperZoomOut = zoom; - - if (Float.compare(zoom, mLastWallpaperZoomOut) > 0) { - mLastWallpaperZoomOut = zoom; - mMaxZoomOutWindow = window; - } else if (window == mMaxZoomOutWindow || Float.compare(oldMaxZoom, mLastWallpaperZoomOut) == 0) { - computeLastWallpaperZoomOut(); - } + if (Math.abs(window.mWallpaperZoomOut - zoom) < WALLPAPER_ZOOM_EPSILON) { + return; + } + final float oldMaxZoom = mLastWallpaperZoomOut; + window.mWallpaperZoomOut = zoom; - if (Float.compare(oldMaxZoom, mLastWallpaperZoomOut) != 0) { - for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) { - final WallpaperWindowToken token = mWallpaperTokens.get(i); - token.updateWallpaperOffset(); - } + if (zoom > mLastWallpaperZoomOut + WALLPAPER_ZOOM_EPSILON) { + mLastWallpaperZoomOut = zoom; + mMaxZoomOutWindow = window; + } else if (window == mMaxZoomOutWindow) { + computeLastWallpaperZoomOut(); + } + + if (Math.abs(oldMaxZoom - mLastWallpaperZoomOut) >= WALLPAPER_ZOOM_EPSILON) { + for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) { + final WallpaperWindowToken token = mWallpaperTokens.get(i); + token.updateWallpaperOffset(); } } } From f0d53bbb27838af4baaed0dd93bc26e50c45f806 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:12:49 +0800 Subject: [PATCH 1185/1315] services: Optimizing wms prop | settings reads the props and settings reads were being done at every window iteration - expensive, add an observer and add caching mechanism Change-Id: I5886612fe1e732d4c23043933b164097110c50c9 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/wm/WindowManagerService.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 0ee5de6840db..8f095be5db9b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -446,8 +446,10 @@ public class WindowManagerService extends IWindowManager.Stub /** Indicates we are removing the focused window when updating the focus. */ static final int UPDATE_FOCUS_REMOVING_FOCUS = 4; - private static final String SYSTEM_SECURE = "ro.secure"; - private static final String SYSTEM_DEBUGGABLE = "ro.debuggable"; + private static final boolean SYSTEM_SECURE = SystemProperties.getBoolean("ro.secure", true); + private static final boolean SYSTEM_DEBUGGABLE = + SystemProperties.getBoolean("ro.debuggable", false); + private static final String WINDOW_IGNORE_SECURE = "window_ignore_secure"; private static final String DENSITY_OVERRIDE = "ro.config.density_override"; private static final String SIZE_OVERRIDE = "ro.config.size_override"; @@ -838,6 +840,8 @@ final class SettingsObserver extends ContentObserver { Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS); private final Uri mDisableSecureWindowsUri = Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS); + private final Uri mWindowIgnoreSecureUri = + Settings.Secure.getUriFor(WINDOW_IGNORE_SECURE); private final Uri mMagnifyImeEnabledUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME); private final Uri mPolicyControlUri = @@ -872,6 +876,8 @@ public SettingsObserver() { UserHandle.USER_ALL); resolver.registerContentObserver(mDisableSecureWindowsUri, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mWindowIgnoreSecureUri, false, this, + UserHandle.USER_ALL); if (com.android.server.accessibility.Flags.enableMagnificationMagnifyNavBarAndIme()) { resolver.registerContentObserver(mMagnifyImeEnabledUri, false, this, UserHandle.USER_ALL); @@ -932,7 +938,7 @@ public void onChange(boolean selfChange, Uri uri) { return; } - if (mDisableSecureWindowsUri.equals(uri)) { + if (mDisableSecureWindowsUri.equals(uri) || mWindowIgnoreSecureUri.equals(uri)) { updateDisableSecureWindows(); return; } @@ -1054,23 +1060,26 @@ void updateDisplaySettingsLocation() { } void updateDisableSecureWindows() { - if (!SystemProperties.getBoolean(SYSTEM_DEBUGGABLE, false)) { - return; - } - - boolean disableSecureWindows; - try { - disableSecureWindows = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0; - } catch (Settings.SettingNotFoundException e) { - disableSecureWindows = false; + final ContentResolver resolver = mContext.getContentResolver(); + final boolean ignoreSecureWindows = + Settings.Secure.getInt(resolver, WINDOW_IGNORE_SECURE, 0) == 1; + boolean disableSecureWindows = false; + if (SYSTEM_DEBUGGABLE) { + try { + disableSecureWindows = Settings.Secure.getIntForUser(resolver, + Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0; + } catch (Settings.SettingNotFoundException e) { + disableSecureWindows = false; + } } - if (mDisableSecureWindows == disableSecureWindows) { + if (mDisableSecureWindows == disableSecureWindows + && mIgnoreSecureWindows == ignoreSecureWindows) { return; } synchronized (mGlobalLock) { mDisableSecureWindows = disableSecureWindows; + mIgnoreSecureWindows = ignoreSecureWindows; mRoot.refreshSecureSurfaceState(); } } @@ -1250,6 +1259,7 @@ public void onAppTransitionFinishedLocked(IBinder token) { private final ScreenRecordingCallbackController mScreenRecordingCallbackController; private volatile boolean mDisableSecureWindows = false; + private volatile boolean mIgnoreSecureWindows = false; /** Creates an instance of the WindowManagerService for the system server. */ public static WindowManagerService main(@NonNull final Context context, @@ -5469,8 +5479,7 @@ public boolean startViewServer(int port) { } private boolean isSystemSecure() { - return "1".equals(SystemProperties.get(SYSTEM_SECURE, "1")) && - "0".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0")); + return SYSTEM_SECURE && !SYSTEM_DEBUGGABLE; } /** @@ -7287,6 +7296,7 @@ private void dumpWindowsLocked(PrintWriter pw, boolean dumpAll, }); pw.print(" mBlurEnabled="); pw.println(mBlurController.getBlurEnabled()); pw.print(" mDisableSecureWindows="); pw.println(mDisableSecureWindows); + pw.print(" mIgnoreSecureWindows="); pw.println(mIgnoreSecureWindows); mInputManagerCallback.dump(pw, " "); mSnapshotController.dump(pw, " "); @@ -10932,8 +10942,7 @@ public void setGlobalDragListener(IGlobalDragListener listener) throws RemoteExc } boolean getDisableSecureWindows() { - return Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.WINDOW_IGNORE_SECURE, 0) == 1 || mDisableSecureWindows; + return mIgnoreSecureWindows || mDisableSecureWindows; } /** From 5bca78caf208c4f4ce3dfc550ecb36cf8424ee91 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:22:49 +0800 Subject: [PATCH 1186/1315] services: Optimizing letterbox updates simpleperf shows a large amount of cpu wasted due to redundant letterbox operations - add redundancy checks and skip letterbox updates when animating windows Change-Id: Id7a3b379daa38c3267074ccb016dacdb154fe4b6 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/wm/AppCompatLetterboxPolicy.java | 16 +++++++++++----- .../com/android/server/wm/DisplayContent.java | 6 +++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index 8af10b72f16d..2c222780ef8b 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -192,9 +192,14 @@ void start(@NonNull WindowState w) { if (shouldNotLayoutLetterbox(w)) { return; } - mAppCompatRoundedCorners.updateRoundedCornersIfNeeded(w); - updateWallpaperForLetterbox(w); - if (shouldShowLetterboxUi(w)) { + final boolean shouldShowLetterboxUi = shouldShowLetterboxUi(w); + final boolean isLetterboxedNotForDisplayCutout = shouldShowLetterboxUi + && !w.isLetterboxedForDisplayCutout(); + mAppCompatRoundedCorners.updateRoundedCornersIfNeeded(w, + isLetterboxedNotForDisplayCutout + && !isFreeformActivityMatchParentAppBoundsHeight()); + updateWallpaperForLetterbox(w, isLetterboxedNotForDisplayCutout); + if (shouldShowLetterboxUi) { mLetterboxPolicyState.layoutLetterboxIfNeeded(w); } else { mLetterboxPolicyState.hide(); @@ -273,7 +278,8 @@ void dump(@NonNull PrintWriter pw, @NonNull String prefix) { mAppCompatConfiguration.dump(pw, prefix); } - private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow) { + private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow, + boolean isLetterboxedNotForDisplayCutout) { final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord .mAppCompatController.getLetterboxOverrides(); final @LetterboxBackgroundType int letterboxBackgroundType = @@ -281,7 +287,7 @@ private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow) { boolean wallpaperShouldBeShown = letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER // Don't use wallpaper as a background if letterboxed for display cutout. - && isLetterboxedNotForDisplayCutout(mainWindow) + && isLetterboxedNotForDisplayCutout // Check that dark scrim alpha or blur radius are provided && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() > 0 || letterboxOverrides.getLetterboxWallpaperDarkScrimAlpha() > 0) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index c3c45769060b..bcf3f7a16bd3 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1112,7 +1112,11 @@ public void onIdMatch(InsetsSource source1, InsetsSource source2) { final ActivityRecord activity = w.mActivityRecord; if (activity != null && activity.isVisibleRequested()) { - activity.updateLetterboxSurfaceIfNeeded(w); + final int type = w.mAttrs.type; + if (!w.mAnimatingExit + && (type == TYPE_BASE_APPLICATION || type == TYPE_APPLICATION_STARTING)) { + activity.updateLetterboxSurfaceIfNeeded(w); + } final boolean updateAllDrawn = activity.updateDrawnWindowStates(w); if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(activity)) { mTmpUpdateAllDrawn.add(activity); From ac193fab08b46bd0f177bd58d4cf42ad1ca40faf Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:27:46 +0800 Subject: [PATCH 1187/1315] services: Optimizing window rounded corners updates Change-Id: Ib5409641d2b2b0a5399b91025bb5f4a735eb818d Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/wm/AppCompatRoundedCorners.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java index 812f1c4a86ac..ea93fb610192 100644 --- a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java +++ b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java @@ -36,6 +36,12 @@ class AppCompatRoundedCorners { private final ActivityRecord mActivityRecord; @NonNull private final Predicate mRoundedCornersWindowCondition; + @NonNull + private final Rect mLastCropBounds = new Rect(); + @Nullable + private SurfaceControl mLastSurfaceControl; + private boolean mLastCropBoundsValid; + private int mLastCornerRadius = Integer.MIN_VALUE; AppCompatRoundedCorners(@NonNull ActivityRecord activityRecord, @NonNull Predicate roundedCornersWindowCondition) { @@ -44,21 +50,51 @@ class AppCompatRoundedCorners { } void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow) { + updateRoundedCornersIfNeeded(mainWindow, mRoundedCornersWindowCondition.test(mainWindow)); + } + + void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow, + boolean roundedCornersWindowCondition) { final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); if (windowSurface == null || !windowSurface.isValid()) { return; } // cropBounds must be non-null for the cornerRadius to be ever applied. - mActivityRecord.getSyncTransaction() - .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) - .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + final boolean requiresRoundedCorners = requiresRoundedCorners(mainWindow, + roundedCornersWindowCondition); + final Rect cropBounds = getCropBoundsIfNeeded(mainWindow, requiresRoundedCorners); + final int cornerRadius = getRoundedCornersRadius(mainWindow, requiresRoundedCorners); + final boolean surfaceChanged = mLastSurfaceControl != windowSurface; + final SurfaceControl.Transaction transaction = mActivityRecord.getSyncTransaction(); + + if (surfaceChanged || !cropBoundsEquals(cropBounds)) { + transaction.setCrop(windowSurface, cropBounds); + mLastSurfaceControl = windowSurface; + if (cropBounds == null) { + mLastCropBoundsValid = false; + } else { + mLastCropBounds.set(cropBounds); + mLastCropBoundsValid = true; + } + } + if (surfaceChanged || mLastCornerRadius != cornerRadius) { + transaction.setCornerRadius(windowSurface, cornerRadius); + mLastSurfaceControl = windowSurface; + mLastCornerRadius = cornerRadius; + } } @VisibleForTesting @Nullable Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow)) { + return getCropBoundsIfNeeded(mainWindow, requiresRoundedCorners(mainWindow)); + } + + @Nullable + private Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow, + boolean requiresRoundedCorners) { + if (!requiresRoundedCorners) { // We don't want corner radius on the window. // In the case the ActivityRecord requires a letterboxed animation we never want // rounded corners on the window because rounded corners are applied at the @@ -108,7 +144,12 @@ Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) { * @param mainWindow The {@link WindowState} to consider for rounded corners calculation. */ int getRoundedCornersRadius(@NonNull final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow)) { + return getRoundedCornersRadius(mainWindow, requiresRoundedCorners(mainWindow)); + } + + private int getRoundedCornersRadius(@NonNull final WindowState mainWindow, + boolean requiresRoundedCorners) { + if (!requiresRoundedCorners) { return 0; } final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord @@ -134,6 +175,14 @@ private static int getInsetsStateCornerRadius(@NonNull InsetsState insetsState, } private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) { + return requiresRoundedCorners(mainWindow, mRoundedCornersWindowCondition.test(mainWindow)); + } + + private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow, + boolean roundedCornersWindowCondition) { + if (!roundedCornersWindowCondition) { + return false; + } if (com.android.wm.shell.Flags.enableCreateAnyBubble()) { final Task task = mActivityRecord.getTask(); if (task != null && task.mLaunchNextToBubble) { @@ -144,8 +193,14 @@ private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) { } final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord .mAppCompatController.getLetterboxOverrides(); - return mRoundedCornersWindowCondition.test(mainWindow) - && letterboxOverrides.isLetterboxActivityCornersRounded(); + return letterboxOverrides.isLetterboxActivityCornersRounded(); + } + + private boolean cropBoundsEquals(@Nullable Rect cropBounds) { + if (cropBounds == null) { + return !mLastCropBoundsValid; + } + return mLastCropBoundsValid && mLastCropBounds.equals(cropBounds); } } From d1dd919002f94548ddbe8b5d1cdaaa8cdc8e2267 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:29:14 +0800 Subject: [PATCH 1188/1315] services: Disabling dma buff stats pull simpleperf shows high cpu usage caused by dma stats pull during app launch, we dont care about dma buf stats, only enable it on eng builds Change-Id: Ifa76e5d0cfbc552a8257b43450e332d49c9c3ad8 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/stats/pull/StatsPullAtomService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index d200f69623f1..81bdc7650339 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -2696,6 +2696,9 @@ int pullProcessSystemIonHeapSizeLocked(int atomTag, List pulledData) } private void registerProcessDmabufMemory() { + if (!Build.IS_ENG) { + return; + } int tagId = FrameworkStatsLog.PROCESS_DMABUF_MEMORY; mStatsManager.setPullAtomCallback( tagId, @@ -2706,6 +2709,9 @@ private void registerProcessDmabufMemory() { } int pullProcessDmabufMemory(int atomTag, List pulledData) { + if (!Build.IS_ENG) { + return StatsManager.PULL_SKIP; + } KernelAllocationStats.ProcessDmabuf[] procBufs = KernelAllocationStats.getDmabufAllocations(); From 130723eec50be33535d13c63d405b6e85255a0d0 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 06:31:38 +0800 Subject: [PATCH 1189/1315] services: Optimizing redundant status bar top hide checks Change-Id: I128348eab541df5b6f0171bfdba402275a5df747 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/statusbar/StatusBarManagerService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index bb283153929c..87a56c466d36 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -207,6 +207,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private int mCurrentUserId; private boolean mTracingEnabled; private int mLastSystemKey = -1; + private volatile boolean mLastTopAppHidesStatusBar; + private volatile boolean mLastTopAppHidesStatusBarValid; private final TileRequestTracker mTileRequestTracker; @@ -229,6 +231,7 @@ private class DeathRecipient implements IBinder.DeathRecipient { public void binderDied() { mBar.asBinder().unlinkToDeath(this,0); mBar = null; + mLastTopAppHidesStatusBarValid = false; notifyBarAttachChanged(); } @@ -713,8 +716,14 @@ public void setTopAppHidesStatusBar(int displayId, boolean hidesStatusBar) { } IStatusBar bar = mBar; if (bar != null) { + if (mLastTopAppHidesStatusBarValid + && mLastTopAppHidesStatusBar == hidesStatusBar) { + return; + } try { bar.setTopAppHidesStatusBar(hidesStatusBar); + mLastTopAppHidesStatusBar = hidesStatusBar; + mLastTopAppHidesStatusBarValid = true; } catch (RemoteException ex) {} } } @@ -1809,6 +1818,7 @@ public RegisterStatusBarResult registerStatusBar(IStatusBar bar) { Slog.i(TAG, "registerStatusBar bar=" + bar); mBar = bar; + mLastTopAppHidesStatusBarValid = false; mDeathRecipient.linkToDeath(); notifyBarAttachChanged(); final ArrayMap icons; @@ -1833,6 +1843,7 @@ public Map registerStatusBarForAllDisplays(ISta Slog.i(TAG, "registerStatusBarForAllDisplays bar=" + bar); mBar = bar; + mLastTopAppHidesStatusBarValid = false; mDeathRecipient.linkToDeath(); notifyBarAttachChanged(); From c056f012eea844e23e6fc1d7e5ad262183326601 Mon Sep 17 00:00:00 2001 From: bijoyv9 Date: Sat, 25 Apr 2026 11:38:29 +0000 Subject: [PATCH 1190/1315] SystemUI: Prevent UDFPS screen flash during sleep transition On devices with optical UDFPS, starting fingerprint authentication while the screen is entering sleep can briefly trigger HBM/illumination and flash the panel. Gate UDFPS listening as soon as the sleep transition is dispatched, keep it blocked until the screen is fully off, and clear the gate on wakeup so fingerprint remains responsive. Change-Id: I9242668e2bfadf0e4bd7f2bc1ee68b749237293e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/keyguard/KeyguardUpdateMonitor.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a7260e4d6d6f..e8b0bf7ea567 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2157,18 +2157,16 @@ protected void handleStartedGoingToSleep(int arg1) { cb.onStartedGoingToSleep(arg1); } } - mGoingToSleep = true; // Resetting assistant visibility state as the device is going to sleep now. // TaskStackChangeListener gets triggered a little late when we transition to AoD, // which results in face auth running once on AoD. mAssistantVisible = false; - mLogger.d("Started going to sleep, mGoingToSleep=true, mAssistantVisible=false"); + mLogger.d("Started going to sleep, mAssistantVisible=false"); updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } protected void handleFinishedGoingToSleep(int arg1) { Assert.isMainThread(); - mGoingToSleep = false; for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -2181,7 +2179,9 @@ protected void handleFinishedGoingToSleep(int arg1) { private void handleScreenTurnedOff() { Assert.isMainThread(); mHardwareFingerprintUnavailableRetryCount = 0; + mGoingToSleep = false; ScrimUtils.get().onScreenTurnedOff(); + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } private void handleDreamingStateChanged(int dreamStart) { @@ -3196,7 +3196,8 @@ protected boolean shouldListenForFingerprint(boolean isUdfps) { boolean shouldListen = shouldListenKeyguardState && shouldListenUserState && shouldListenBouncerState && shouldListenUdfpsState && !mBiometricPromptShowing - && shouldListenSecureLockDeviceState && shouldListenFpsState && !mIsDeviceInPocket; + && shouldListenSecureLockDeviceState && shouldListenFpsState && !mIsDeviceInPocket + && (!mGoingToSleep || !isUdfps); logListenerModelData( new KeyguardFingerprintListenModel( System.currentTimeMillis(), @@ -4178,11 +4179,15 @@ public static boolean isSimPinSecure(int state) { public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) { synchronized (this) { mDeviceInteractive = true; + mGoingToSleep = false; } mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0)); } public void dispatchStartedGoingToSleep(int why) { + synchronized (this) { + mGoingToSleep = true; + } mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_GOING_TO_SLEEP, why, 0)); } From 9d32f67beb809b97b632477d0010e7a472beba51 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 16:15:36 +0800 Subject: [PATCH 1191/1315] fixup! SystemUI: Prevent UDFPS screen flash during sleep transition https://github.com/AxionAOSP/issue_tracker/issues/178 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../src/com/android/keyguard/KeyguardUpdateMonitor.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e8b0bf7ea567..07e37c46b2d0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -166,6 +166,7 @@ import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt; @@ -328,6 +329,7 @@ public void onExpandedChanged(boolean isExpanded) { } }; private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + private final Provider mDozeParameters; private final Object mSimDataLockObject = new Object(); HashMap mSimDatasBySlotId = new HashMap<>(); @@ -2173,6 +2175,9 @@ protected void handleFinishedGoingToSleep(int arg1) { cb.onFinishedGoingToSleep(arg1); } } + if (isUdfpsSupported() && mDozeParameters.get().getAlwaysOn()) { + mGoingToSleep = false; + } updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } @@ -2296,7 +2301,8 @@ protected KeyguardUpdateMonitor( Provider sceneInteractor, Provider communalSceneInteractor, Provider - keyguardServiceShowLockscreenInteractor) { + keyguardServiceShowLockscreenInteractor, + Provider dozeParameters) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2355,6 +2361,7 @@ protected KeyguardUpdateMonitor( if (mPocketManager != null) { mPocketManager.addCallback(mPocketCallback); } + mDozeParameters = dozeParameters; mHandler = new Handler(mainLooper) { @Override From 8c646c6cf1c6e4654ba29852f82f7b82b1464a0d Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 07:39:13 +0800 Subject: [PATCH 1192/1315] services: Fixing wallpaper token leak Change-Id: Ifab772e1d14185a2da6babd1773bad67ce727658 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../Shell/src/com/android/wm/shell/transition/Transitions.java | 1 + .../core/java/com/android/server/wm/WallpaperWindowToken.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 92c4fcce3e00..6683ca4f554c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -725,6 +725,7 @@ public void onTransitionReady(@NonNull IBinder transitionToken, @NonNull Transit if (existing != null) { Log.e(TAG, "Got duplicate transitionReady for " + transitionToken); // The transition is already somewhere else in the pipeline, so just return here. + info.releaseAnimSurfaces(); t.apply(); if (existing.mFinishT != null) { existing.mFinishT.merge(finishT); diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 94d24537bd24..937697d0d4fd 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -78,6 +78,9 @@ WallpaperWindowToken asWallpaperToken() { @Override void setExiting(boolean animateExit) { super.setExiting(animateExit); + if (mSurfaceControl != null) { + getSyncTransaction().setVisibility(mSurfaceControl, false); + } mDisplayContent.mWallpaperController.removeWallpaperToken(this); } From 4e0def06e41a7e8bf475e272f84b0a281c427187 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 10:16:01 +0800 Subject: [PATCH 1193/1315] core: Fixing zygote socket session leak Change-Id: Idaabdf5de4d80927041282d3ac29dcae7402c31f Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/com/android/internal/os/Zygote.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 168e4e5b347c..43e2e6a92ce2 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -852,6 +852,7 @@ private static Runnable childMain(@Nullable ZygoteCommandBuffer argBuffer, throw new RuntimeException(ex); } } + IoUtils.closeQuietly(sessionSocket); } if (writePipe != null) { From 23364e608a44c2d183661b397241a518c3f91e8a Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 11:16:35 +0800 Subject: [PATCH 1194/1315] core: Caching views to optimize performance Change-Id: Ia4d67a5ba70bd66d9711c46494b3c7492b8c2eac Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/view/LayoutInflater.java | 30 ++++ .../android/view/ViewBackgroundThread.java | 83 +++++++++++ core/java/android/view/ViewRootImpl.java | 6 + .../android/internal/util/ViewCacheData.java | 134 ++++++++++++++++++ .../internal/util/ViewCacheManager.java | 129 +++++++++++++++++ 5 files changed, 382 insertions(+) create mode 100644 core/java/android/view/ViewBackgroundThread.java create mode 100644 core/java/com/android/internal/util/ViewCacheData.java create mode 100644 core/java/com/android/internal/util/ViewCacheManager.java diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 3bb76b89b095..b42961ffc08b 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -39,6 +39,7 @@ import android.widget.FrameLayout; import com.android.internal.R; +import com.android.internal.util.ViewCacheManager; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -457,6 +458,35 @@ public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean a + Integer.toHexString(resource) + ")"); } + ViewCacheManager cache = ViewCacheManager.getInstance(); + cache.recordLayoutRes(resource); + if (cache.isEnable()) { + View cached = cache.tryGet(resource); + if (cached != null) { + if (root != null) { + try { + XmlResourceParser parser = res.getLayout(resource); + try { + AttributeSet attrs = Xml.asAttributeSet(parser); + advanceToRootNode(parser); + ViewGroup.LayoutParams params = root.generateLayoutParams(attrs); + if (!attachToRoot) { + cached.setLayoutParams(params); + return cached; + } + root.addView(cached, params); + return root; + } finally { + parser.close(); + } + } catch (Exception e) { + } + } else { + return cached; + } + } + } + XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); diff --git a/core/java/android/view/ViewBackgroundThread.java b/core/java/android/view/ViewBackgroundThread.java new file mode 100644 index 000000000000..bf7a0ae303e3 --- /dev/null +++ b/core/java/android/view/ViewBackgroundThread.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.view; + +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; + +import java.util.concurrent.Executor; + +/** + * @hide + */ +public final class ViewBackgroundThread extends HandlerThread { + private static final long SLOW_DELIVERY_THRESHOLD_MS = 30000; + private static final long SLOW_DISPATCH_THRESHOLD_MS = 10000; + private static volatile Handler sHandler; + private static HandlerExecutor sHandlerExecutor; + private static volatile ViewBackgroundThread sInstance; + + private ViewBackgroundThread() { + super("view.bg", Process.THREAD_PRIORITY_BACKGROUND); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new ViewBackgroundThread(); + sInstance.start(); + } + if (sHandler == null) { + Looper looper = sInstance.getLooper(); + looper.setSlowLogThresholdMs(SLOW_DISPATCH_THRESHOLD_MS, + SLOW_DELIVERY_THRESHOLD_MS); + sHandler = new Handler(sInstance.getLooper()); + sHandlerExecutor = new HandlerExecutor(sHandler); + } + } + + public static void init() { + synchronized (ViewBackgroundThread.class) { + if (sInstance == null) { + sInstance = new ViewBackgroundThread(); + sInstance.start(); + } + } + } + + public static ViewBackgroundThread get() { + synchronized (ViewBackgroundThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + + public static Handler getHandler() { + synchronized (ViewBackgroundThread.class) { + ensureThreadLocked(); + return sHandler; + } + } + + public static Executor getExecutor() { + synchronized (ViewBackgroundThread.class) { + ensureThreadLocked(); + return sHandlerExecutor; + } + } +} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 0f2fd0283ec5..e47bc2838295 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -297,6 +297,7 @@ import com.android.internal.policy.PhoneFallbackEventHandler; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.ViewCacheManager; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; import com.android.internal.view.SurfaceCallbackHelper; @@ -642,6 +643,7 @@ default void onConfigurationChanged(@NonNull Configuration overrideConfig, private boolean mPendingDragResizing; private boolean mDragResizing; private boolean mInvalidateRootRequested; + private boolean mFirstFrameDrawn = true; private int mCanvasOffsetX; private int mCanvasOffsetY; CompatibilityInfo.Translator mTranslator; @@ -3161,6 +3163,10 @@ void doTraversal() { mTraversalScheduled = false; mQueue.removeSyncBarrier(mTraversalBarrier); performTraversals(); + if (mFirstFrameDrawn) { + ViewCacheManager.getInstance().onTraversalEnd(this); + mFirstFrameDrawn = false; + } } } diff --git a/core/java/com/android/internal/util/ViewCacheData.java b/core/java/com/android/internal/util/ViewCacheData.java new file mode 100644 index 000000000000..5c916963450d --- /dev/null +++ b/core/java/com/android/internal/util/ViewCacheData.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util; + +import android.app.ActivityThread; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ViewCacheData { + private static final String FILE_NAME = "view-cache-data"; + private static final String KEY_DEPEND_UI = "depend_ui"; + + private final SharedPreferences mPrefs; + private final Map mCachedData = new HashMap<>(); + private StringBuilder mPendingBuilder = new StringBuilder(); + private String mPendingActivity; + private boolean mDirty; + private final List mDependUiList = new ArrayList<>(); + + public ViewCacheData(Context context) { + mPrefs = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE); + load(); + } + + private void load() { + Map all = mPrefs.getAll(); + for (Map.Entry e : all.entrySet()) { + String val = (String) e.getValue(); + if (val != null) { + String key = e.getKey(); + if (KEY_DEPEND_UI.equals(key)) { + parseDependUi(val); + } else { + mCachedData.put(key, val); + } + } + } + } + + private void parseDependUi(String val) { + for (String s : val.split("/")) { + try { + mDependUiList.add(Integer.parseInt(s)); + } catch (NumberFormatException ignored) { + } + } + } + + public synchronized List getLayoutsForActivity(String activityName) { + String val = mCachedData.get(activityName); + if (val == null) return null; + List ids = new ArrayList<>(); + for (String s : val.split("/")) { + try { + ids.add(Integer.parseInt(s)); + } catch (NumberFormatException ignored) { + } + } + return ids; + } + + public synchronized boolean isValid(int layoutId) { + return !mDependUiList.contains(layoutId); + } + + public synchronized void recordDependUi(int layoutId) { + if (!mDependUiList.contains(layoutId)) { + mDependUiList.add(layoutId); + String current = mDependUiList.isEmpty() ? "" : joinIds(mDependUiList); + mPrefs.edit().putString(KEY_DEPEND_UI, current).apply(); + } + } + + public synchronized void record(int layoutId) { + if (mPendingActivity == null) { + mPendingActivity = resolveProcessName(); + } + if (mPendingBuilder.length() > 0) { + mPendingBuilder.append("/"); + } + mPendingBuilder.append(layoutId); + mDirty = true; + } + + public synchronized void sync() { + if (!mDirty || mPendingActivity == null) return; + String current = mPendingBuilder.toString(); + String old = mCachedData.get(mPendingActivity); + if (!current.equals(old)) { + mCachedData.put(mPendingActivity, current); + mPrefs.edit().putString(mPendingActivity, current).apply(); + } + mPendingBuilder.setLength(0); + mPendingActivity = null; + mDirty = false; + } + + private static String resolveProcessName() { + try { + ActivityThread at = ActivityThread.currentActivityThread(); + return at != null ? at.getProcessName() : null; + } catch (Exception e) { + return null; + } + } + + private static String joinIds(List ids) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ids.size(); i++) { + if (i > 0) sb.append("/"); + sb.append(ids.get(i)); + } + return sb.toString(); + } +} diff --git a/core/java/com/android/internal/util/ViewCacheManager.java b/core/java/com/android/internal/util/ViewCacheManager.java new file mode 100644 index 000000000000..df2863c6b8ea --- /dev/null +++ b/core/java/com/android/internal/util/ViewCacheManager.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util; + +import android.content.ComponentName; +import android.content.Context; +import android.os.SystemProperties; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.view.ViewBackgroundThread; + +import java.util.ArrayList; +import java.util.List; + +public class ViewCacheManager { + private static final String TAG = "ViewCacheManager"; + private static final boolean ENABLED = SystemProperties.getBoolean( + "persist.sys.perf.view_cache", true); + private static volatile ViewCacheManager sInstance; + + private final SparseArray> mViewCache = new SparseArray<>(); + private final Object mLock = new Object(); + private ViewCacheData mData; + private boolean mActive; + private String mCurrentActivity; + + public static ViewCacheManager getInstance() { + if (sInstance == null) { + sInstance = new ViewCacheManager(); + } + return sInstance; + } + + public boolean isEnable() { + return ENABLED && mActive; + } + + public void onActivityAttached(Context context, ComponentName component) { + if (!ENABLED || component == null) return; + mActive = false; + mCurrentActivity = component.getClassName(); + synchronized (mLock) { + mViewCache.clear(); + } + if (mData == null) { + mData = new ViewCacheData(context); + } + List layoutIds = mData.getLayoutsForActivity(mCurrentActivity); + if (layoutIds == null || layoutIds.isEmpty()) return; + mActive = true; + ViewBackgroundThread.getHandler().post(() -> { + preInflateLayouts(context, layoutIds); + }); + } + + private void preInflateLayouts(Context context, List layoutIds) { + LayoutInflater inflater = LayoutInflater.from(context) + .cloneInContext(context); + for (int id : layoutIds) { + if (Thread.interrupted()) break; + if (mData != null && !mData.isValid(id)) continue; + try { + View view = inflater.inflate(id, null, false); + if (view != null) { + put(id, view); + } + } catch (Exception e) { + if (mData != null) { + mData.recordDependUi(id); + } + } + } + } + + public View tryGet(int layoutId) { + if (!ENABLED) return null; + synchronized (mLock) { + List entries = mViewCache.get(layoutId); + if (entries != null && !entries.isEmpty()) { + return entries.remove(0); + } + } + return null; + } + + public void put(int layoutId, View view) { + synchronized (mLock) { + List entries = mViewCache.get(layoutId); + if (entries == null) { + entries = new ArrayList<>(); + mViewCache.put(layoutId, entries); + } + entries.add(view); + } + } + + public void recordLayoutRes(int resource) { + if (mData != null) { + mData.record(resource); + } + } + + public void onTraversalEnd(ViewRootImpl vri) { + if (mData != null) { + mData.sync(); + } + mActive = false; + mCurrentActivity = null; + synchronized (mLock) { + mViewCache.clear(); + } + } +} From a362ab9d85741b187ab72cf7675b1aea1df15beb Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 12:22:26 +0800 Subject: [PATCH 1195/1315] services: Skipping atom stats pulling on none eng builds simpleperf shows that onPullAtom burns up to 22% cpu. skip the stats pull 22.62% 0.00% binder:1505_13 5428 __start_thread | -- __start_thread | -- __pthread_start(void*) (.__uniq.67847048707805468364044055584648682506) | |--98.45%-- android::IPCThreadState::executeCommand(int) | |--0.26%-- [hit in function] | | | |--87.29%-- JavaBBinder::onTransact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int) | | | | | --97.74%-- art::JValue art::InvokeVirtualOrInterfaceWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list) | | | | | --99.96%-- art_quick_invoke_stub | | android.os.Binder.execTransact | | android.os.Binder.execTransactInternal | | | | | |--82.14%-- android.os.IPullAtomCallback$Stub.onTransact | | | NterpCommonInvokeInstance | | | android.app.StatsManager$PullAtomCallbackInternal.onPullAtom | | | NterpCommonInvokeInterface | | | [DEDUPED] ?.execute | | | android.app.StatsManager$PullAtomCallbackInternal$$ExternalSyntheticLambda0.run | | | NterpCommonInvokeStatic | | | android.app.StatsManager$PullAtomCallbackInternal.$r8$lambda$oIAiCNGFyxeWs0XtQyXXxYxjHhU Change-Id: I223988855b21e9877bca043315c1dad197664c1d Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/stats/pull/StatsPullAtomService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index 81bdc7650339..f390273c543d 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -564,6 +564,9 @@ public void noteUidProcessState(int uid, int state) { private class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCallback { @Override public int onPullAtom(int atomTag, List data) { + if (!Build.IS_ENG) { + return StatsManager.PULL_SKIP; + } if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "StatsPull-" + atomTag); } From e5e0022a8acccf0176fb406907437b3760941914 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 12:26:02 +0800 Subject: [PATCH 1196/1315] SystemUI: Fixing connectivity callbackhandler regressions the SimpleDateFormatting was expensinve and not worth it, the date was only used for debugging. 43.58% 0.00% com.android.systemui 1900 __libc_init | -- __libc_init | -- main | |--83.04%-- android.os.Handler.dispatchMessage | | | |--39.69%-- android.net.wifi.WifiManager$TrafficStateCallbackProxy$$ExternalSyntheticLambda0.run | | NterpCommonInvokeStatic | | android.net.wifi.WifiManager$TrafficStateCallbackProxy.$r8$lambda$vOD3LaVFDswH8Z7Zmrpsn2dfEdg | | NterpCommonInvokeInstance | | android.net.wifi.WifiManager$TrafficStateCallbackProxy.lambda$onStateChanged$0 | | NterpCommonInvokeInterface | | |--38.01%-- [hit in function] | | | | | --61.99%-- com.android.systemui.statusbar.connectivity.WifiSignalController$WifiTrafficStateCallback.onStateChanged | | com.android.systemui.statusbar.connectivity.SignalController.notifyListenersIfNecessary | | com.android.systemui.statusbar.connectivity.WifiSignalController.notifyListeners | | com.android.systemui.statusbar.connectivity.CallbackHandler.setWifiIndicators | | java.text.Format.format | | java.text.DateFormat.format | | java.text.SimpleDateFormat.format | | java.text.SimpleDateFormat.format | | java.text.SimpleDateFormat.subFormat | | java.text.SimpleDateFormat.zeroPaddingNumber | | | | | |--77.78%-- java.text.DecimalFormat.setMinimumIntegerDigits | | | android.icu.text.DecimalFormat.setMinimumIntegerDigits | | | | | | | |--77.95%-- android.icu.text.DecimalFormat.refreshFormatter | | | | android.icu.number.NumberPropertyMapper.oldToNew | | | | android.icu.impl.number.PropertiesAffixPatternProvider. | | | | art_quick_string_builder_append | | | | artStringBuilderAppend | | | | art::mirror::Object* art::gc::Heap::AllocObjectWithAllocator(art::Thread*, art::ObjPtr, unsigned long, art::gc::AllocatorType, art::StringBuilderAppend::Builder const&) | | | | | | | --22.05%-- [DEDUPED] | | | | | --22.22%-- java.text.DecimalFormat.format | | android.icu.text.DecimalFormat.format | | android.icu.number.LocalizedNumberFormatter.formatImpl | | android.icu.number.NumberFormatterImpl.preProcessUnsafe | | android.icu.number.NumberFormatterImpl.macrosToMicroGenerator Change-Id: I963e44e9c954e67b6207652397b1d17d70ed36c5 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../statusbar/connectivity/CallbackHandler.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java index 03d6494e9de7..da1f0125d30a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.connectivity.NetworkController.EmergencyListener; import java.io.PrintWriter; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; @@ -48,7 +47,6 @@ public class CallbackHandler extends Handler implements EmergencyListener, Signa private static final int MSG_ADD_REMOVE_EMERGENCY = 6; private static final int MSG_ADD_REMOVE_SIGNAL = 7; private static final int HISTORY_SIZE = 64; - private static final SimpleDateFormat SSDF = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); // All the callbacks. private final ArrayList mEmergencyListeners = new ArrayList<>(); @@ -120,7 +118,7 @@ public void handleMessage(Message msg) { @Override public void setWifiIndicators(final WifiIndicators indicators) { String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append(indicators) .toString(); recordLastCallback(log); @@ -134,7 +132,7 @@ public void setWifiIndicators(final WifiIndicators indicators) { @Override public void setMobileDataIndicators(final MobileDataIndicators indicators) { String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append(indicators) .toString(); recordLastCallback(log); @@ -157,7 +155,7 @@ public void setConnectivityStatus(boolean noDefaultNetwork, boolean noValidatedN if (!currentCallback.equals(mLastCallback)) { mLastCallback = currentCallback; String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append(currentCallback).append(",") .toString(); recordLastCallback(log); @@ -179,7 +177,7 @@ public void setSubs(List subs) { if (!currentCallback.equals(mLastCallback)) { mLastCallback = currentCallback; String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append(currentCallback).append(",") .toString(); recordLastCallback(log); @@ -205,7 +203,7 @@ public void setEmergencyCallsOnly(boolean emergencyOnly) { @Override public void setEthernetIndicators(IconState icon) { String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append("setEthernetIndicators: ") .append("icon=").append(icon) .toString(); @@ -222,7 +220,7 @@ public void setIsAirplaneMode(IconState icon) { if (!currentCallback.equals(mLastCallback)) { mLastCallback = currentCallback; String log = new StringBuilder() - .append(SSDF.format(System.currentTimeMillis())).append(",") + .append(System.currentTimeMillis()).append(",") .append(currentCallback).append(",") .toString(); recordLastCallback(log); From f01e80e25436b9dc4254d5a64e5fb6e90afe28fb Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 10:23:08 +0800 Subject: [PATCH 1197/1315] services: Fixing high-res wallpaper performance issue limiting generated static wallpaper crops to the max zoomed display size instead of keeping oversized source crops as the runtime wallpaper surface fixes qs and transition jank with high-res wallpapers where imagewallpaper creates large buffers that surfaceflinger has to compose every frame during wallpaper zoom and blur animations formula device dimensions x max wallpaper zoom + 0.1f result original wallpaper - 1840x4096 7.54mp generated crop - 1193x2654 3.17mp imagewallpaper requested surface - 1193x2654 sample size - 1.5430424 Change-Id: I2d90fdc97710db04b8c3a40c3122ecfb966f4e7b Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/wallpaper/WallpaperCropper.java | 40 ++++++++++++++++--- .../wallpaper/WallpaperManagerService.java | 2 +- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 0568c5b9a0d4..57fb7e967da6 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -31,6 +31,7 @@ import static com.android.window.flags.Flags.multiCrop; import android.app.WallpaperManager; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageDecoder; @@ -45,6 +46,7 @@ import android.view.View; import android.window.DesktopExperienceFlags; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.utils.TimingsTraceAndSlog; @@ -93,9 +95,12 @@ public class WallpaperCropper { private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; - WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { + private final float mMaxWallpaperScale; + + WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper, Resources resources) { mWallpaperDisplayHelper = wallpaperDisplayHelper; mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo(); + mMaxWallpaperScale = Math.max(1f, resources.getFloat(R.dimen.config_wallpaperMaxScale)); } /** @@ -641,15 +646,17 @@ private void generateCropInternal(WallpaperData wallpaper) { (float) crop.height() / displayForThisOrientation.y)); sampleSize = Math.min(sampleSize, sampleSizeForThisOrientation); } - // If the total crop has more width or height than either the max texture size - // or twice the largest display dimension, downsample the image + // If the total crop has more width or height than either the max texture size, + // twice the largest display dimension, or the largest useful runtime zoomed + // display size, downsample the image. int maxCropSize = Math.min( 2 * mWallpaperDisplayHelper.getDefaultDisplayLargestDimension(), GLHelper.getMaxTextureSize()); float minimumSampleSize = Math.max(1f, Math.max( (float) cropHint.height() / maxCropSize, - (float) cropHint.width()) / maxCropSize); - sampleSize = Math.max(sampleSize, minimumSampleSize); + (float) cropHint.width() / maxCropSize)); + float runtimeSampleSize = getMaxZoomCropSampleSize(cropHint); + sampleSize = Math.max(sampleSize, Math.max(minimumSampleSize, runtimeSampleSize)); needScale = sampleSize > 1f; } @@ -845,6 +852,29 @@ private void generateCropInternal(WallpaperData wallpaper) { } } + private float getMaxZoomCropSampleSize(Rect cropHint) { + int maxDisplayWidth = 0; + int maxDisplayHeight = 0; + SparseArray displaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); + for (int i = 0; i < displaySizes.size(); i++) { + Point size = displaySizes.valueAt(i); + maxDisplayWidth = Math.max(maxDisplayWidth, size.x); + maxDisplayHeight = Math.max(maxDisplayHeight, size.y); + } + + if (maxDisplayWidth <= 0 || maxDisplayHeight <= 0) { + return 1f; + } + + final int cropWidth = cropHint.width(); + final int cropHeight = cropHint.height(); + final float maxRuntimeWidth = maxDisplayWidth * mMaxWallpaperScale; + final float maxRuntimeHeight = maxDisplayHeight * mMaxWallpaperScale; + + return Math.max(1f, + Math.max(cropWidth / maxRuntimeWidth, cropHeight / maxRuntimeHeight)); + } + private static Rect getCropForExternalDisplay( Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo, diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 0afa5eacc87f..71b29db2547b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1523,7 +1523,7 @@ public WallpaperManagerService(Context context) { WindowManager windowManager = mContext.getSystemService(WindowManager.class); mWallpaperDisplayHelper = new WallpaperDisplayHelper( displayManager, windowManager, mWindowManagerInternal, mContext.getResources()); - mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); + mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper, mContext.getResources()); mActivityManager = mContext.getSystemService(ActivityManager.class); if (mContext.getResources().getBoolean( From db37cd8f36add0ecaf82d05a9fab8689b24bf4cc Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 7 May 2026 17:27:01 +0000 Subject: [PATCH 1198/1315] SystemUI: Fade out depth wallpaper fade out smoothly Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/systemui/util/ScrimUtils.kt | 3 ++ .../systemui/util/WallpaperDepthUtils.java | 33 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt index c2f99910802f..f4010a2da059 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ScrimUtils.kt @@ -115,6 +115,8 @@ class ScrimUtils private constructor(context: Context?) { mWallpaperDepthUtils?.updateDepthWallpaperVisibility() mWallpaperDepthUtils?.updateDepthWallpaper() }, 120) + } else { + mWallpaperDepthUtils?.hideDepthWallpaperImmediate() } } } @@ -124,6 +126,7 @@ class ScrimUtils private constructor(context: Context?) { postKeyguardRetry() if (fadingAway) { mWallpaperDepthUtils?.hideDepthWallpaper() + mWallpaperDepthUtils?.hideDepthWallpaperImmediate() } } diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java index 52e05f9938aa..f323b4694220 100644 --- a/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java +++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperDepthUtils.java @@ -70,6 +70,7 @@ public class WallpaperDepthUtils { private Bitmap mWallpaperBitmap; private int mOffsetX; private int mOffsetY; + private boolean mUnlocking; private WallpaperDepthUtils(Context context, ScrimController scrimController) { mContext = context.getApplicationContext(); @@ -103,6 +104,20 @@ public static WallpaperDepthUtils getInstance(Context context, ScrimController s public static WallpaperDepthUtils get() { return instance; } + + public void onUnlockStarted() { + mUnlocking = true; + hideDepthWallpaperImmediate(); + } + + public void onUnlockCancelled() { + mUnlocking = false; + updateDepthWallpaperVisibility(); + } + + public void onUnlockCompleted() { + mUnlocking = false; + } public void onDozingChanged(boolean dozing) { if (mDozing == dozing) { @@ -209,6 +224,7 @@ && isDWallpaperEnabled() && !mBouncerShowing && !mGlanceableHubShowing && !mDynamicBarExpanded + && !mUnlocking && currentState == ScrimState.KEYGUARD && mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE && !MediaViewController.get(mContext).albumArtVisible(); @@ -227,10 +243,25 @@ public void updateDepthWallpaperVisibility() { } public void hideDepthWallpaper() { - if (mLockScreenSubject.getVisibility() == View.GONE) return; + if (mLockScreenSubject == null || mLockScreenSubject.getVisibility() == View.GONE) return; mLockScreenSubject.post(() -> mLockScreenSubject.setVisibility(View.GONE)); } + public void hideDepthWallpaperImmediate() { + if (mLockScreenSubject == null) return; + mLockScreenSubject.post(() -> { + mLockScreenSubject.animate().cancel(); + mLockScreenSubject.animate() + .alpha(0f) + .setDuration(120) + .withEndAction(() -> { + mLockScreenSubject.setVisibility(View.GONE); + mLockScreenSubject.setAlpha(1f); + }) + .start(); + }); + } + public Bitmap getResizedBitmap(Bitmap wallpaperBitmap, float xOffsetDp, float yOffsetDp) { Rect displayBounds = mContext.getSystemService(WindowManager.class) .getCurrentWindowMetrics() From 9d2fc2d9ae3cbdedb72848e8c2bfa3c83c9240b3 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:34:57 +0800 Subject: [PATCH 1199/1315] services: Add immersive lock gesture Change-Id: I219bc7b9aac04cccab299c7ac940d1ebad10593e Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../com/android/server/wm/DisplayPolicy.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 2c8a11e94df6..95eec1fb7323 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -106,10 +106,12 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; +import android.view.Display; import android.view.DisplayInfo; import android.view.InsetsFlags; import android.view.InsetsFrameProvider; @@ -421,6 +423,8 @@ UiModeManagerInternal getUiModeManagerInternal() { private final ForceShowNavBarSettingsObserver mForceShowNavBarSettingsObserver; private boolean mForceShowNavigationBarEnabled; + private volatile boolean mGamingGestureLocked; + private class PolicyHandler extends Handler { PolicyHandler(Looper looper) { @@ -519,6 +523,9 @@ private Insets getControllableInsets(WindowState win) { @Override public void onSwipeFromTop() { + if (isGamingGestureLocked()) { + return; + } synchronized (mLock) { requestTransientBars(mTopGestureHost, getControllableInsets(mTopGestureHost).top > 0); @@ -527,6 +534,9 @@ public void onSwipeFromTop() { @Override public void onSwipeFromBottom() { + if (isGamingGestureLocked()) { + return; + } synchronized (mLock) { requestTransientBars(mBottomGestureHost, getControllableInsets(mBottomGestureHost).bottom > 0); @@ -534,12 +544,18 @@ public void onSwipeFromBottom() { } private boolean allowsSideSwipe(Region excludedRegion) { + if (isGamingGestureLocked()) { + return false; + } return mNavigationBarAlwaysShowOnSideGesture && !mSystemGestures.currentGestureStartedInRegion(excludedRegion); } @Override public void onSwipeFromRight() { + if (isGamingGestureLocked()) { + return; + } final Region excludedRegion = Region.obtain(); synchronized (mLock) { mDisplayContent.calculateSystemGestureExclusion( @@ -555,6 +571,9 @@ public void onSwipeFromRight() { @Override public void onSwipeFromLeft() { + if (isGamingGestureLocked()) { + return; + } final Region excludedRegion = Region.obtain(); synchronized (mLock) { mDisplayContent.calculateSystemGestureExclusion( @@ -762,6 +781,20 @@ public void onAppTransitionFinishedLocked(IBinder token) { mForceShowNavBarSettingsObserver.setOnChangeRunnable(this::updateForceShowNavBarSettings); mForceShowNavigationBarEnabled = mForceShowNavBarSettingsObserver.isEnabled(); mHandler.post(mForceShowNavBarSettingsObserver::register); + + ContentResolver resolver = mContext.getContentResolver(); + mGamingGestureLocked = Settings.Secure.getIntForUser(resolver, + "ax_gaming_gesture_lock", 0, UserHandle.USER_CURRENT) == 1; + ContentObserver gestureLockObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + mGamingGestureLocked = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + "ax_gaming_gesture_lock", 0, UserHandle.USER_CURRENT) == 1; + } + }; + resolver.registerContentObserver(Settings.Secure.getUriFor( + "ax_gaming_gesture_lock"), false, gestureLockObserver, UserHandle.USER_ALL); } private void updateForceShowNavBarSettings() { @@ -843,6 +876,10 @@ public int getDockMode() { return mDockMode; } + public boolean isGamingGestureLocked() { + return mGamingGestureLocked; + } + public boolean hasNavigationBar() { return mHasNavigationBar; } @@ -2446,6 +2483,9 @@ void requestTransientBars(WindowState swipeTarget, boolean isGestureOnSystemBar) if (CLIENT_TRANSIENT) { return; } + if (isGamingGestureLocked()) { + return; + } if (swipeTarget == null || !mService.mPolicy.isUserSetupComplete()) { // Swipe-up for navigation bar is disabled during setup return; From 065dede21ee88e407f60bc53150a1fb727ba9bf1 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:19:37 +0800 Subject: [PATCH 1200/1315] services: Caching isOverlappingWithNavBar to avoid redundant rectangle math Change-Id: I8bed376acb56131d257c48e4672298144c7440bb Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/wm/DisplayPolicy.java | 6 +++--- .../java/com/android/server/wm/WindowState.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 95eec1fb7323..abbb257a60a1 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1637,7 +1637,7 @@ public void applyPostLayoutPolicyLw(WindowState win, WindowManager.LayoutParams // Check if the freeform window overlaps with the navigation bar area. if (!mIsFreeformWindowOverlappingWithNavBar && win.inFreeformWindowingMode() - && win.mActivityRecord != null && isOverlappingWithNavBar(win)) { + && win.mActivityRecord != null && win.isOverlappingWithNavBar()) { mIsFreeformWindowOverlappingWithNavBar = true; } @@ -1752,7 +1752,7 @@ && fillsDisplayWindowingMode(win)) { // mode; if it's in gesture navigation mode, the navigation bar will be // NAV_BAR_FORCE_TRANSPARENT and its appearance won't be decided by overlapping // windows. - if (isOverlappingWithNavBar(win)) { + if (win.isOverlappingWithNavBar()) { if (mNavBarColorWindowCandidate == null) { mNavBarColorWindowCandidate = win; addSystemBarColorApp(win); @@ -1785,7 +1785,7 @@ && addStatusBarAppearanceRegionsForDimmingWindow( addSystemBarColorApp(win); } } - if (isOverlappingWithNavBar(win) && mNavBarColorWindowCandidate == null) { + if (win.isOverlappingWithNavBar() && mNavBarColorWindowCandidate == null) { mNavBarColorWindowCandidate = win; addSystemBarColorApp(win); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 2f25587ee91f..02ba606536af 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -598,6 +598,8 @@ class WindowState extends WindowContainer implements WindowManagerP /** The time when the window was last requested to redraw for orientation change. */ private long mOrientationChangeRedrawRequestTime; + private int mLastOverlappingWithNavBarSeq = -1; + private boolean mIsOverlappingWithNavBar; /** Is this window now (or just being) removed? */ boolean mRemoved; @@ -6090,6 +6092,18 @@ boolean setWallpaperOffset(int dx, int dy, float scale) { return true; } + boolean isOverlappingWithNavBar() { + if (!isVisible()) { + return false; + } + if (mLastOverlappingWithNavBarSeq == mDisplayContent.mLayoutSeq) { + return mIsOverlappingWithNavBar; + } + mIsOverlappingWithNavBar = DisplayPolicy.isOverlappingWithNavBar(this); + mLastOverlappingWithNavBarSeq = mDisplayContent.mLayoutSeq; + return mIsOverlappingWithNavBar; + } + boolean isTrustedOverlay() { WindowState parentWindow = getParentWindow(); return isWindowTrustedOverlay() || (parentWindow != null From 2e156022efb638209057154c5693b9257e8a9319 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 May 2026 07:28:58 +0800 Subject: [PATCH 1201/1315] services: Optimizing navigation bar draws Change-Id: I133eff520b8a45b6e1e197d9b37f60fae1b17bc9 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/wm/DisplayPolicy.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index abbb257a60a1..8ce8b8cf57e6 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1752,7 +1752,8 @@ && fillsDisplayWindowingMode(win)) { // mode; if it's in gesture navigation mode, the navigation bar will be // NAV_BAR_FORCE_TRANSPARENT and its appearance won't be decided by overlapping // windows. - if (win.isOverlappingWithNavBar()) { + if ((mNavBarColorWindowCandidate == null || mNavBarBackgroundWindowCandidate == null) + && win.isOverlappingWithNavBar()) { if (mNavBarColorWindowCandidate == null) { mNavBarColorWindowCandidate = win; addSystemBarColorApp(win); @@ -2950,8 +2951,9 @@ private Rect getBarContentFrameForWindow(WindowState win, @InsetsType int type) final Rect df = displayFrames.mUnrestricted; final Rect safe = sTmpDisplayCutoutSafe; final Insets waterfallInsets = state.getDisplayCutout().getWaterfallInsets(); - final Rect outRect = new Rect(); + final Rect outRect = sTmpRect2; final Rect sourceContent = sTmpRect; + outRect.setEmpty(); safe.set(displayFrames.mDisplayCutoutSafe); for (int i = state.sourceSize() - 1; i >= 0; i--) { final InsetsSource source = state.sourceAt(i); @@ -3019,6 +3021,9 @@ private int configureStatusBarOpacity(int appearance) { final WindowState window = mStatusBarBackgroundWindows.get(i); drawBackground &= drawsBarBackground(window, Type.statusBars()); isFullyTransparentAllowed &= isFullyTransparentAllowed(window, Type.statusBars()); + if (!drawBackground && !isFullyTransparentAllowed) { + break; + } } if (drawBackground) { From 6822c8ccd5ca9945990d4a18e0b9d6256e39802d Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:21:08 +0800 Subject: [PATCH 1202/1315] services: Background apps compression Change-Id: I2994221dd607792b7622c422b400eccc53fcdda6 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/am/CachedAppOptimizer.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 75ca8260df9e..fd411757fdc3 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -96,6 +96,8 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; @@ -352,6 +354,7 @@ public enum CancelCompactReason { static final int UID_FROZEN_STATE_CHANGED_MSG = 6; static final int DEADLOCK_WATCHDOG_MSG = 7; static final int BINDER_ERROR_MSG = 8; + static final int COMPACT_APP_SWITCH_MSG = 9; // When free swap falls below this percentage threshold any full (file + anon) // compactions will be downgraded to file only compactions to reduce pressure @@ -493,6 +496,9 @@ public void onChange(boolean selfChange, Uri uri) { DEFAULT_COMPACT_THROTTLE_MAX_OOM_ADJ; @GuardedBy("mPhenotypeFlagLock") private volatile boolean mUseCompaction = DEFAULT_USE_COMPACTION; + + private static final long APP_SWITCH_COMPACT_DELAY_MS = 10_000; + private volatile boolean mUseFreezer = false; // set to DEFAULT in init() @GuardedBy("this") private int mFreezerDisableCount = 1; // Freezer is initially disabled, until enabled @@ -1426,6 +1432,7 @@ void onWakefulnessChanged(int wakefulness) { // Remove any pending compaction we may have scheduled to happen while screen was // off cancelAllCompactions(CancelCompactReason.SCREEN_ON); + mCompactionHandler.removeMessages(COMPACT_APP_SWITCH_MSG); } } } @@ -1474,6 +1481,14 @@ void onOomAdjustChanged(int oldAdj, int newAdj, ProcessRecord app) { // if the process moved out of cached state if (newAdj < oldAdj && newAdj < ProcessList.CACHED_APP_MIN_ADJ) { cancelCompactionForProcess(app, CancelCompactReason.OOM_IMPROVEMENT); + mCompactionHandler.removeMessages(COMPACT_APP_SWITCH_MSG, app); + } + + if (newAdj >= ProcessList.CACHED_APP_MIN_ADJ + && oldAdj < ProcessList.CACHED_APP_MIN_ADJ) { + mCompactionHandler.sendMessageDelayed( + mCompactionHandler.obtainMessage(COMPACT_APP_SWITCH_MSG, app), + APP_SWITCH_COMPACT_DELAY_MS); } } } @@ -1540,6 +1555,18 @@ boolean isProcessFrozen(int pid) { } } + private boolean isSystemUnderHighLoad() { + try { + String loadStr = new String( + Files.readAllBytes(Paths.get("/proc/loadavg"))); + double oneMinLoad = Double.parseDouble(loadStr.split(" ")[0]); + int numCpus = Runtime.getRuntime().availableProcessors(); + return (oneMinLoad / numCpus) > 0.8; + } catch (Exception e) { + return false; + } + } + private static int getCompactionFlags(CompactProfile profile) { if (profile == CompactProfile.FULL) { return COMPACT_ACTION_FILE_FLAG | COMPACT_ACTION_ANON_FLAG; @@ -1896,6 +1923,22 @@ public void handleMessage(Message msg) { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; } + case COMPACT_APP_SWITCH_MSG: { + ProcessRecord proc = (ProcessRecord) msg.obj; + if (isSystemUnderHighLoad()) { + if (DEBUG_COMPACTION) { + Slog.d(TAG_AM, "Skipping app-switch compaction for " + + proc.processName + " due to high CPU load"); + } + break; + } + synchronized (mProcLock) { + if (proc.getCurAdj() >= ProcessList.CACHED_APP_MIN_ADJ) { + compactApp(proc, CompactProfile.ANON, CompactSource.APP, false); + } + } + break; + } } } } From f6f9a3c484367efc8ff4092cacec042e198caf10 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:51:17 +0800 Subject: [PATCH 1203/1315] services: Reducing app freezer thrashing fullsome throttle was 500ms which lets the same cached proc get re-compacted every half second during rapid app switching. bumped all throttles to sane values so freezer stops pinning a core on madvise_pageout during app cycling. Change-Id: I41319723bbaed9c88207c59f60a22684401c49b3 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/com/android/server/am/CachedAppOptimizer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index fd411757fdc3..2e1a8aec48d4 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -277,10 +277,10 @@ public class CachedAppOptimizer { // Defaults for phenotype flags. @VisibleForTesting static final boolean DEFAULT_USE_COMPACTION = true; @VisibleForTesting static final boolean DEFAULT_USE_FREEZER = true; - @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_1 = 5_000; - @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_2 = 10_000; - @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_3 = 500; - @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 5*60*1000; + @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_1 = 30_000; + @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_2 = 60_000; + @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_3 = 10_000; + @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 15*60*1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_5 = 10 * 60 * 1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_6 = 10 * 60 * 1000; @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_MIN_OOM_ADJ = From 62b51cfc20b9ea821719e4c4b4a77dd8eb39bc87 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:41:37 +0800 Subject: [PATCH 1204/1315] services: Deferring zram compaction during app launch simpleperf during app launches shows CachedAppOptimizer at 5.5% system CPU with 45% in LZ4_compress_fast_extState competing with the launching app for cpu and memory bandwidth. defer non-forced compaction for 1.5s after any process transitions to foreground adj, matching the existing high-load skip pattern Change-Id: I93e6803669eafc88b123df33c871f8d21354adfb Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../android/server/am/CachedAppOptimizer.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 2e1a8aec48d4..1ca75bc61c0e 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -499,6 +499,10 @@ public void onChange(boolean selfChange, Uri uri) { private static final long APP_SWITCH_COMPACT_DELAY_MS = 10_000; + private static final long COMPACT_LAUNCH_DEFER_DURATION_MS = 1_500; + + private volatile long mLastAppLaunchUptime = 0; + private volatile boolean mUseFreezer = false; // set to DEFAULT in init() @GuardedBy("this") private int mFreezerDisableCount = 1; // Freezer is initially disabled, until enabled @@ -1490,6 +1494,11 @@ void onOomAdjustChanged(int oldAdj, int newAdj, ProcessRecord app) { mCompactionHandler.obtainMessage(COMPACT_APP_SWITCH_MSG, app), APP_SWITCH_COMPACT_DELAY_MS); } + + if (newAdj <= ProcessList.FOREGROUND_APP_ADJ + && oldAdj > ProcessList.FOREGROUND_APP_ADJ) { + mLastAppLaunchUptime = SystemClock.uptimeMillis(); + } } } @@ -1781,6 +1790,13 @@ public void handleMessage(Message msg) { } if (!forceCompaction) { + if (start - mLastAppLaunchUptime < COMPACT_LAUNCH_DEFER_DURATION_MS) { + if (DEBUG_COMPACTION) { + Slog.d(TAG_AM, "Skipping compaction for " + name + + ": app launch in progress"); + } + return; + } if (shouldOomAdjThrottleCompaction(proc)) { mCompactStatsManager.logCompactionThrottled( CompactionStatsManager.COMPACT_THROTTLE_REASON_OOM_ADJ, From 7d15fef0571e7804acfcd01b55111a396ebb6845 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 12:29:12 +0800 Subject: [PATCH 1205/1315] services: Skipping unnecessary full compaction when awake Change-Id: Ie2ed0b920729a29d001a096345d520564e932f4e Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../core/java/com/android/server/am/CachedAppOptimizer.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 1ca75bc61c0e..7c87b3442b40 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -502,6 +502,7 @@ public void onChange(boolean selfChange, Uri uri) { private static final long COMPACT_LAUNCH_DEFER_DURATION_MS = 1_500; private volatile long mLastAppLaunchUptime = 0; + private volatile boolean mIsAwake = true; private volatile boolean mUseFreezer = false; // set to DEFAULT in init() @GuardedBy("this") @@ -1431,6 +1432,7 @@ void onCleanupApplicationRecordLocked(ProcessRecord app) { } void onWakefulnessChanged(int wakefulness) { + mIsAwake = wakefulness == PowerManagerInternal.WAKEFULNESS_AWAKE; if(wakefulness == PowerManagerInternal.WAKEFULNESS_AWAKE) { if (useCompaction()) { // Remove any pending compaction we may have scheduled to happen while screen was @@ -1507,6 +1509,10 @@ void onOomAdjustChanged(int oldAdj, int newAdj, ProcessRecord app) { */ void onProcessFrozen(ProcessRecord frozenProc) { if (useCompaction()) { + if (mIsAwake) { + frozenProc.onProcessFrozen(); + return; + } synchronized (mProcLock) { // only full-compact if process is cached if (frozenProc.getSetAdj() >= mCompactThrottleMinOomAdj) { From f4ce2de54e5827615933ba856b7340396bf4fda8 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:25:06 +0800 Subject: [PATCH 1206/1315] core: Upgrading to m3e animation specs Change-Id: I0bc91f3c97af7995fc3174f72dedd6febb291ec6 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../view/animation/AnimationUtils.java | 96 +++++++++++++++++-- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 1ea19e8e6804..581dc2f4f0c5 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -533,23 +533,31 @@ public final class ActivityAnimations { private static Animation sCloseEnter; private static Animation sCloseExit; - private static Interpolator sFastOutExtraSlowInInterpolator; + private static SpringInterpolator sSpatialSpec; + private static SpringInterpolator sEffectsSpec; + private static int sBackdropColor; - private static final float DISTANCE = 0.1f; + private static final float DISTANCE = 0.333f; private ActivityAnimations() {} /** @hide */ public static void maybeInit(Context context) { - if (sFastOutExtraSlowInInterpolator == null) { - sFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( - context, R.interpolator.fast_out_extra_slow_in); + if (sSpatialSpec == null) { + sSpatialSpec = new SpringInterpolator(0.8f, 380f); + sEffectsSpec = new SpringInterpolator(1.0f, 3800f); } + sBackdropColor = context.getColor( + com.android.internal.R.color.materialColorSurfaceContainer); + if (sOpenEnter != null) sOpenEnter.setBackdropColor(sBackdropColor); + if (sOpenExit != null) sOpenExit.setBackdropColor(sBackdropColor); + if (sCloseEnter != null) sCloseEnter.setBackdropColor(sBackdropColor); + if (sCloseExit != null) sCloseExit.setBackdropColor(sBackdropColor); } private static class ActivityAnimFactory { private float fromX = 0f, toX = 0f; - private long duration = 200L; + private float fromAlpha = 1f, toAlpha = 1f; public ActivityAnimFactory fromX(float ratio) { this.fromX = ratio; @@ -561,6 +569,12 @@ public ActivityAnimFactory toX(float ratio) { return this; } + public ActivityAnimFactory fade(float from, float to) { + this.fromAlpha = from; + this.toAlpha = to; + return this; + } + public Animation build() { AnimationSet animationSet = new AnimationSet(false); TranslateAnimation slide = new TranslateAnimation( @@ -569,9 +583,17 @@ public Animation build() { Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f ); - slide.setDuration(duration); - slide.setInterpolator(sFastOutExtraSlowInInterpolator); + slide.setDuration(sSpatialSpec.getDurationMs()); + slide.setInterpolator(sSpatialSpec); animationSet.addAnimation(slide); + if (fromAlpha != toAlpha) { + AlphaAnimation fade = new AlphaAnimation(fromAlpha, toAlpha); + fade.setDuration(sEffectsSpec.getDurationMs()); + fade.setInterpolator(sEffectsSpec); + animationSet.addAnimation(fade); + } + animationSet.setShowBackdrop(true); + animationSet.setBackdropColor(sBackdropColor); return animationSet; } } @@ -582,6 +604,7 @@ public static Animation getOpenEnter() { sOpenEnter = new ActivityAnimFactory() .fromX(1.0f) .toX(0.0f) + .fade(0.0f, 1.0f) .build(); } return sOpenEnter; @@ -593,6 +616,7 @@ public static Animation getOpenExit() { sOpenExit = new ActivityAnimFactory() .fromX(0.0f) .toX(-DISTANCE) + .fade(1.0f, 0.0f) .build(); } return sOpenExit; @@ -604,6 +628,7 @@ public static Animation getCloseEnter() { sCloseEnter = new ActivityAnimFactory() .fromX(-DISTANCE) .toX(0.0f) + .fade(0.0f, 1.0f) .build(); } return sCloseEnter; @@ -615,9 +640,64 @@ public static Animation getCloseExit() { sCloseExit = new ActivityAnimFactory() .fromX(0.0f) .toX(1.0f) + .fade(1.0f, 0.0f) .build(); } return sCloseExit; } } + + /** @hide */ + public static final class SpringInterpolator implements Interpolator { + private final float mDampingRatio; + private final float mOmega0; + private final long mDurationMs; + private final float mDurationSec; + private final float mEndOutput; + private final float mEndGap; + + public SpringInterpolator(float dampingRatio, float stiffness) { + mDampingRatio = dampingRatio; + mOmega0 = (float) Math.sqrt(stiffness); + final float settleSec; + if (dampingRatio >= 1.0f) { + settleSec = 9.23f / mOmega0; + } else { + settleSec = 6.91f / (dampingRatio * mOmega0); + } + mDurationMs = Math.max(50L, (long) (settleSec * 1000f)); + mDurationSec = mDurationMs / 1000f; + mEndOutput = rawSpring(mDurationSec); + mEndGap = 1.0f - mEndOutput; + } + + public long getDurationMs() { + return mDurationMs; + } + + private float rawSpring(float t) { + final float zeta = mDampingRatio; + final float w0 = mOmega0; + if (zeta < 1.0f) { + final float wd = w0 * (float) Math.sqrt(1.0f - zeta * zeta); + final float env = (float) Math.exp(-zeta * w0 * t); + return 1.0f - env * ((float) Math.cos(wd * t) + + (zeta * w0 / wd) * (float) Math.sin(wd * t)); + } else if (zeta > 1.0f) { + final float d = (float) Math.sqrt(zeta * zeta - 1.0f); + final float r1 = -w0 * (zeta - d); + final float r2 = -w0 * (zeta + d); + return 1.0f - (r2 * (float) Math.exp(r1 * t) + - r1 * (float) Math.exp(r2 * t)) / (r2 - r1); + } else { + final float env = (float) Math.exp(-w0 * t); + return 1.0f - env * (1.0f + w0 * t); + } + } + + @Override + public float getInterpolation(float input) { + return rawSpring(input * mDurationSec) + mEndGap * input; + } + } } From 615439c83584a2e18cd3b197745f269d1e884707 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:14:11 +0800 Subject: [PATCH 1207/1315] core: Optimizing animations performance preloading the animations during application bind, makes the activity loading way more faster, removing the latency during activity launch click Change-Id: I8ae7e6c950d710593e695e94ae152d6815637d67 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 11 +++ .../view/animation/AnimationUtils.java | 93 +++++++++---------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 47df7a65b2b4..da50d59cf5cc 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -211,6 +211,7 @@ import android.util.UtilConfig; import android.util.proto.ProtoOutputStream; import android.view.Choreographer; +import android.view.animation.AnimationUtils; import android.view.Display; import android.view.SurfaceControl; import android.view.ThreadedRenderer; @@ -8192,6 +8193,16 @@ private void handleBindApplication(AppBindData data) { } } + if (!Process.isIsolated()) { + try { + if (AnimationUtils.ActivityAnimations.sPerfAnimEnabled) { + AnimationUtils.ActivityAnimations.preload(appContext); + } + } catch (Exception e) { + Slog.e(TAG, "Failed to preload animations", e); + } + } + try { mgr.finishAttachApplication(mStartSeq, timestampApplicationOnCreateNs); } catch (RemoteException ex) { diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 581dc2f4f0c5..e5491e5d6722 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -230,17 +230,16 @@ public static long getExpectedPresentationTimeMillis() { public static Animation loadAnimation(Context context, @AnimRes int id) throws NotFoundException { - if (SystemProperties.getBoolean("persist.sys.activity_anim_perf_override", false)) { - ActivityAnimations.maybeInit(context); + if (ActivityAnimations.sPerfAnimEnabled) { switch (id) { case R.anim.activity_open_enter: - return ActivityAnimations.getOpenEnter(); + return ActivityAnimations.getOpenEnter(context); case R.anim.activity_open_exit: - return ActivityAnimations.getOpenExit(); + return ActivityAnimations.getOpenExit(context); case R.anim.activity_close_enter: - return ActivityAnimations.getCloseEnter(); + return ActivityAnimations.getCloseEnter(context); case R.anim.activity_close_exit: - return ActivityAnimations.getCloseExit(); + return ActivityAnimations.getCloseExit(context); } } @@ -528,6 +527,9 @@ private static Interpolator createInterpolatorFromXml( /** @hide */ public final class ActivityAnimations { + public static final boolean sPerfAnimEnabled = SystemProperties.getBoolean( + "persist.sys.activity_anim_perf_override", false); + private static Animation sOpenEnter; private static Animation sOpenExit; private static Animation sCloseEnter; @@ -535,24 +537,40 @@ public final class ActivityAnimations { private static SpringInterpolator sSpatialSpec; private static SpringInterpolator sEffectsSpec; - private static int sBackdropColor; private static final float DISTANCE = 0.333f; private ActivityAnimations() {} /** @hide */ - public static void maybeInit(Context context) { - if (sSpatialSpec == null) { - sSpatialSpec = new SpringInterpolator(0.8f, 380f); - sEffectsSpec = new SpringInterpolator(1.0f, 3800f); - } - sBackdropColor = context.getColor( + public static void preload(Context context) { + sSpatialSpec = new SpringInterpolator(0.8f, 380f); + sEffectsSpec = new SpringInterpolator(1.0f, 3800f); + sOpenEnter = new ActivityAnimFactory() + .fromX(1.0f) + .toX(0.0f) + .fade(0.0f, 1.0f) + .build(); + sOpenExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(-DISTANCE) + .fade(1.0f, 0.0f) + .build(); + sCloseEnter = new ActivityAnimFactory() + .fromX(-DISTANCE) + .toX(0.0f) + .fade(0.0f, 1.0f) + .build(); + sCloseExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(1.0f) + .fade(1.0f, 0.0f) + .build(); + } + + private static int loadBackdropColor(Context context) { + return context.getColor( com.android.internal.R.color.materialColorSurfaceContainer); - if (sOpenEnter != null) sOpenEnter.setBackdropColor(sBackdropColor); - if (sOpenExit != null) sOpenExit.setBackdropColor(sBackdropColor); - if (sCloseEnter != null) sCloseEnter.setBackdropColor(sBackdropColor); - if (sCloseExit != null) sCloseExit.setBackdropColor(sBackdropColor); } private static class ActivityAnimFactory { @@ -593,56 +611,31 @@ public Animation build() { animationSet.addAnimation(fade); } animationSet.setShowBackdrop(true); - animationSet.setBackdropColor(sBackdropColor); return animationSet; } } /** @hide */ - public static Animation getOpenEnter() { - if (sOpenEnter == null) { - sOpenEnter = new ActivityAnimFactory() - .fromX(1.0f) - .toX(0.0f) - .fade(0.0f, 1.0f) - .build(); - } + public static Animation getOpenEnter(Context context) { + sOpenEnter.setBackdropColor(loadBackdropColor(context)); return sOpenEnter; } /** @hide */ - public static Animation getOpenExit() { - if (sOpenExit == null) { - sOpenExit = new ActivityAnimFactory() - .fromX(0.0f) - .toX(-DISTANCE) - .fade(1.0f, 0.0f) - .build(); - } + public static Animation getOpenExit(Context context) { + sOpenExit.setBackdropColor(loadBackdropColor(context)); return sOpenExit; } /** @hide */ - public static Animation getCloseEnter() { - if (sCloseEnter == null) { - sCloseEnter = new ActivityAnimFactory() - .fromX(-DISTANCE) - .toX(0.0f) - .fade(0.0f, 1.0f) - .build(); - } + public static Animation getCloseEnter(Context context) { + sCloseEnter.setBackdropColor(loadBackdropColor(context)); return sCloseEnter; } /** @hide */ - public static Animation getCloseExit() { - if (sCloseExit == null) { - sCloseExit = new ActivityAnimFactory() - .fromX(0.0f) - .toX(1.0f) - .fade(1.0f, 0.0f) - .build(); - } + public static Animation getCloseExit(Context context) { + sCloseExit.setBackdropColor(loadBackdropColor(context)); return sCloseExit; } } From 6cea6097a9e6e0bf9f04c947d5b298235e021b6c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 22:13:42 +0800 Subject: [PATCH 1208/1315] core: removing animation regressions the backdrop was heavy and messy, mtk hates complex fade animations, programatically load app start animation to reduce pressure during splash screen animation, simpleperf shows parsing the animation from xml consumes high cpu usage due to resource lookup. Change-Id: Icc38c2d3db1a05ceedf41433d13cc75a8de7732f Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 4 +- .../view/animation/AnimationUtils.java | 67 ++++++++----------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index da50d59cf5cc..d1785c28e45d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -8195,8 +8195,8 @@ private void handleBindApplication(AppBindData data) { if (!Process.isIsolated()) { try { - if (AnimationUtils.ActivityAnimations.sPerfAnimEnabled) { - AnimationUtils.ActivityAnimations.preload(appContext); + if (AnimationUtils.sPerfAnimEnabled) { + AnimationUtils.ActivityAnimations.preload(); } } catch (Exception e) { Slog.e(TAG, "Failed to preload animations", e); diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index e5491e5d6722..19463738dac8 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -60,6 +60,10 @@ public class AnimationUtils { */ private static final int TOGETHER = 0; private static final int SEQUENTIALLY = 1; + + /** @hide **/ + public static final boolean sPerfAnimEnabled = SystemProperties.getBoolean( + "persist.sys.activity_anim_perf_override", false); private static boolean sExpectedPresentationTimeFlagValue; static { @@ -230,16 +234,18 @@ public static long getExpectedPresentationTimeMillis() { public static Animation loadAnimation(Context context, @AnimRes int id) throws NotFoundException { - if (ActivityAnimations.sPerfAnimEnabled) { + if (sPerfAnimEnabled) { switch (id) { case R.anim.activity_open_enter: - return ActivityAnimations.getOpenEnter(context); + return ActivityAnimations.getOpenEnter(); case R.anim.activity_open_exit: - return ActivityAnimations.getOpenExit(context); + return ActivityAnimations.getOpenExit(); case R.anim.activity_close_enter: - return ActivityAnimations.getCloseEnter(context); + return ActivityAnimations.getCloseEnter(); case R.anim.activity_close_exit: - return ActivityAnimations.getCloseExit(context); + return ActivityAnimations.getCloseExit(); + case R.anim.app_starting_exit: + return ActivityAnimations.getAppStartingExit(); } } @@ -527,55 +533,52 @@ private static Interpolator createInterpolatorFromXml( /** @hide */ public final class ActivityAnimations { - public static final boolean sPerfAnimEnabled = SystemProperties.getBoolean( - "persist.sys.activity_anim_perf_override", false); - private static Animation sOpenEnter; private static Animation sOpenExit; private static Animation sCloseEnter; private static Animation sCloseExit; + private static Animation sAppStartingExit; private static SpringInterpolator sSpatialSpec; private static SpringInterpolator sEffectsSpec; private static final float DISTANCE = 0.333f; + private static final long APP_STARTING_EXIT_DURATION_MS = 150L; private ActivityAnimations() {} /** @hide */ - public static void preload(Context context) { + public static void preload() { sSpatialSpec = new SpringInterpolator(0.8f, 380f); sEffectsSpec = new SpringInterpolator(1.0f, 3800f); sOpenEnter = new ActivityAnimFactory() .fromX(1.0f) .toX(0.0f) - .fade(0.0f, 1.0f) .build(); sOpenExit = new ActivityAnimFactory() .fromX(0.0f) .toX(-DISTANCE) - .fade(1.0f, 0.0f) .build(); sCloseEnter = new ActivityAnimFactory() .fromX(-DISTANCE) .toX(0.0f) - .fade(0.0f, 1.0f) .build(); sCloseExit = new ActivityAnimFactory() .fromX(0.0f) .toX(1.0f) - .fade(1.0f, 0.0f) .build(); + sAppStartingExit = buildAppStartingExit(); } - private static int loadBackdropColor(Context context) { - return context.getColor( - com.android.internal.R.color.materialColorSurfaceContainer); + private static Animation buildAppStartingExit() { + Animation animation = new AlphaAnimation(1.0f, 0.0f); + animation.setDuration(APP_STARTING_EXIT_DURATION_MS); + animation.setInterpolator(new LinearInterpolator()); + return animation; } private static class ActivityAnimFactory { private float fromX = 0f, toX = 0f; - private float fromAlpha = 1f, toAlpha = 1f; public ActivityAnimFactory fromX(float ratio) { this.fromX = ratio; @@ -587,12 +590,6 @@ public ActivityAnimFactory toX(float ratio) { return this; } - public ActivityAnimFactory fade(float from, float to) { - this.fromAlpha = from; - this.toAlpha = to; - return this; - } - public Animation build() { AnimationSet animationSet = new AnimationSet(false); TranslateAnimation slide = new TranslateAnimation( @@ -604,40 +601,34 @@ public Animation build() { slide.setDuration(sSpatialSpec.getDurationMs()); slide.setInterpolator(sSpatialSpec); animationSet.addAnimation(slide); - if (fromAlpha != toAlpha) { - AlphaAnimation fade = new AlphaAnimation(fromAlpha, toAlpha); - fade.setDuration(sEffectsSpec.getDurationMs()); - fade.setInterpolator(sEffectsSpec); - animationSet.addAnimation(fade); - } - animationSet.setShowBackdrop(true); return animationSet; } } /** @hide */ - public static Animation getOpenEnter(Context context) { - sOpenEnter.setBackdropColor(loadBackdropColor(context)); + public static Animation getOpenEnter() { return sOpenEnter; } /** @hide */ - public static Animation getOpenExit(Context context) { - sOpenExit.setBackdropColor(loadBackdropColor(context)); + public static Animation getOpenExit() { return sOpenExit; } /** @hide */ - public static Animation getCloseEnter(Context context) { - sCloseEnter.setBackdropColor(loadBackdropColor(context)); + public static Animation getCloseEnter() { return sCloseEnter; } /** @hide */ - public static Animation getCloseExit(Context context) { - sCloseExit.setBackdropColor(loadBackdropColor(context)); + public static Animation getCloseExit() { return sCloseExit; } + + /** @hide */ + public static Animation getAppStartingExit() { + return sAppStartingExit; + } } /** @hide */ From 3ab9510c6d21dd70e8393d2a8e12fcbc1f3d492f Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:08:30 +0800 Subject: [PATCH 1209/1315] Add support for game space Change-Id: Iaaf20bcbaa50c42dd6b7739ae233b245a2c5ad5c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/app/IGameSpaceCallback.aidl | 21 ++ .../internal/app/IGameSpaceService.aidl | 23 ++ .../collection/NotificationEntry.java | 10 + .../StatusBarNotificationActivityStarter.java | 1 + .../android/server/AxExtServiceFactory.java | 2 + .../server/wm/ActivityTaskSupervisor.java | 1 + .../com/android/server/wm/DisplayContent.java | 1 + .../android/server/wm/GameListManager.java | 150 +++++++++++++ .../android/server/wm/GamePackageHandler.java | 97 +++++++++ .../android/server/wm/GameSpaceService.java | 206 ++++++++++++++++++ .../server/wm/GameStateDispatcher.java | 143 ++++++++++++ .../android/server/wm/KeyguardController.java | 4 + 12 files changed, 659 insertions(+) create mode 100644 core/java/com/android/internal/app/IGameSpaceCallback.aidl create mode 100644 core/java/com/android/internal/app/IGameSpaceService.aidl create mode 100644 services/core/java/com/android/server/wm/GameListManager.java create mode 100644 services/core/java/com/android/server/wm/GamePackageHandler.java create mode 100644 services/core/java/com/android/server/wm/GameSpaceService.java create mode 100644 services/core/java/com/android/server/wm/GameStateDispatcher.java diff --git a/core/java/com/android/internal/app/IGameSpaceCallback.aidl b/core/java/com/android/internal/app/IGameSpaceCallback.aidl new file mode 100644 index 000000000000..42b885ac1eb6 --- /dev/null +++ b/core/java/com/android/internal/app/IGameSpaceCallback.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +oneway interface IGameSpaceCallback { + void onGameStart(String packageName); + void onGameLeave(); +} diff --git a/core/java/com/android/internal/app/IGameSpaceService.aidl b/core/java/com/android/internal/app/IGameSpaceService.aidl new file mode 100644 index 000000000000..691caea94f06 --- /dev/null +++ b/core/java/com/android/internal/app/IGameSpaceService.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.app; + +import com.android.internal.app.IGameSpaceCallback; + +interface IGameSpaceService { + void registerCallback(IGameSpaceCallback callback); + void unregisterCallback(IGameSpaceCallback callback); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 55085271a8d2..3d172554fbe0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -48,6 +48,8 @@ import android.os.Bundle; import android.os.Parcelable; import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; @@ -886,6 +888,14 @@ private void updateIsBlockable() { } private boolean shouldSuppressVisualEffect(int effect) { + if (effect == SUPPRESSED_EFFECT_FULL_SCREEN_INTENT + && row != null + && Settings.Secure.getIntForUser( + row.getContext().getContentResolver(), + "ax_gaming_mode_active", 0, + UserHandle.USER_CURRENT) == 1) { + return true; + } if (isExemptFromDndVisualSuppression()) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 8d4ce26469d7..12ee69897636 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -702,6 +702,7 @@ private void removeHunAfterClick(ExpandableNotificationRow row) { @VisibleForTesting void launchFullScreenIntent(NotificationEntry entry) { + if (entry.shouldSuppressFullScreenIntent()) return; // Skip if device is in VR mode. if (mPresenter.isDeviceInVrMode()) { mLogger.logFullScreenIntentSuppressedByVR(entry); diff --git a/services/core/java/com/android/server/AxExtServiceFactory.java b/services/core/java/com/android/server/AxExtServiceFactory.java index 57c137b8a14b..7ef6d067f5e9 100644 --- a/services/core/java/com/android/server/AxExtServiceFactory.java +++ b/services/core/java/com/android/server/AxExtServiceFactory.java @@ -22,6 +22,7 @@ import com.android.server.spoof.AxSpoofManager; import com.android.server.spoof.IAxSpoofManager; import com.android.server.wm.AxSandboxService; +import com.android.server.wm.GameSpaceService; import com.android.server.wm.WindowManagerService; public class AxExtServiceFactory { @@ -86,6 +87,7 @@ public static T getOrCreate(IAxExtServiceFactory.ExtType type) { public static void systemReady() { AxSandboxService.systemReady(); getSpoofManager().systemReady(); + GameSpaceService.systemReady(); } public static void onLateSystemReady() { diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 361c220e8a97..d3b83eba4c5a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1916,6 +1916,7 @@ void removeTask(Task task, boolean killProcess, boolean removeFromRecents, Strin mBalController.checkActivityAllowedToClearTask( task, callingUid, callingPid, callerActivityClassName); AxSandboxService.get().removeTask(task, reason); + GameSpaceService.get().removeTask(task, reason); } finally { task.mInRemoveTask = false; mService.mChainTracker.endPartial(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index bcf3f7a16bd3..701ec2a2c340 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4156,6 +4156,7 @@ boolean setFocusedApp(ActivityRecord newFocus) { if (mDisplayId == DEFAULT_DISPLAY && newFocus != null) { AxSandboxService.get().onAppFocusChanged(newFocus, newTask); + GameSpaceService.get().onAppFocusChanged(newFocus, newTask); } getInputMonitor().setFocusedAppLw(newFocus); diff --git a/services/core/java/com/android/server/wm/GameListManager.java b/services/core/java/com/android/server/wm/GameListManager.java new file mode 100644 index 000000000000..eb43e1026560 --- /dev/null +++ b/services/core/java/com/android/server/wm/GameListManager.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class GameListManager { + + interface GameListChangeListener { + void onGameListChanged(); + } + + private static final String GAME_LIST_KEY = "gamespace_game_list"; + + private final Context mContext; + private final Map mGameList = Collections.synchronizedMap(new HashMap<>()); + private final List mListeners = new ArrayList<>(); + + GameListManager(Context context) { + mContext = context; + loadGameList(); + } + + void loadGameList() { + String raw = Settings.System.getStringForUser(mContext.getContentResolver(), + GAME_LIST_KEY, UserHandle.USER_CURRENT); + Map parsed = parseGameList(raw); + synchronized (mGameList) { + mGameList.clear(); + mGameList.putAll(parsed); + } + notifyListeners(); + } + + boolean isGame(String packageName) { + synchronized (mGameList) { + return mGameList.containsKey(packageName); + } + } + + boolean isGameInPerfMode(String packageName) { + synchronized (mGameList) { + return "2".equals(mGameList.get(packageName)); + } + } + + void registerGameListObserver(Handler handler) { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(GAME_LIST_KEY), + false, + new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange) { + loadGameList(); + } + }, + UserHandle.USER_ALL + ); + } + + private Map parseGameList(String raw) { + Map map = new HashMap<>(); + if (raw == null || raw.isEmpty()) return map; + + for (String entry : raw.split(";")) { + String[] parts = entry.split("="); + if (parts.length == 2 + && parts[0].matches("[a-zA-Z0-9_.]+") + && parts[1].matches("\\d+")) { + map.put(parts[0].trim(), parts[1].trim()); + } + } + return map; + } + + void addGame(String packageName) { + updateGameList(packageName, true); + } + + void removeGame(String packageName) { + updateGameList(packageName, false); + } + + private void updateGameList(String packageName, boolean add) { + ContentResolver cr = mContext.getContentResolver(); + String raw = Settings.System.getStringForUser(cr, GAME_LIST_KEY, UserHandle.USER_CURRENT); + Map gameMap = parseGameList(raw); + + boolean modified; + if (add) { + modified = !"2".equals(gameMap.get(packageName)); + if (modified) gameMap.put(packageName, "2"); + } else { + modified = gameMap.remove(packageName) != null; + } + + if (modified) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : gameMap.entrySet()) { + if (sb.length() > 0) sb.append(';'); + sb.append(e.getKey()).append('=').append(e.getValue()); + } + Settings.System.putStringForUser(cr, GAME_LIST_KEY, sb.toString(), + UserHandle.USER_CURRENT); + synchronized (mGameList) { + if (add) mGameList.put(packageName, "2"); + else mGameList.remove(packageName); + } + notifyListeners(); + } + } + + void addListener(GameListChangeListener listener) { + synchronized (mListeners) { + mListeners.add(listener); + } + } + + private void notifyListeners() { + synchronized (mListeners) { + for (GameListChangeListener listener : mListeners) { + listener.onGameListChanged(); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/GamePackageHandler.java b/services/core/java/com/android/server/wm/GamePackageHandler.java new file mode 100644 index 000000000000..44e01a539060 --- /dev/null +++ b/services/core/java/com/android/server/wm/GamePackageHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.widget.Toast; + +import com.android.server.UiThread; + +class GamePackageHandler { + + private final Context mContext; + private final PackageManager mPackageManager; + private final GameListManager mGameListManager; + private final Handler mBgHandler; + + GamePackageHandler(Context context, GameListManager manager, Handler bgHandler) { + mContext = context; + mPackageManager = context.getPackageManager(); + mGameListManager = manager; + mBgHandler = bgHandler; + } + + void registerPackageReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addDataScheme("package"); + + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String pkg = intent.getData() != null + ? intent.getData().getSchemeSpecificPart() : null; + if (pkg == null) return; + + mBgHandler.post(() -> { + String action = intent.getAction(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + if (isGame(pkg)) { + String label = getAppLabel(pkg); + mGameListManager.addGame(pkg); + UiThread.getHandler().post( + () -> showGameAddedToast(label)); + } + } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { + mGameListManager.removeGame(pkg); + } + }); + } + }, filter); + } + + private boolean isGame(String pkg) { + try { + ApplicationInfo info = mPackageManager.getApplicationInfo( + pkg, PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA)); + return info.category == ApplicationInfo.CATEGORY_GAME; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private String getAppLabel(String pkg) { + try { + return mPackageManager + .getApplicationLabel(mPackageManager.getApplicationInfo( + pkg, PackageManager.ApplicationInfoFlags.of(0))) + .toString(); + } catch (PackageManager.NameNotFoundException e) { + return pkg; + } + } + + private void showGameAddedToast(String appLabel) { + Toast.makeText(mContext, "Added " + appLabel + " to GameSpace", Toast.LENGTH_LONG).show(); + } +} diff --git a/services/core/java/com/android/server/wm/GameSpaceService.java b/services/core/java/com/android/server/wm/GameSpaceService.java new file mode 100644 index 000000000000..82bbf1de90a6 --- /dev/null +++ b/services/core/java/com/android/server/wm/GameSpaceService.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Slog; + +import com.android.internal.app.IGameSpaceCallback; +import com.android.internal.app.IGameSpaceService; +import com.android.server.NtServiceInjector; +import com.android.server.UiThread; +import com.android.server.am.ActivityManagerService; + +import java.util.concurrent.CopyOnWriteArrayList; + +public class GameSpaceService extends IGameSpaceService.Stub { + + private static final String TAG = "GameSpaceService"; + private static final boolean DEBUG = false; + + private static GameSpaceService sInstance; + + private final Context mContext; + private final ActivityManagerService mActivityManager; + private final CopyOnWriteArrayList mCallbacks = new CopyOnWriteArrayList<>(); + private final GameListManager mGameListManager; + private final GameStateDispatcher mGameStateDispatcher; + private final GamePackageHandler mGamePackageHandler; + + private final HandlerThread mBgThread = new HandlerThread("GameSpaceBg"); + private final Handler mBgHandler; + + private String mCurrentGame; + private Runnable mPendingBoost; + + private GameSpaceService(Context context, ActivityManagerService am) { + mContext = context; + mActivityManager = am; + mGameListManager = new GameListManager(context); + mGameStateDispatcher = new GameStateDispatcher(context, mCallbacks); + + mBgThread.start(); + mBgHandler = new Handler(mBgThread.getLooper()); + + mGamePackageHandler = new GamePackageHandler(context, mGameListManager, mBgHandler); + mGamePackageHandler.registerPackageReceiver(); + + mGameListManager.registerGameListObserver(mBgHandler); + mGameListManager.addListener(() -> { + mBgHandler.post(() -> { + if (mCurrentGame != null && mGameListManager.isGame(mCurrentGame)) { + boolean inPerfMode = mGameListManager.isGameInPerfMode(mCurrentGame); + mGameStateDispatcher.boostGame(inPerfMode); + } + }); + }); + } + + public static void systemReady() { + if (sInstance == null) { + sInstance = new GameSpaceService(NtServiceInjector.getCtx(), NtServiceInjector.getAm()); + ServiceManager.addService("game_space", sInstance); + Slog.i(TAG, "GameSpaceService initialized"); + } + } + + public static GameSpaceService get() { + return sInstance; + } + + private void startOverlay() { + final String currentGame = mCurrentGame; + if (currentGame == null) return; + + mBgHandler.post(() -> { + if (mPendingBoost != null) { + mBgHandler.removeCallbacks(mPendingBoost); + } + + if (mGameListManager.isGameInPerfMode(currentGame)) { + mPendingBoost = () -> mGameStateDispatcher.boostGame(true); + mBgHandler.postDelayed(mPendingBoost, 500); + } + + UiThread.getHandler().post( + () -> mGameStateDispatcher.dispatchGameState(true, currentGame)); + }); + } + + private void stopOverlay() { + mBgHandler.post(() -> { + if (mPendingBoost != null) { + mBgHandler.removeCallbacks(mPendingBoost); + mPendingBoost = null; + } + UiThread.getHandler().post( + () -> mGameStateDispatcher.dispatchGameState(false, null)); + mGameStateDispatcher.boostGame(false); + }); + } + + public void onAppFocusChanged(ActivityRecord record, Task task) { + if (record == null || record.packageName == null) return; + + String packageName = record.packageName; + + mBgHandler.post(() -> { + boolean gameActive = mCurrentGame != null + && mActivityManager.isPackageTopApp(mCurrentGame); + + if (task != null && task.getWindowingMode() == WINDOWING_MODE_FREEFORM && gameActive) { + if (DEBUG) Slog.d(TAG, "Freeform focused but game still TOP_APP, ignoring."); + return; + } + + boolean isGame = mGameListManager.isGame(packageName); + boolean shouldStartOverlay = false; + boolean shouldStopOverlay = false; + + if (isGame) { + if (!packageName.equals(mCurrentGame)) { + if (mCurrentGame != null) { + shouldStopOverlay = true; + } + mCurrentGame = packageName; + shouldStartOverlay = true; + } + } else if (mCurrentGame != null) { + mCurrentGame = null; + shouldStopOverlay = true; + } + + if (shouldStopOverlay) stopOverlay(); + if (shouldStartOverlay) startOverlay(); + }); + } + + public void removeTask(Task task, String reason) { + if (task == null) return; + + mBgHandler.post(() -> { + ActivityRecord top = task.getTopMostActivity(); + + if (mCurrentGame != null && top != null + && mCurrentGame.equals(top.packageName)) { + if (DEBUG) Slog.d(TAG, "removeTask: clearing active game " + mCurrentGame); + mCurrentGame = null; + stopOverlay(); + } + }); + } + + public void onKeyguardChanged(boolean showing) { + mBgHandler.post(() -> { + if (mCurrentGame == null) return; + + if (showing) { + stopOverlay(); + } else { + startOverlay(); + } + }); + } + + @Override + public void registerCallback(IGameSpaceCallback callback) { + if (callback == null || mCallbacks.contains(callback)) return; + + mCallbacks.add(callback); + + try { + IBinder binder = callback.asBinder(); + binder.linkToDeath(() -> { + mCallbacks.remove(callback); + if (DEBUG) Slog.d(TAG, "Callback died, removed"); + }, 0); + } catch (RemoteException e) { + mCallbacks.remove(callback); + } + } + + @Override + public void unregisterCallback(IGameSpaceCallback callback) { + mCallbacks.remove(callback); + } +} diff --git a/services/core/java/com/android/server/wm/GameStateDispatcher.java b/services/core/java/com/android/server/wm/GameStateDispatcher.java new file mode 100644 index 000000000000..12f5f63d4ef4 --- /dev/null +++ b/services/core/java/com/android/server/wm/GameStateDispatcher.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import android.content.Context; +import android.os.BatteryManager; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; + +import lineageos.health.HealthInterface; + +import com.android.internal.app.IGameSpaceCallback; + +import java.util.List; + +class GameStateDispatcher { + + private static final String TAG = "GameStateDispatcher"; + private static final String KEY_GAMING_MODE_ACTIVE = "ax_gaming_mode_active"; + private static final String KEY_BYPASS_CHARGE_ENABLED = "bypass_charge_enabled"; + + private final Context mContext; + private final List mCallbacks; + + private int mChargeControlLimit = 100; + private boolean mWasChargingControlEnabled = false; + + GameStateDispatcher(Context context, List callbacks) { + mContext = context; + mCallbacks = callbacks; + } + + void dispatchGameState(boolean active, String packageName) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + KEY_GAMING_MODE_ACTIVE, active ? 1 : 0, UserHandle.USER_CURRENT); + + for (IGameSpaceCallback callback : mCallbacks) { + try { + if (active && packageName != null) { + callback.onGameStart(packageName); + } else { + callback.onGameLeave(); + } + } catch (Exception e) { + Slog.w(TAG, "Removing dead callback", e); + mCallbacks.remove(callback); + } + } + + if (active) { + if (bypassChargeEnabled()) { + mChargeControlLimit = getChargingLimit(); + setBypassActive(true); + setSmartChargeLvl(battLevel()); + } + } else { + if (bypassChargeEnabled()) { + setBypassActive(false); + setSmartChargeLvl(mChargeControlLimit); + } + } + } + + void boostGame(boolean enable) { + int perfByUser = Settings.System.getIntForUser( + mContext.getContentResolver(), "power_mode_perf_by_user", 0, + UserHandle.USER_CURRENT); + if (perfByUser == 1) return; + + Settings.System.putIntForUser(mContext.getContentResolver(), + "persist.sys.power_mode_perf", enable ? 1 : 0, + UserHandle.USER_CURRENT); + SystemProperties.set("persist.sys.power_mode_perf", enable ? "1" : "0"); + } + + void setBypassCharge(boolean enable) { + if (!bypassChargeEnabled()) return; + + if (enable) { + mChargeControlLimit = getChargingLimit(); + } + + setBypassActive(enable); + setSmartChargeLvl(enable ? battLevel() : mChargeControlLimit); + } + + private boolean bypassChargeEnabled() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + KEY_BYPASS_CHARGE_ENABLED, 0, UserHandle.USER_CURRENT) == 1; + } + + private int getChargingLimit() { + try { + HealthInterface health = HealthInterface.getInstance(mContext); + mWasChargingControlEnabled = health.getEnabled(); + if (mWasChargingControlEnabled) { + return health.getLimit(); + } + } catch (Exception e) { + Slog.w(TAG, "Failed to get charging limit", e); + } + return 100; + } + + private int battLevel() { + BatteryManager bm = mContext.getSystemService(BatteryManager.class); + return bm != null ? bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) : -1; + } + + private void setSmartChargeLvl(int value) { + try { + HealthInterface health = HealthInterface.getInstance(mContext); + health.setMode(HealthInterface.MODE_LIMIT); + health.setLimit(value); + } catch (Exception e) { + Slog.w(TAG, "Failed to set charging limit", e); + } + } + + private void setBypassActive(boolean value) { + try { + HealthInterface health = HealthInterface.getInstance(mContext); + health.setEnabled(value || mWasChargingControlEnabled); + } catch (Exception e) { + Slog.w(TAG, "Failed to set charging bypass", e); + } + } +} diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 4c110747e40d..a8e6f24b30a3 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -244,6 +244,10 @@ void setKeyguardShown(int displayId, boolean keyguardShowing, boolean aodShowing state.mAodShowing = aodShowing; state.writeEventLog("setKeyguardShown"); + if (displayId == DEFAULT_DISPLAY && keyguardChanged) { + GameSpaceService.get().onKeyguardChanged(keyguardShowing); + } + if (keyguardChanged || (mWindowManager.mFlags.mAodTransition && aodChanged)) { if (keyguardChanged) { // Irrelevant to AOD. From 516716f95015a818eda7a4b71fd72eb57f9f9d61 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 30 Apr 2026 21:19:49 +0530 Subject: [PATCH 1210/1315] services: Add toggle for gamespace auto detect apps Signed-off-by: Pranav Vashi Signed-off-by: Mrick343 --- .../android/server/wm/GameListManager.java | 73 +++++++++++++++++++ .../android/server/wm/GamePackageHandler.java | 11 +++ 2 files changed, 84 insertions(+) diff --git a/services/core/java/com/android/server/wm/GameListManager.java b/services/core/java/com/android/server/wm/GameListManager.java index eb43e1026560..324de9307c95 100644 --- a/services/core/java/com/android/server/wm/GameListManager.java +++ b/services/core/java/com/android/server/wm/GameListManager.java @@ -25,8 +25,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; class GameListManager { @@ -35,14 +37,19 @@ interface GameListChangeListener { } private static final String GAME_LIST_KEY = "gamespace_game_list"; + private static final String DENIED_LIST_KEY = "gamespace_denied_list"; private final Context mContext; private final Map mGameList = Collections.synchronizedMap(new HashMap<>()); private final List mListeners = new ArrayList<>(); + private final Set mDeniedList = + Collections.synchronizedSet(new HashSet<>()); + GameListManager(Context context) { mContext = context; loadGameList(); + loadDeniedList(); } void loadGameList() { @@ -56,6 +63,60 @@ void loadGameList() { notifyListeners(); } + void loadDeniedList() { + String raw = Settings.System.getStringForUser(mContext.getContentResolver(), + DENIED_LIST_KEY, UserHandle.USER_CURRENT); + Set parsed = parseDeniedList(raw); + synchronized (mDeniedList) { + mDeniedList.clear(); + mDeniedList.addAll(parsed); + } + } + + private Set parseDeniedList(String raw) { + Set set = new HashSet<>(); + if (raw == null || raw.isEmpty()) return set; + for (String pkg : raw.split(";")) { + String trimmed = pkg.trim(); + if (!trimmed.isEmpty() && trimmed.matches("[a-zA-Z0-9_.]+")) { + set.add(trimmed); + } + } + return set; + } + + private void writeDeniedList() { + StringBuilder sb = new StringBuilder(); + synchronized (mDeniedList) { + for (String pkg : mDeniedList) { + if (sb.length() > 0) sb.append(';'); + sb.append(pkg); + } + } + Settings.System.putStringForUser(mContext.getContentResolver(), + DENIED_LIST_KEY, sb.toString(), UserHandle.USER_CURRENT); + } + + boolean isDenied(String packageName) { + synchronized (mDeniedList) { + return mDeniedList.contains(packageName); + } + } + + void addDenied(String packageName) { + synchronized (mDeniedList) { + if (!mDeniedList.add(packageName)) return; + } + writeDeniedList(); + } + + void removeDenied(String packageName) { + synchronized (mDeniedList) { + if (!mDeniedList.remove(packageName)) return; + } + writeDeniedList(); + } + boolean isGame(String packageName) { synchronized (mGameList) { return mGameList.containsKey(packageName); @@ -80,6 +141,18 @@ public void onChange(boolean selfChange) { }, UserHandle.USER_ALL ); + + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(DENIED_LIST_KEY), + false, + new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange) { + loadDeniedList(); + } + }, + UserHandle.USER_ALL + ); } private Map parseGameList(String raw) { diff --git a/services/core/java/com/android/server/wm/GamePackageHandler.java b/services/core/java/com/android/server/wm/GamePackageHandler.java index 44e01a539060..8b27cfd251ec 100644 --- a/services/core/java/com/android/server/wm/GamePackageHandler.java +++ b/services/core/java/com/android/server/wm/GamePackageHandler.java @@ -22,6 +22,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; import android.widget.Toast; import com.android.server.UiThread; @@ -40,6 +42,12 @@ class GamePackageHandler { mBgHandler = bgHandler; } + private boolean isAutoDetectEnabled() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + "gamespace_auto_game_detect", 1, + UserHandle.USER_CURRENT) != 0; + } + void registerPackageReceiver() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_ADDED); @@ -56,6 +64,9 @@ public void onReceive(Context context, Intent intent) { mBgHandler.post(() -> { String action = intent.getAction(); if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + if (!isAutoDetectEnabled()) return; + if (mGameListManager.isDenied(pkg)) return; + if (mGameListManager.isGame(pkg)) return; if (isGame(pkg)) { String label = getAppLabel(pkg); mGameListManager.addGame(pkg); From c886eb50eb69592a51852af1b82d21a17729f25c Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 30 Apr 2026 21:27:02 +0530 Subject: [PATCH 1211/1315] services: Make game added toast translatable Signed-off-by: Pranav Vashi Signed-off-by: Mrick343 --- core/res/res/values/matrixx_config.xml | 1 - core/res/res/values/matrixx_strings.xml | 4 +++- core/res/res/values/matrixx_symbols.xml | 3 +++ .../java/com/android/server/wm/GamePackageHandler.java | 7 ++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/res/res/values/matrixx_config.xml b/core/res/res/values/matrixx_config.xml index 95cf9c014d64..fcac34dc5273 100644 --- a/core/res/res/values/matrixx_config.xml +++ b/core/res/res/values/matrixx_config.xml @@ -4,7 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - sans-serif-light diff --git a/core/res/res/values/matrixx_strings.xml b/core/res/res/values/matrixx_strings.xml index 4b25afcad7f6..ef483f574dcc 100644 --- a/core/res/res/values/matrixx_strings.xml +++ b/core/res/res/values/matrixx_strings.xml @@ -5,7 +5,6 @@ --> - Settings restart required For all changes to take effect, a Settings app restart is required. Restart Settings app now? @@ -43,4 +42,7 @@ Tap to turn off sleep mode Sleep mode turned on Sleep mode turned off + + + %s added to GameSpace diff --git a/core/res/res/values/matrixx_symbols.xml b/core/res/res/values/matrixx_symbols.xml index 28b9d172e4a8..8612a396afbc 100644 --- a/core/res/res/values/matrixx_symbols.xml +++ b/core/res/res/values/matrixx_symbols.xml @@ -112,4 +112,7 @@ + + + diff --git a/services/core/java/com/android/server/wm/GamePackageHandler.java b/services/core/java/com/android/server/wm/GamePackageHandler.java index 8b27cfd251ec..929b76ded815 100644 --- a/services/core/java/com/android/server/wm/GamePackageHandler.java +++ b/services/core/java/com/android/server/wm/GamePackageHandler.java @@ -26,6 +26,7 @@ import android.provider.Settings; import android.widget.Toast; +import com.android.internal.R; import com.android.server.UiThread; class GamePackageHandler { @@ -103,6 +104,10 @@ private String getAppLabel(String pkg) { } private void showGameAddedToast(String appLabel) { - Toast.makeText(mContext, "Added " + appLabel + " to GameSpace", Toast.LENGTH_LONG).show(); + Toast.makeText( + mContext, + mContext.getString(R.string.gamespace_new_game_added, appLabel), + Toast.LENGTH_LONG + ).show(); } } From 2c067192c38512d363841948508e7f7c499f0f63 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 21 May 2024 06:13:48 +0800 Subject: [PATCH 1212/1315] SystemUI: Implement keyguard user switcher [1/2] Signed-off-by: minaripenguin Signed-off-by: Aston-Martinn Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../res/drawable/user_avatar_bg.xml | 2 +- .../layout/keyguard_bouncer_user_switcher.xml | 9 +++++-- .../SystemUI/res-keyguard/values/dimens.xml | 27 ++++++++++--------- .../KeyguardSecurityContainerController.java | 5 +++- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/SettingsLib/res/drawable/user_avatar_bg.xml b/packages/SettingsLib/res/drawable/user_avatar_bg.xml index 1f50496f3a5c..caf9103fc2a4 100644 --- a/packages/SettingsLib/res/drawable/user_avatar_bg.xml +++ b/packages/SettingsLib/res/drawable/user_avatar_bg.xml @@ -22,7 +22,7 @@ android:viewportHeight="100"> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml index 90851e2a921c..5be464eede17 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml @@ -38,13 +38,18 @@ android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="wrap_content" - android:layout_marginTop="30dp"> + android:layout_marginTop="24dp" + android:gravity="center"> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 1062fbc263b9..7e65aaaab7ee 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -52,7 +52,7 @@ 8dp - 60dp + 24dp 0dp @@ -140,28 +140,29 @@ 120dp - 20sp - 20sp - 24sp - 28dp + 15sp + 15sp + 20sp + 24dp 12dp - 248dp + 160dp 12dp 4dp 2dp - 10dp - 12dp - 44dp - 80dp + 6dp + 6dp + 24dp + 48dp + 24dp 16dp - 190dp - 222dp + 120dp + 152dp 64dp 8dp - 14sp + 13sp 12dp diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 1b353b019673..e702027385ad 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -46,6 +46,7 @@ import android.metrics.LogMaker; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -1186,7 +1187,9 @@ private boolean canUseOneHandedBouncer() { } private boolean canDisplayUserSwitcher() { - return getContext().getResources().getBoolean(R.bool.config_enableBouncerUserSwitcher); + return Settings.System.getIntForUser(getContext().getContentResolver(), + "kg_user_switcher_enabled", 0, + UserHandle.USER_CURRENT) == 1; } private void configureMode() { From f3ec8aa45e5517bf20fec55f38964b84db202d9d Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 2 May 2026 03:47:27 +0000 Subject: [PATCH 1213/1315] SystemUI: Cache immutable sysprop and optimize resolver lookup Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../compose/infinitegrid/CustomColorScheme.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt index e469783fabb8..cb0369da010e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.content.Context -import android.content.res.Configuration import android.os.SystemProperties import android.provider.Settings import androidx.compose.runtime.Composable @@ -25,37 +24,42 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext class CustomColorScheme(private val context: Context) { - val qsTileColor: Color + val qsTileColor: Color get() { - val blurEnabledByDefault = SystemProperties.getBoolean("ro.custom.blur.enable", false) + val resolver = context.contentResolver val blurEnabled = Settings.Global.getInt( - context.contentResolver, - Settings.Global.DISABLE_WINDOW_BLURS, + resolver, + Settings.Global.DISABLE_WINDOW_BLURS, if (blurEnabledByDefault) 0 else 1 ) != 1 - + val useAlternateColor = Settings.System.getInt( - context.contentResolver, + resolver, Settings.System.QS_TILE_ALTERNATE_COLOR, 0 ) == 1 - + val colorRes = if (blurEnabled) { - if (useAlternateColor) + if (useAlternateColor) com.android.internal.R.color.surface_effect_2 - else + else com.android.internal.R.color.surface_effect_1 } else { com.android.internal.R.color.materialColorSurfaceBright } - val tileColor = context.resources.getColor(colorRes, context.theme) - return Color(tileColor) + + return Color(context.resources.getColor(colorRes, context.theme)) } companion object { + private val blurEnabledByDefault: Boolean by lazy { + SystemProperties.getBoolean("ro.custom.blur.enable", false) + } + val current: CustomColorScheme @Composable @ReadOnlyComposable get() = CustomColorScheme(LocalContext.current) } } + From db6c1056332928d7628bea0cff628a0eb6ec2c59 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:19:52 +0800 Subject: [PATCH 1214/1315] libs: Gating hwui per-frame traces for userdebug builds simpleperf on userdebug shows RenderThread at 47.9% CPU during scroll, with 2.77% in __vfprintf and 1.05% in __strlen from 11 ATRACE points firing every frame in the render hot path (DrawFrameTask, CanvasContext, RenderNode, SkiaPipeline). on userdebug these are always active. add debug.hwui.trace_each_frame property (default false) with HWUI_FRAME_ATRACE_* conditional macros that short-circuit to a branch-not-taken when disabled Change-Id: Ia0e81e1f39a6b7b84fd44c588681d9fc5a653996 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- libs/hwui/Properties.cpp | 4 ++ libs/hwui/Properties.h | 4 ++ libs/hwui/RenderNode.cpp | 3 +- .../hwui/pipeline/skia/SkiaOpenGLPipeline.cpp | 3 +- libs/hwui/pipeline/skia/SkiaPipeline.cpp | 5 +- .../hwui/pipeline/skia/SkiaVulkanPipeline.cpp | 3 +- libs/hwui/renderthread/CanvasContext.cpp | 7 ++- libs/hwui/renderthread/DrawFrameTask.cpp | 7 ++- libs/hwui/utils/FrameTraceUtils.h | 59 +++++++++++++++++++ 9 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 libs/hwui/utils/FrameTraceUtils.h diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index eebe86819cfc..9623d38d502b 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -111,6 +111,8 @@ bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; bool Properties::skipTelemetry = false; +bool Properties::traceEachFrame = false; + int Properties::timeoutMultiplier = 1; StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; @@ -190,6 +192,8 @@ bool Properties::load() { skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY, hwui_flags::skip_eglmanager_telemetry()); + traceEachFrame = base::GetBoolProperty(PROPERTY_TRACE_EACH_FRAME, false); + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 9b8465fee1d5..fc9ae13e3bdd 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -236,6 +236,8 @@ enum DebugLevel { #define PROPERTY_SKIP_EGLMANAGER_TELEMETRY "debug.hwui.skip_eglmanager_telemetry" +#define PROPERTY_TRACE_EACH_FRAME "debug.hwui.trace_each_frame" + /** * Property for font reading library. */ @@ -357,6 +359,8 @@ class Properties { static bool hdr10bitPlus; static bool skipTelemetry; + static bool traceEachFrame; + static int timeoutMultiplier; static StretchEffectBehavior getStretchEffectBehavior() { diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 575d47b46683..5af6ca7063be 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -30,6 +30,7 @@ #include "FeatureFlags.h" #include "Properties.h" #include "TreeInfo.h" +#include "utils/FrameTraceUtils.h" #include "VectorDrawable.h" #include "private/hwui/WebViewFunctor.h" #include "renderthread/CanvasContext.h" @@ -135,7 +136,7 @@ int RenderNode::getAllocatedSize() { void RenderNode::prepareTree(TreeInfo& info) { - ATRACE_CALL(); + HWUI_FRAME_ATRACE_CALL(); LOG_ALWAYS_FATAL_IF(!info.damageAccumulator, "DamageAccumulator missing"); MarkAndSweepRemoved observer(&info); diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 0768f457972b..f7069c161fe5 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -38,6 +38,7 @@ #include "renderthread/EglManager.h" #include "renderthread/Frame.h" #include "renderthread/IRenderPipeline.h" +#include "utils/FrameTraceUtils.h" #include "utils/GLUtils.h" using namespace android::uirenderer::renderthread; @@ -178,7 +179,7 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( } { - ATRACE_NAME("flush commands"); + HWUI_FRAME_ATRACE_NAME("flush commands"); skgpu::ganesh::FlushAndSubmit(surface); } layerUpdateQueue->clear(); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 3c2fcb7dba8d..bc6fc26e66ac 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -49,6 +49,7 @@ #include "thread/CommonPool.h" #include "tools/SkSharingProc.h" #include "utils/Color.h" +#include "utils/FrameTraceUtils.h" #include "utils/String8.h" using namespace android::uirenderer::renderthread; @@ -72,7 +73,7 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) { LightingInfo::updateLighting(lightGeometry, lightInfo); - ATRACE_NAME("draw layers"); + HWUI_FRAME_ATRACE_NAME("draw layers"); renderLayersImpl(*layerUpdateQueue, opaque); layerUpdateQueue->clear(); } @@ -99,7 +100,7 @@ bool SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamag return false; } - ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), + HWUI_FRAME_ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), bounds.height()); layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 05020abb22ca..44cd53dd9741 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -34,6 +34,7 @@ #include "pipeline/skia/VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" +#include "utils/FrameTraceUtils.h" #include "renderthread/IRenderPipeline.h" using namespace android::uirenderer::renderthread; @@ -114,7 +115,7 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( VulkanManager::VkDrawResult drawResult; { - ATRACE_NAME("flush commands"); + HWUI_FRAME_ATRACE_NAME("flush commands"); drawResult = vulkanManager().finishFrame(backBuffer.get()); } layerUpdateQueue->clear(); diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index b24c2cc65b0e..3a9693459668 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -35,6 +35,7 @@ #include #include "../Properties.h" +#include "../utils/FrameTraceUtils.h" #include "AnimationContext.h" #include "ColorArea.h" #include "FeatureFlags.h" @@ -665,7 +666,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { SkRect windowDirty = computeDirtyRect(frame, &dirty); - ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); + HWUI_FRAME_ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); IRenderPipeline::DrawResult drawResult; { @@ -686,7 +687,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { const auto inputEventId = static_cast(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId)); - ATRACE_FORMAT( + HWUI_FRAME_ATRACE_FORMAT( "frameTimelineInfo(frameNumber=%llu, vsyncId=%lld, inputEventId=0x%" PRIx32 ")", frameCompleteNr, vsyncId, inputEventId); const ANativeWindowFrameTimelineInfo ftl = { @@ -979,7 +980,7 @@ const SkM44& CanvasContext::getPixelSnapMatrix() const { void CanvasContext::prepareAndDraw(RenderNode* node) { int64_t vsyncId = mRenderThread.timeLord().lastVsyncId(); - ATRACE_FORMAT("%s %" PRId64, __func__, vsyncId); + HWUI_FRAME_ATRACE_FORMAT("%s %" PRId64, __func__, vsyncId); nsecs_t vsync = mRenderThread.timeLord().computeFrameTimeNanos(); int64_t frameDeadline = mRenderThread.timeLord().lastFrameDeadline(); diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 826d00e1f32f..37e87060d1b7 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -24,6 +24,7 @@ #include "../DeferredLayerUpdater.h" #include "../DisplayList.h" #include "../Properties.h" +#include "../utils/FrameTraceUtils.h" #include "../RenderNode.h" #include "CanvasContext.h" #include "HardwareBufferRenderParams.h" @@ -80,7 +81,7 @@ int DrawFrameTask::drawFrame() { } void DrawFrameTask::postAndWait() { - ATRACE_CALL(); + HWUI_FRAME_ATRACE_CALL(); AutoMutex _lock(mLock); mRenderThread->queue().post([this]() { run(); }); mSignal.wait(mLock); @@ -88,7 +89,7 @@ void DrawFrameTask::postAndWait() { void DrawFrameTask::run() { const int64_t vsyncId = mFrameInfo[static_cast(FrameInfoIndex::FrameTimelineVsyncId)]; - ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId); + HWUI_FRAME_ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId); mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued); mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio); @@ -167,7 +168,7 @@ void DrawFrameTask::run() { } bool DrawFrameTask::syncFrameState(TreeInfo& info) { - ATRACE_CALL(); + HWUI_FRAME_ATRACE_CALL(); int64_t vsync = mFrameInfo[static_cast(FrameInfoIndex::Vsync)]; int64_t intendedVsync = mFrameInfo[static_cast(FrameInfoIndex::IntendedVsync)]; int64_t vsyncId = mFrameInfo[static_cast(FrameInfoIndex::FrameTimelineVsyncId)]; diff --git a/libs/hwui/utils/FrameTraceUtils.h b/libs/hwui/utils/FrameTraceUtils.h new file mode 100644 index 000000000000..8f6a9074d9f7 --- /dev/null +++ b/libs/hwui/utils/FrameTraceUtils.h @@ -0,0 +1,59 @@ +/* + * Copyright 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "Properties.h" + +namespace android { +namespace uirenderer { + +class ConditionalTraceEnder { +public: + inline ConditionalTraceEnder(bool active) : mActive(active) {} + inline ~ConditionalTraceEnder() { + if (mActive) ATRACE_END(); + } + +private: + bool mActive; +}; + +} // namespace uirenderer +} // namespace android + +#define HWUI_FRAME_ATRACE_CALL() \ + ::android::ScopedTrace PASTE(___tracer, __LINE__)( \ + CC_UNLIKELY(::android::uirenderer::Properties::traceEachFrame) \ + ? ATRACE_TAG : 0, \ + __FUNCTION__) + +#define HWUI_FRAME_ATRACE_NAME(name) \ + ::android::ScopedTrace PASTE(___tracer, __LINE__)( \ + CC_UNLIKELY(::android::uirenderer::Properties::traceEachFrame) \ + ? ATRACE_TAG : 0, \ + name) + +#define HWUI_FRAME_ATRACE_FORMAT(fmt, ...) \ + ::android::uirenderer::ConditionalTraceEnder PASTE(___tracer, __LINE__)( \ + CC_UNLIKELY(::android::uirenderer::Properties::traceEachFrame) && \ + CC_UNLIKELY(ATRACE_ENABLED()) && \ + (::android::TraceUtils::atraceFormatBegin(fmt, ##__VA_ARGS__), true)) From ca3926827247d2efdff62401d5e1b051ed4ebc8e Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 20 Apr 2026 04:17:00 +0000 Subject: [PATCH 1215/1315] libs: Add INTERACTION boost for default transition animations - from https://github.com/AxionAOSP/android_frameworks_base/commit/48dad93ecf25d2745fb116230e5d23d7ad809976 Co-authored-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../transition/DefaultTransitionHandler.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 9daeed72cfdb..20cdad92d421 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -88,9 +88,11 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.power.Boost; import android.hardware.HardwareBuffer; import android.os.Handler; import android.os.IBinder; +import android.os.PowerManagerInternal; import android.os.UserHandle; import android.util.ArrayMap; import android.view.SurfaceControl; @@ -107,6 +109,7 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; +import com.android.server.LocalServices; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.animation.SizeChangeAnimation; @@ -661,6 +664,21 @@ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionIn mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); } + long longestDurationMs = 0L; + for (int i = 0; i < animations.size(); ++i) { + final Animator a = animations.get(i); + if (a instanceof ValueAnimator va) { + final long d = va.getDuration(); + if (d > longestDurationMs) longestDurationMs = d; + } + } + if (longestDurationMs > 0L) { + PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class); + if (pmi != null) { + pmi.setPowerBoost(Boost.INTERACTION, (int) (longestDurationMs + 100L)); + } + } + // now start animations. they are started on another thread, so we have to post them // *after* applying the startTransaction mAnimExecutor.execute(() -> { From 55f8c41ce85aa914b19438d79715873c4d3e8882 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:57:40 +0800 Subject: [PATCH 1216/1315] libs: Bumping hwui shader cache for fewer fling stalls Change-Id: I84842e8e67ab3736108fb77ddc6e875011d4f7e5 Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- libs/hwui/pipeline/skia/ShaderCache.cpp | 4 ++-- libs/hwui/pipeline/skia/ShaderCache.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index 22f59a67bccb..c255c9b0c1df 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -36,8 +36,8 @@ namespace skiapipeline { // Cache size limits. static const size_t maxKeySize = 1024; -static const size_t maxValueSize = 2 * 1024 * 1024; -static const size_t maxTotalSize = 4 * 1024 * 1024; +static const size_t maxValueSize = 4 * 1024 * 1024; +static const size_t maxTotalSize = 32 * 1024 * 1024; static_assert(maxKeySize + maxValueSize < maxTotalSize); ShaderCache::ShaderCache() { diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 4c011613710b..0f76e2ff7205 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -170,7 +170,7 @@ class ShaderCache : public GrContextOptions::PersistentCache { * * WARNING: setting this to 0 will disable writing the cache to disk. */ - unsigned int mDeferredSaveDelayMs = 4 * 1000; + unsigned int mDeferredSaveDelayMs = 10 * 1000; /** * "mMutex" is the shared mutex used to prevent concurrent access to the member From 0fca29d5aa7a1bda78349c5fe862cfacc94bbc1c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:04:22 +0800 Subject: [PATCH 1217/1315] core: Swallowing scheduler exceptions Change-Id: Ic5c6f9074604139cc7de51f576128127c5a31c2c Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/jni/android_util_Process.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index fe69dd9a21b2..74c35777afe4 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -551,12 +551,7 @@ void android_os_Process_setThreadScheduler(JNIEnv* env, jclass clazz, #if defined(__linux__) struct sched_param param; param.sched_priority = pri; - int rc = sched_setscheduler(tid, policy, ¶m); - if (rc) { - signalExceptionForPriorityError(env, errno, tid); - } -#else - signalExceptionForPriorityError(env, ENOSYS, tid); + sched_setscheduler(tid, policy, ¶m); #endif } From 8d8da73885fe991922b82380ad716e3bf2144482 Mon Sep 17 00:00:00 2001 From: Mrick343 Date: Wed, 13 May 2026 06:06:22 +0000 Subject: [PATCH 1218/1315] Revert "Adding dynamic font feature" This reverts commit 73aaf3963883a4781c714611fbd2320068488410. Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 6 - core/java/android/widget/TextView.java | 15 - .../internal/util/android/FontController.java | 259 ------------------ graphics/java/android/graphics/Typeface.java | 51 +--- 4 files changed, 3 insertions(+), 328 deletions(-) delete mode 100644 core/java/com/android/internal/util/android/FontController.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d1785c28e45d..15972ee6296b 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -256,7 +256,6 @@ import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.internal.policy.DecorView; import com.android.internal.protolog.ProtoLog; -import com.android.internal.util.android.FontController; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.Preconditions; @@ -7185,8 +7184,6 @@ public void handleConfigurationChanged(Configuration config, int deviceId) { mConfigurationController.handleConfigurationChanged(config); updateDeviceIdForNonUIContexts(deviceId); - FontController.OnConfigurationChanged(getApplication().getResources()); - // These are only done to maintain @UnsupportedAppUsage and should be removed someday. mCurDefaultDisplayDpi = mConfigurationController.getCurDefaultDisplayDpi(); mConfiguration = mConfigurationController.getConfiguration(); @@ -7941,9 +7938,6 @@ private void handleBindApplication(AppBindData data) { data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null /* baseLoader */, false /* securityViolation */, true /* includeCode */, false /* registerPackage */, isSdkSandbox); - - FontController.OnConfigurationChanged(data.info.getResources()); - if (isSdkSandbox) { data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid, data.sdkSandboxClientAppPackage); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 695d5e2835a3..0589afd1185e 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -873,7 +873,6 @@ private void applyErrorDrawableIfNeeded(int layoutDirection) { // more bold. private int mFontWeightAdjustment; private Typeface mOriginalTypeface; - private String mFontFamily; // True if setKeyListener() has been explicitly called private boolean mListenerChanged = false; @@ -4389,7 +4388,6 @@ private void readTextAppearance(Context context, TypedArray appearance, attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex); if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; - mFontFamily = null; } break; case com.android.internal.R.styleable.TextAppearance_fontFamily: @@ -4402,7 +4400,6 @@ private void readTextAppearance(Context context, TypedArray appearance, } if (attributes.mFontTypeface == null) { attributes.mFontFamily = appearance.getString(attr); - mFontFamily = attributes.mFontFamily; } attributes.mFontFamilyExplicit = true; break; @@ -4498,7 +4495,6 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; - mFontFamily = null; } setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight); @@ -4532,10 +4528,6 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { setFontVariationSettings(attributes.mFontVariationSettings); } - if (Typeface.getFontName().equals("inter")) { - setFontFeatureSettings("'ss01'"); - } - if (attributes.mHasLineBreakStyle || attributes.mHasLineBreakWordStyle) { updateLineBreakConfigFromTextAppearance(attributes.mHasLineBreakStyle, attributes.mHasLineBreakWordStyle, attributes.mLineBreakStyle, @@ -4677,13 +4669,6 @@ protected void onConfigurationChanged(Configuration newConfig) { invalidate(); } } - - if (!TextUtils.equals(mFontFamily, Typeface.getFontName())) { - Typeface tf = Typeface.getOverrideTypeface(mFontFamily); - setTypeface(tf); - mFontFamily = Typeface.getFontName(); - } - if (mFontWeightAdjustment != newConfig.fontWeightAdjustment) { mFontWeightAdjustment = newConfig.fontWeightAdjustment; setTypeface(getTypeface()); diff --git a/core/java/com/android/internal/util/android/FontController.java b/core/java/com/android/internal/util/android/FontController.java deleted file mode 100644 index a684b6daeb12..000000000000 --- a/core/java/com/android/internal/util/android/FontController.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2025 AxionOS - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.internal.util.android; - -import android.app.ActivityThread; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.os.SystemProperties; -import android.util.ArrayMap; -import android.util.Log; -import android.text.TextUtils; -import com.android.internal.R; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.Set; - -public class FontController { - - private static final String TAG = "FontController"; - - private static FontController sInstance = null; - - private volatile Resources sResources = null; - - private volatile String sFontFamily = "sans-serif"; - - private static final Set OVERRIDE_FONTS = new HashSet<>(Arrays.asList( - "google", "sans-serif", "gsf-" - )); - - private static final Set SYS_OVERRIDE_FONTS = new HashSet<>(Arrays.asList( - "serif", "monospace", "variable" - )); - - private static final Set EXCLUDED_APPS = new HashSet<>(Arrays.asList( - "it.subito", - "tv.arte.plus7", - "com.google.android.gm" - )); - - private static final Set SYS_OVERRIDE_APPS = new HashSet<>(Arrays.asList( - "com.android.settings", - "com.android.systemui", - "com.android.launcher3", - "android" - )); - - private static final Map WEIGHT_MAP = new ArrayMap<>(); - static { - WEIGHT_MAP.put("thin", 100); - WEIGHT_MAP.put("extralight", 200); - WEIGHT_MAP.put("light", 300); - WEIGHT_MAP.put("normal", 400); - WEIGHT_MAP.put("regular", 400); - WEIGHT_MAP.put("medium", 500); - WEIGHT_MAP.put("semibold", 600); - WEIGHT_MAP.put("bold", 700); - WEIGHT_MAP.put("extrabold", 800); - WEIGHT_MAP.put("black", 900); - - WEIGHT_MAP.put("variable-display-large-emphasized", 500); - WEIGHT_MAP.put("variable-display-medium-emphasized", 500); - WEIGHT_MAP.put("variable-display-small-emphasized", 500); - WEIGHT_MAP.put("variable-headline-large-emphasized", 500); - WEIGHT_MAP.put("variable-headline-medium-emphasized", 500); - WEIGHT_MAP.put("variable-headline-small-emphasized", 500); - WEIGHT_MAP.put("variable-title-large-emphasized", 500); - WEIGHT_MAP.put("variable-title-medium-emphasized", 600); - WEIGHT_MAP.put("variable-title-small-emphasized", 600); - WEIGHT_MAP.put("variable-label-large-emphasized", 600); - WEIGHT_MAP.put("variable-label-medium-emphasized", 600); - WEIGHT_MAP.put("variable-label-small-emphasized", 600); - WEIGHT_MAP.put("variable-body-large-emphasized", 500); - WEIGHT_MAP.put("variable-body-medium-emphasized", 500); - WEIGHT_MAP.put("variable-body-small-emphasized", 500); - - WEIGHT_MAP.put("variable-display-large", 400); - WEIGHT_MAP.put("variable-display-medium", 400); - WEIGHT_MAP.put("variable-display-small", 400); - WEIGHT_MAP.put("variable-headline-large", 400); - WEIGHT_MAP.put("variable-headline-medium", 400); - WEIGHT_MAP.put("variable-headline-small", 400); - WEIGHT_MAP.put("variable-title-large", 400); - WEIGHT_MAP.put("variable-title-medium", 500); - WEIGHT_MAP.put("variable-title-small", 500); - WEIGHT_MAP.put("variable-label-large", 500); - WEIGHT_MAP.put("variable-label-medium", 500); - WEIGHT_MAP.put("variable-label-small", 500); - WEIGHT_MAP.put("variable-body-large", 400); - WEIGHT_MAP.put("variable-body-medium", 400); - WEIGHT_MAP.put("variable-body-small", 400); - } - - public static FontController get() { - if (sInstance == null) { - sInstance = new FontController(); - } - return sInstance; - } - - private FontController() {} - - public static void OnConfigurationChanged(Resources res) { - get().handleOnConfiguration(res); - } - - public static Typeface getOverrideTypeface(String fontToOverride) { - return get().getOverrideTypefaceInternal(fontToOverride); - } - - public static String getCurrentFontFamily() { - return get().getCurrentFont(); - } - - private String getCurrentFont() { - if (sResources == null) return sFontFamily; - try { - int configId = sResources.getIdentifier("config_bodyFontFamily", "string", "android"); - if (configId != 0) { - String currFont = sResources.getString(configId); - if (!TextUtils.equals(sFontFamily, currFont)) { - sFontFamily = currFont; - logger("Font changed to: " + sFontFamily); - } - } - } catch (Exception e) { - logger("getCurrentFont failed: " + e.getMessage()); - } - return sFontFamily; - } - - private Typeface getOverrideTypefaceInternal(String fontToOverride) { - if (fontToOverride == null) return null; - - String pkgName = getCurrentPackageName(); - - if (pkgName == null) return null; - - final boolean isSysPkg = SYS_OVERRIDE_APPS.contains(pkgName); - - if (pkgName != null && EXCLUDED_APPS.contains(pkgName) && !isSysPkg) { - logger("Excluded app, skipping override: " + pkgName); - return null; - } - - String currentFont = getCurrentFont(); - - if (fontToOverride.matches("^" + Pattern.quote(currentFont) + "(-.*)?$")) { - logger(fontToOverride + " matches current font root '" + currentFont + "', skipping override!"); - return null; - } - - boolean override = OVERRIDE_FONTS.stream().anyMatch(fontToOverride::contains) - || (isSysPkg && SYS_OVERRIDE_FONTS.stream().anyMatch(fontToOverride::contains)); - if (!override) { - logger("Not on override list, skipping override: " + fontToOverride); - return null; - } - - int adjustment = getFontWeightAdjustment(); - return TypefaceFactory.create(fontToOverride, currentFont, adjustment); - } - - private void handleOnConfiguration(Resources res) { - sResources = res; - String pkgName = getCurrentPackageName(); - if (pkgName == null || EXCLUDED_APPS.contains(pkgName)) return; - logger("handleOnConfiguration: Changing default font to: " + Typeface.getFontName()); - Typeface.changeFont(); - } - - private String getCurrentPackageName() { - try { - return ActivityThread.currentPackageName(); - } catch (Exception e) { - logger("getCurrentPackageName failed: " + e.getMessage()); - return null; - } - } - - private int getFontWeightAdjustment() { - try { - Resources res = sResources; - if (res == null) return 0; - Configuration cfg = res.getConfiguration(); - return cfg != null ? cfg.fontWeightAdjustment : 0; - } catch (Exception e) { - logger("getFontWeightAdjustment failed: " + e.getMessage()); - return 0; - } - } - - private static void logger(String msg) { - if (SystemProperties.getBoolean("persist.sys.ax_font_debug", false)) { - Log.d(TAG, msg); - } - } - - private static class TypefaceFactory { - - public static Typeface create(String fontToOverride, String currentFont, int fontWeightAdjustment) { - int weight = resolveWeightByName(fontToOverride); - - if (fontWeightAdjustment != 0) { - weight = Math.min(1000, Math.max(100, weight + fontWeightAdjustment)); - } - - boolean isBold = weight >= 700; - boolean isItalic = fontToOverride.contains("italic"); - - int style = Typeface.NORMAL; - if (isBold && isItalic) style = Typeface.BOLD_ITALIC; - else if (isBold) style = Typeface.BOLD; - else if (isItalic) style = Typeface.ITALIC; - - Typeface base = Typeface.getSystemDefaultTypeface(currentFont); - Typeface result = Typeface.create(base, style); - result = Typeface.create(result, weight, isItalic); - - logger("TypefaceFactory.create: fontToOverride=" + fontToOverride + - ", style=" + style + - ", weight=" + weight + - ", adj=" + fontWeightAdjustment + - ", isItalic=" + isItalic + - ", success=" + (result != null)); - - return result; - } - - private static int resolveWeightByName(String familyName) { - Integer exactMatch = WEIGHT_MAP.get(familyName); - if (exactMatch != null) { - return exactMatch; - } - for (Map.Entry entry : WEIGHT_MAP.entrySet()) { - if (familyName.contains(entry.getKey())) { - return entry.getValue(); - } - } - return 400; - } - } -} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index e6ff52d5273a..78768f52317f 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -27,11 +27,8 @@ import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UiThread; -import android.app.ActivityThread; -import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; -import android.content.res.Resources; import android.graphics.fonts.Font; import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontStyle; @@ -59,7 +56,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.android.FontController; import com.android.internal.util.Preconditions; import com.android.text.flags.Flags; @@ -81,7 +77,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -183,7 +178,7 @@ public static void clearTypefaceCachesForTestingPurpose() { */ @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769347) - static final Map sSystemFontMap = new HashMap<>(); + static final Map sSystemFontMap = new ArrayMap<>(); // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping. static ByteBuffer sSystemFontMapBuffer = null; @@ -296,8 +291,6 @@ private void completeTypefaceInitialization(@NonNull Typeface initializedTypefac */ public static final String DEFAULT_FAMILY = "sans-serif"; - private static volatile String sFontName = DEFAULT_FAMILY; - static { if (Flags.doNotOverwriteStaticFinalField()) { DEFAULT_BOLD = new Typeface(Typeface.BOLD, 700, null); @@ -1004,7 +997,7 @@ public CustomFallbackBuilder(@NonNull FontFamily family) { * @return The best matching typeface. */ public static Typeface create(String familyName, @Style int style) { - return create(getOverrideTypeface(familyName), style); + return create(getSystemDefaultTypeface(familyName), style); } /** @@ -1387,14 +1380,7 @@ public void releaseNativeObjectForTest() { mCleaner.run(); } - /** @hide */ - public static Typeface getOverrideTypeface(@NonNull String familyName) { - Typeface tf = FontController.getOverrideTypeface(familyName); - return tf == null ? getSystemDefaultTypeface(familyName) : tf; - } - - /** @hide */ - public static Typeface getSystemDefaultTypeface(@NonNull String familyName) { + private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { Typeface tf = sSystemFontMap.get(familyName); return tf == null ? Typeface.DEFAULT : tf; } @@ -1598,37 +1584,6 @@ public static void initializePendingTypefaceLocked(Typeface pending, String fami systemFontMap.put(familyName, pending); } - /** @hide */ - public static void changeFont() { - synchronized (sDynamicCacheLock) { - sDynamicTypefaceCache.evictAll(); - } - - String fontFamily = FontController.getCurrentFontFamily(); - - sFontName = fontFamily; - - Typeface tf = getOverrideTypeface(sFontName); - - Typeface tfBold = create(tf, BOLD); - Typeface tfItalic = create(tf, ITALIC); - Typeface tfItalicBold = create(tf, BOLD_ITALIC); - - nativeForceSetStaticFinalField("DEFAULT", tf); - nativeForceSetStaticFinalField("DEFAULT_BOLD", tfBold); - nativeForceSetStaticFinalField("SANS_SERIF", tf); - - changeDefaultFontForTest( - Arrays.asList( - tf, tfBold, tfItalic, tfItalicBold), - Arrays.asList(tf, Typeface.SERIF, Typeface.MONOSPACE)); - } - - /** @hide */ - public static String getFontName() { - return sFontName; - } - /** @hide */ @VisibleForTesting public static void setSystemFontMap(Map systemFontMap) { From ab5f8e62598f240ec10b6d082c6e0fb0cb59ccbf Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:09:21 +0800 Subject: [PATCH 1219/1315] core: Adding dynamic font feature optimized overriding of system fonts without reflection usage and wider font override support Change-Id: I4940b525584a3ad7c83193ff2484e6726c5437f9 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- core/java/android/app/ActivityThread.java | 3 + .../android/app/ConfigurationController.java | 2 + core/java/android/widget/TextView.java | 15 ++ .../internal/util/matrixx/FontController.java | 246 ++++++++++++++++++ graphics/java/android/graphics/Typeface.java | 72 ++++- .../graphics/fonts/OtfFontFileParser.java | 4 +- 6 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 core/java/com/android/internal/util/matrixx/FontController.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 15972ee6296b..f826964b4fc6 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7938,6 +7938,9 @@ private void handleBindApplication(AppBindData data) { data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null /* baseLoader */, false /* securityViolation */, true /* includeCode */, false /* registerPackage */, isSdkSandbox); + + Typeface.changeFont(); + if (isSdkSandbox) { data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid, data.sdkSandboxClientAppPackage); diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index 3ed1263a7112..676f0e3559c0 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -29,6 +29,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; +import android.graphics.Typeface; import android.os.LocaleList; import android.os.Trace; import android.util.DisplayMetrics; @@ -226,6 +227,7 @@ private void handleConfigurationChangedInner(@Nullable Configuration config, mActivityThread.collectComponentCallbacks(false /* includeUiContexts */); freeTextLayoutCachesIfNeeded(configDiff); + Typeface.changeFont(); if (callbacks != null) { final int size = callbacks.size(); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0589afd1185e..0dbbdc8d43e4 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -873,6 +873,7 @@ private void applyErrorDrawableIfNeeded(int layoutDirection) { // more bold. private int mFontWeightAdjustment; private Typeface mOriginalTypeface; + private String mFontFamily; // True if setKeyListener() has been explicitly called private boolean mListenerChanged = false; @@ -4388,6 +4389,7 @@ private void readTextAppearance(Context context, TypedArray appearance, attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex); if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } break; case com.android.internal.R.styleable.TextAppearance_fontFamily: @@ -4400,6 +4402,7 @@ private void readTextAppearance(Context context, TypedArray appearance, } if (attributes.mFontTypeface == null) { attributes.mFontFamily = appearance.getString(attr); + mFontFamily = attributes.mFontFamily; } attributes.mFontFamilyExplicit = true; break; @@ -4495,6 +4498,7 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight); @@ -4528,6 +4532,10 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { setFontVariationSettings(attributes.mFontVariationSettings); } + if (Typeface.getFontName().equals("inter")) { + setFontFeatureSettings("'ss01'"); + } + if (attributes.mHasLineBreakStyle || attributes.mHasLineBreakWordStyle) { updateLineBreakConfigFromTextAppearance(attributes.mHasLineBreakStyle, attributes.mHasLineBreakWordStyle, attributes.mLineBreakStyle, @@ -4669,6 +4677,13 @@ protected void onConfigurationChanged(Configuration newConfig) { invalidate(); } } + + if (!TextUtils.equals(mFontFamily, Typeface.getFontName())) { + Typeface tf = Typeface.getOverrideTypeface(mFontFamily); + setTypeface(tf); + mFontFamily = Typeface.getFontName(); + } + if (mFontWeightAdjustment != newConfig.fontWeightAdjustment) { mFontWeightAdjustment = newConfig.fontWeightAdjustment; setTypeface(getTypeface()); diff --git a/core/java/com/android/internal/util/matrixx/FontController.java b/core/java/com/android/internal/util/matrixx/FontController.java new file mode 100644 index 000000000000..dc4f938fc5e7 --- /dev/null +++ b/core/java/com/android/internal/util/matrixx/FontController.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util.matrixx; + +import android.app.ActivityThread; +import android.graphics.Typeface; +import android.os.SystemProperties; +import android.util.Log; +import android.util.LruCache; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class FontController { + + private static final String TAG = "FontController"; + + private static FontController sInstance; + + public static final String GSF_FONT = "google-sans-flex"; + public static final String PROP_DEFAULT_FONT = "persist.sys.ax_default_font"; + public static final String PROP_OVERLAY_FONTS = "persist.sys.ax_overlay_fonts"; + private static final String SEPARATOR = ":"; + + private volatile String[] mFontConfig; + private volatile String mRawProp; + + private static final int IDX_BODY = 0; + private static final int IDX_BODY_MEDIUM = 1; + private static final int IDX_HEADLINE = 2; + private static final int IDX_HEADLINE_MEDIUM = 3; + private static final int FIELD_COUNT = 4; + + private final LruCache mCache = new LruCache<>(30); + + private static final Set EXCLUDED_APPS = new HashSet<>(Arrays.asList( + "it.subito", + "tv.arte.plus7", + "com.google.android.gm" + )); + + private static final Set WHITELIST_FONTS = new HashSet<>(Arrays.asList( + "serif", + "monospace", + "cursive", + "NotoSansSC", + "NotoSansTC", + "NotoSansJP", + "NotoSansKR", + "NotoColorEmoji", + "NotoColorEmojiFlags", + "NotoSansMono", + "RobotoMono", + "DroidSansMono", + "CutiveMono", + "CarroisGothicSC", + "source-code-pro", + "gs-clock", + "google-sans-flex-clock" + )); + + private static final String[][] WEIGHT_KEYWORDS = { + {"thin", "100"}, + {"extralight", "200"}, + {"light", "300"}, + {"semibold", "600"}, + {"extrabold", "800"}, + {"bold", "700"}, + {"medium", "500"}, + {"black", "900"}, + }; + + public static FontController get() { + if (sInstance == null) { + sInstance = new FontController(); + } + return sInstance; + } + + private FontController() {} + + public static String getBodyFont() { + return get().resolveConfig()[IDX_BODY]; + } + + public static String getBodyFontMedium() { + return get().resolveConfig()[IDX_BODY_MEDIUM]; + } + + public static String getHeadlineFont() { + return get().resolveConfig()[IDX_HEADLINE]; + } + + public static String getHeadlineFontMedium() { + return get().resolveConfig()[IDX_HEADLINE_MEDIUM]; + } + + public static boolean isCustomFontActive() { + return !GSF_FONT.equals(getBodyFont()); + } + + public static boolean isExcludedApp() { + String pkg = getCurrentPackageName(); + return pkg != null && EXCLUDED_APPS.contains(pkg); + } + + public static boolean isFontWhitelisted(String familyName) { + if (familyName == null) return false; + for (String wl : WHITELIST_FONTS) { + if (familyName.contains(wl)) return true; + } + return false; + } + + public static Typeface getOverrideTypeface(String familyName) { + if (familyName == null) return null; + if (isExcludedApp()) return null; + if (isFontWhitelisted(familyName)) return null; + if (!isCustomFontActive()) return null; + + FontController fc = get(); + String[] config = fc.resolveConfig(); + + if (familyName.equals(config[IDX_HEADLINE])) { + return Typeface.getSystemDefaultTypeface(config[IDX_HEADLINE]); + } + if (familyName.equals(config[IDX_HEADLINE_MEDIUM])) { + return Typeface.getSystemDefaultTypeface(config[IDX_HEADLINE_MEDIUM]); + } + if (familyName.equals(config[IDX_BODY_MEDIUM])) { + return Typeface.getSystemDefaultTypeface(config[IDX_BODY_MEDIUM]); + } + + boolean isVariable = familyName.startsWith("variable-"); + int weight = isVariable ? resolveWeight(familyName) : resolveWeight(familyName); + boolean isItalic = familyName.contains("italic"); + + Typeface base = resolveBase(config, familyName, isVariable, weight); + + if (weight == 400 && !isItalic && base == Typeface.DEFAULT) { + return Typeface.DEFAULT; + } + + Typeface cached = fc.mCache.get(familyName); + if (cached != null) return cached; + + Typeface result = Typeface.create(base, weight, isItalic); + fc.mCache.put(familyName, result); + return result; + } + + private static Typeface resolveBase(String[] config, String name, + boolean isVariable, int weight) { + if (!isVariable) return Typeface.DEFAULT; + + boolean isHeadlineRole = name.startsWith("variable-display") + || name.startsWith("variable-headline"); + boolean isMediumWeight = weight >= 500; + + String baseName; + if (isHeadlineRole) { + baseName = isMediumWeight ? config[IDX_HEADLINE_MEDIUM] : config[IDX_HEADLINE]; + } else { + baseName = isMediumWeight ? config[IDX_BODY_MEDIUM] : config[IDX_BODY]; + } + + Typeface base = Typeface.getSystemDefaultTypeface(baseName); + return base != null ? base : Typeface.DEFAULT; + } + + private static int resolveWeight(String name) { + if (name.startsWith("variable-")) { + boolean emphasized = name.endsWith("-emphasized"); + if (name.contains("-title-medium") || name.contains("-title-small") + || name.contains("-label-")) { + return emphasized ? 600 : 500; + } + return emphasized ? 500 : 400; + } + + for (String[] entry : WEIGHT_KEYWORDS) { + if (name.contains(entry[0])) return Integer.parseInt(entry[1]); + } + return 400; + } + + public static void clearCaches() { + get().mCache.evictAll(); + } + + private String[] resolveConfig() { + String defaultFont = SystemProperties.get(PROP_DEFAULT_FONT, GSF_FONT); + String raw = SystemProperties.get(PROP_OVERLAY_FONTS, ""); + + if (raw.equals(mRawProp) && mFontConfig != null) { + return mFontConfig; + } + + String[] config; + if (raw.isEmpty()) { + config = new String[] { defaultFont, defaultFont, defaultFont, defaultFont }; + } else { + String[] parts = raw.split(SEPARATOR, -1); + config = new String[FIELD_COUNT]; + for (int i = 0; i < FIELD_COUNT; i++) { + config[i] = (i < parts.length && !parts[i].isEmpty()) + ? parts[i] : defaultFont; + } + } + + mRawProp = raw; + mFontConfig = config; + logger("Font config: body=" + config[IDX_BODY] + + " bodyMed=" + config[IDX_BODY_MEDIUM] + + " headline=" + config[IDX_HEADLINE] + + " headlineMed=" + config[IDX_HEADLINE_MEDIUM]); + return config; + } + + private static String getCurrentPackageName() { + try { + return ActivityThread.currentPackageName(); + } catch (Exception e) { + return null; + } + } + + private static void logger(String msg) { + if (SystemProperties.getBoolean("persist.sys.ax_font_debug", false)) { + Log.d(TAG, msg); + } + } +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 78768f52317f..bde80231dd66 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -27,8 +27,11 @@ import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UiThread; +import android.app.ActivityThread; +import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; +import android.content.res.Resources; import android.graphics.fonts.Font; import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontStyle; @@ -56,6 +59,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.matrixx.FontController; import com.android.internal.util.Preconditions; import com.android.text.flags.Flags; @@ -77,6 +81,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -154,7 +159,7 @@ private static class NoImagePreloadHolder { private static final LruCache> sVariableCache = new LruCache<>(16); private static final Object sVariableCacheLock = new Object(); - + /** @hide */ @VisibleForTesting public static void clearTypefaceCachesForTestingPurpose() { @@ -178,7 +183,7 @@ public static void clearTypefaceCachesForTestingPurpose() { */ @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769347) - static final Map sSystemFontMap = new ArrayMap<>(); + static final Map sSystemFontMap = new HashMap<>(); // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping. static ByteBuffer sSystemFontMapBuffer = null; @@ -289,7 +294,10 @@ private void completeTypefaceInitialization(@NonNull Typeface initializedTypefac * The key of the default font family. * @hide */ - public static final String DEFAULT_FAMILY = "sans-serif"; + public static final String DEFAULT_FAMILY = + SystemProperties.get("persist.sys.ax_default_font", "google-sans-flex"); + + private static volatile String sFontName = DEFAULT_FAMILY; static { if (Flags.doNotOverwriteStaticFinalField()) { @@ -997,7 +1005,7 @@ public CustomFallbackBuilder(@NonNull FontFamily family) { * @return The best matching typeface. */ public static Typeface create(String familyName, @Style int style) { - return create(getSystemDefaultTypeface(familyName), style); + return create(getOverrideTypeface(familyName), style); } /** @@ -1380,11 +1388,21 @@ public void releaseNativeObjectForTest() { mCleaner.run(); } - private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { + /** @hide */ + public static Typeface getOverrideTypeface(@NonNull String familyName) { + if (DEFAULT.mPendingTypeface != null && DEFAULT.mPendingTypeface.get() == null) { + return getSystemDefaultTypeface(familyName); + } + Typeface tf = FontController.getOverrideTypeface(familyName); + return tf != null ? tf : getSystemDefaultTypeface(familyName); + } + + /** @hide */ + public static Typeface getSystemDefaultTypeface(@NonNull String familyName) { Typeface tf = sSystemFontMap.get(familyName); return tf == null ? Typeface.DEFAULT : tf; } - + /** @hide */ @VisibleForTesting public static void initSystemDefaultTypefaces(Map fallbacks, @@ -1584,6 +1602,47 @@ public static void initializePendingTypefaceLocked(Typeface pending, String fami systemFontMap.put(familyName, pending); } + /** @hide */ + public static void changeFont() { + synchronized (sStyledCacheLock) { + sStyledTypefaceCache.clear(); + } + synchronized (sWeightCacheLock) { + sWeightTypefaceCache.clear(); + } + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.evictAll(); + } + + FontController.clearCaches(); + + sFontName = FontController.getBodyFont(); + + Typeface base = sSystemFontMap.get(sFontName); + if (base == null) base = sSystemFontMap.get(DEFAULT_FAMILY); + if (base == null) return; + + sDefaultTypeface = base; + + Typeface tf = create(base, NORMAL); + Typeface tfBold = create(base, BOLD); + Typeface tfItalic = create(base, ITALIC); + Typeface tfItalicBold = create(base, BOLD_ITALIC); + + nativeForceSetStaticFinalField("DEFAULT", tf); + nativeForceSetStaticFinalField("DEFAULT_BOLD", tfBold); + nativeForceSetStaticFinalField("SANS_SERIF", tf); + + changeDefaultFontForTest( + Arrays.asList(tf, tfBold, tfItalic, tfItalicBold), + Arrays.asList(tf, Typeface.SERIF, Typeface.MONOSPACE)); + } + + /** @hide */ + public static String getFontName() { + return sFontName; + } + /** @hide */ @VisibleForTesting public static void setSystemFontMap(Map systemFontMap) { @@ -1718,6 +1777,7 @@ public static void init() { // Preload Roboto-Regular.ttf in Zygote for improving app launch performance. preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "Roboto-Regular.ttf"); preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "RobotoStatic-Regular.ttf"); + preloadFontFile(SystemFonts.OEM_FONT_DIR + "GoogleSansFlex-Regular.ttf"); preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "GoogleSans-Regular.ttf"); preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "GoogleSans-Italic.ttf"); diff --git a/services/core/java/com/android/server/graphics/fonts/OtfFontFileParser.java b/services/core/java/com/android/server/graphics/fonts/OtfFontFileParser.java index 1ed3972ed309..8e00c4f7f404 100644 --- a/services/core/java/com/android/server/graphics/fonts/OtfFontFileParser.java +++ b/services/core/java/com/android/server/graphics/fonts/OtfFontFileParser.java @@ -87,7 +87,9 @@ public void tryToCreateTypeface(File file) throws Throwable { try { Font font = new Font.Builder(buffer).build(); FontFamily family = new FontFamily.Builder(font).build(); - Typeface typeface = new Typeface.CustomFallbackBuilder(family).build(); + Typeface typeface = new Typeface.CustomFallbackBuilder(family) + .setSystemFallback(Typeface.DEFAULT_FAMILY) + .build(); TextPaint p = new TextPaint(); p.setTextSize(24f); From be29f295b202f50fccb06344ca0e1e04b078c206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?GHOST=20=7C=20=E3=82=B4=E3=83=BC=E3=82=B9=E3=83=88?= <90827302+Ghosuto@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:49:10 +0530 Subject: [PATCH 1220/1315] core: Fix MD3 variable font weight mapping Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/FontController.java | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/core/java/com/android/internal/util/matrixx/FontController.java b/core/java/com/android/internal/util/matrixx/FontController.java index dc4f938fc5e7..b3088ad6a5c1 100644 --- a/core/java/com/android/internal/util/matrixx/FontController.java +++ b/core/java/com/android/internal/util/matrixx/FontController.java @@ -15,13 +15,17 @@ package com.android.internal.util.matrixx; import android.app.ActivityThread; +import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Typeface; import android.os.SystemProperties; +import android.util.ArrayMap; import android.util.Log; import android.util.LruCache; import java.util.Arrays; import java.util.HashSet; +import java.util.Map; import java.util.Set; public class FontController { @@ -83,6 +87,40 @@ public class FontController { {"black", "900"}, }; + private static final Map VARIABLE_WEIGHT_MAP = new ArrayMap<>(); + static { + VARIABLE_WEIGHT_MAP.put("variable-display-large", 400); + VARIABLE_WEIGHT_MAP.put("variable-display-medium", 400); + VARIABLE_WEIGHT_MAP.put("variable-display-small", 400); + VARIABLE_WEIGHT_MAP.put("variable-headline-large", 400); + VARIABLE_WEIGHT_MAP.put("variable-headline-medium", 400); + VARIABLE_WEIGHT_MAP.put("variable-headline-small", 400); + VARIABLE_WEIGHT_MAP.put("variable-title-large", 400); + VARIABLE_WEIGHT_MAP.put("variable-title-medium", 500); + VARIABLE_WEIGHT_MAP.put("variable-title-small", 500); + VARIABLE_WEIGHT_MAP.put("variable-label-large", 500); + VARIABLE_WEIGHT_MAP.put("variable-label-medium", 500); + VARIABLE_WEIGHT_MAP.put("variable-label-small", 500); + VARIABLE_WEIGHT_MAP.put("variable-body-large", 400); + VARIABLE_WEIGHT_MAP.put("variable-body-medium", 400); + VARIABLE_WEIGHT_MAP.put("variable-body-small", 400); + VARIABLE_WEIGHT_MAP.put("variable-display-large-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-display-medium-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-display-small-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-headline-large-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-headline-medium-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-headline-small-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-title-large-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-title-medium-emphasized", 600); + VARIABLE_WEIGHT_MAP.put("variable-title-small-emphasized", 600); + VARIABLE_WEIGHT_MAP.put("variable-label-large-emphasized", 600); + VARIABLE_WEIGHT_MAP.put("variable-label-medium-emphasized", 600); + VARIABLE_WEIGHT_MAP.put("variable-label-small-emphasized", 600); + VARIABLE_WEIGHT_MAP.put("variable-body-large-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-body-medium-emphasized", 500); + VARIABLE_WEIGHT_MAP.put("variable-body-small-emphasized", 500); + } + public static FontController get() { if (sInstance == null) { sInstance = new FontController(); @@ -145,9 +183,14 @@ public static Typeface getOverrideTypeface(String familyName) { } boolean isVariable = familyName.startsWith("variable-"); - int weight = isVariable ? resolveWeight(familyName) : resolveWeight(familyName); + int weight = resolveWeight(familyName); boolean isItalic = familyName.contains("italic"); + int weightAdjustment = getFontWeightAdjustment(); + if (weightAdjustment != 0) { + weight = Math.min(1000, Math.max(100, weight + weightAdjustment)); + } + Typeface base = resolveBase(config, familyName, isVariable, weight); if (weight == 400 && !isItalic && base == Typeface.DEFAULT) { @@ -166,8 +209,7 @@ private static Typeface resolveBase(String[] config, String name, boolean isVariable, int weight) { if (!isVariable) return Typeface.DEFAULT; - boolean isHeadlineRole = name.startsWith("variable-display") - || name.startsWith("variable-headline"); + boolean isHeadlineRole = name.startsWith("variable-headline"); boolean isMediumWeight = weight >= 500; String baseName; @@ -183,12 +225,9 @@ private static Typeface resolveBase(String[] config, String name, private static int resolveWeight(String name) { if (name.startsWith("variable-")) { - boolean emphasized = name.endsWith("-emphasized"); - if (name.contains("-title-medium") || name.contains("-title-small") - || name.contains("-label-")) { - return emphasized ? 600 : 500; - } - return emphasized ? 500 : 400; + Integer exact = VARIABLE_WEIGHT_MAP.get(name); + if (exact != null) return exact; + return name.endsWith("-emphasized") ? 500 : 400; } for (String[] entry : WEIGHT_KEYWORDS) { @@ -197,6 +236,19 @@ private static int resolveWeight(String name) { return 400; } + private static int getFontWeightAdjustment() { + try { + ActivityThread at = ActivityThread.currentActivityThread(); + if (at == null) return 0; + Resources res = at.getApplication().getResources(); + if (res == null) return 0; + Configuration cfg = res.getConfiguration(); + return cfg != null ? cfg.fontWeightAdjustment : 0; + } catch (Exception e) { + return 0; + } + } + public static void clearCaches() { get().mCache.evictAll(); } From f7ca512de71b737e27e95a692abc2bef4d460d4f Mon Sep 17 00:00:00 2001 From: Pranav Praveen <72447760+TwistedVision518@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:41:30 +0000 Subject: [PATCH 1221/1315] graphics: Introduce selectable emoji customization Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/android/graphics/FontListParser.java | 52 ++++++++++++++++--- .../android/graphics/fonts/SystemFonts.java | 52 +++++++++++++++---- 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index da40a7ac3a49..9066f55e5531 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -39,6 +39,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -110,17 +111,35 @@ public static FontConfig parse( long lastModifiedDate, int configVersion ) throws IOException, XmlPullParserException { - FontCustomizationParser.Result oemCustomization; + final List customizationXmlPaths = new ArrayList<>(); if (oemCustomizationXmlPath != null) { - try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) { - oemCustomization = FontCustomizationParser.parse(is, productFontDir, - updatableFontMap); + customizationXmlPaths.add(oemCustomizationXmlPath); + } + return parse(fontsXmlPath, systemFontDir, customizationXmlPaths, productFontDir, + updatableFontMap, lastModifiedDate, configVersion); + } + + /** + * Parses system font config XMLs with multiple OEM customization layers. + */ + public static FontConfig parse( + @NonNull String fontsXmlPath, + @NonNull String systemFontDir, + @NonNull List customizationXmlPaths, + @Nullable String productFontDir, + @Nullable Map updatableFontMap, + long lastModifiedDate, + int configVersion + ) throws IOException, XmlPullParserException { + FontCustomizationParser.Result oemCustomization = new FontCustomizationParser.Result(); + for (String customizationXmlPath : customizationXmlPaths) { + try (InputStream is = new FileInputStream(customizationXmlPath)) { + oemCustomization = mergeCustomizationResults( + oemCustomization, + FontCustomizationParser.parse(is, productFontDir, updatableFontMap)); } catch (IOException e) { - // OEM customization may not exists. Ignoring - oemCustomization = new FontCustomizationParser.Result(); + // OEM customization may not exist. Ignoring. } - } else { - oemCustomization = new FontCustomizationParser.Result(); } try (InputStream is = new FileInputStream(fontsXmlPath)) { @@ -132,6 +151,23 @@ public static FontConfig parse( } } + private static FontCustomizationParser.Result mergeCustomizationResults( + @NonNull FontCustomizationParser.Result base, + @NonNull FontCustomizationParser.Result extra) { + final Map namedFamilies = + new HashMap<>(base.getAdditionalNamedFamilies()); + namedFamilies.putAll(extra.getAdditionalNamedFamilies()); + + final List localeFallbacks = + new ArrayList<>(base.getLocaleFamilyCustomizations()); + localeFallbacks.addAll(extra.getLocaleFamilyCustomizations()); + + final List aliases = new ArrayList<>(base.getAdditionalAliases()); + aliases.addAll(extra.getAdditionalAliases()); + + return new FontCustomizationParser.Result(namedFamilies, localeFallbacks, aliases); + } + /** * Parses the familyset tag in font.xml * @param parser a XML pull parser diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 399345abd8ab..5f0109060d6e 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -25,6 +25,7 @@ import android.graphics.FontListParser; import android.graphics.Typeface; import android.os.LocaleList; +import android.os.SystemProperties; import android.ravenwood.annotation.RavenwoodReplace; import android.text.FontConfig; import android.util.ArrayMap; @@ -63,6 +64,13 @@ public final class SystemFonts { /** @hide */ public static final String SYSTEM_FONT_DIR = getSystemFontDir(); private static final String OEM_XML = "/product/etc/fonts_customization.xml"; + private static final String OEM_EMOJI_XML_IOS = "/product/etc/fonts_customization_emoji_ios.xml"; + private static final String OEM_EMOJI_XML_SAMSUNG = + "/product/etc/fonts_customization_emoji_samsung.xml"; + private static final String PROP_EMOJI_STYLE = "persist.sys.ax_emoji_style"; + private static final String EMOJI_STYLE_ANDROID = "android"; + private static final String EMOJI_STYLE_IOS = "ios"; + private static final String EMOJI_STYLE_SAMSUNG = "samsung"; /** @hide */ public static final String OEM_FONT_DIR = "/product/fonts/"; @@ -352,8 +360,9 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na long lastModifiedDate, int configVersion ) { - return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, - updatableFontMap, lastModifiedDate, configVersion); + return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, + getEmojiCustomizationXml(), OEM_FONT_DIR, updatableFontMap, + lastModifiedDate, configVersion); } /** @@ -368,8 +377,9 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na long lastModifiedDate, int configVersion ) { - return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, - updatableFontMap, lastModifiedDate, configVersion); + return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, + getEmojiCustomizationXml(), OEM_FONT_DIR, updatableFontMap, + lastModifiedDate, configVersion); } /** @@ -377,22 +387,23 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na * @hide */ public static @NonNull FontConfig getSystemPreinstalledFontConfig() { - return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null, - 0, 0); + return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, + getEmojiCustomizationXml(), OEM_FONT_DIR, null, 0, 0); } /** * @hide */ public static @NonNull FontConfig getSystemPreinstalledFontConfigFromLegacyXml() { - return getSystemFontConfigInternal(LEGACY_FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, - null, 0, 0); + return getSystemFontConfigInternal(LEGACY_FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, + getEmojiCustomizationXml(), OEM_FONT_DIR, null, 0, 0); } /* package */ static @NonNull FontConfig getSystemFontConfigInternal( @NonNull String fontsXml, @NonNull String systemFontDir, @Nullable String oemXml, + @Nullable String emojiXml, @Nullable String productFontDir, @Nullable Map updatableFontMap, long lastModifiedDate, @@ -400,8 +411,15 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na ) { try { Log.i(TAG, "Loading font config from " + fontsXml); - return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir, - updatableFontMap, lastModifiedDate, configVersion); + final List customizationXmls = new ArrayList<>(); + if (oemXml != null) { + customizationXmls.add(oemXml); + } + if (emojiXml != null) { + customizationXmls.add(emojiXml); + } + return FontListParser.parse(fontsXml, systemFontDir, customizationXmls, + productFontDir, updatableFontMap, lastModifiedDate, configVersion); } catch (IOException e) { Log.e(TAG, "Failed to open/read system font configurations.", e); return new FontConfig(Collections.emptyList(), Collections.emptyList(), @@ -413,6 +431,20 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na } } + private static @Nullable String getEmojiCustomizationXml() { + final String style = SystemProperties.get(PROP_EMOJI_STYLE, EMOJI_STYLE_ANDROID); + if (EMOJI_STYLE_SAMSUNG.equals(style)) { + return OEM_EMOJI_XML_SAMSUNG; + } + if (EMOJI_STYLE_IOS.equals(style)) { + return OEM_EMOJI_XML_IOS; + } + if (EMOJI_STYLE_ANDROID.equals(style)) { + return null; + } + return OEM_EMOJI_XML_IOS; + } + /** * Build the system fallback from FontConfig. * @hide From fbc802dee018cc9b578daa586b4bf4769c88d01b Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 1 May 2026 07:01:30 +0000 Subject: [PATCH 1222/1315] graphics: Add swiftui and facebook emoji Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../java/android/graphics/fonts/SystemFonts.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 5f0109060d6e..1a4ac71552e0 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -67,10 +67,16 @@ public final class SystemFonts { private static final String OEM_EMOJI_XML_IOS = "/product/etc/fonts_customization_emoji_ios.xml"; private static final String OEM_EMOJI_XML_SAMSUNG = "/product/etc/fonts_customization_emoji_samsung.xml"; + private static final String OEM_EMOJI_XML_SWIFTUI = + "/product/etc/fonts_customization_emoji_swiftui.xml"; + private static final String OEM_EMOJI_XML_FACEBOOK = + "/product/etc/fonts_customization_emoji_facebook.xml"; private static final String PROP_EMOJI_STYLE = "persist.sys.ax_emoji_style"; private static final String EMOJI_STYLE_ANDROID = "android"; private static final String EMOJI_STYLE_IOS = "ios"; private static final String EMOJI_STYLE_SAMSUNG = "samsung"; + private static final String EMOJI_STYLE_SWIFTUI = "swiftui"; + private static final String EMOJI_STYLE_FACEBOOK = "facebook"; /** @hide */ public static final String OEM_FONT_DIR = "/product/fonts/"; @@ -439,6 +445,12 @@ private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList na if (EMOJI_STYLE_IOS.equals(style)) { return OEM_EMOJI_XML_IOS; } + if (EMOJI_STYLE_SWIFTUI.equals(style)) { + return OEM_EMOJI_XML_SWIFTUI; + } + if (EMOJI_STYLE_FACEBOOK.equals(style)) { + return OEM_EMOJI_XML_FACEBOOK; + } if (EMOJI_STYLE_ANDROID.equals(style)) { return null; } From b9f25db34044cda6cdfd96a6b6723f29c9457708 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:48:10 +0800 Subject: [PATCH 1223/1315] core: Adding omnijaws front end hooks Change-Id: I908179bcc7e7131d33e54f90791c75543c2f5f9f Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../internal/util/matrixx/OmniJawsClient.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/core/java/com/android/internal/util/matrixx/OmniJawsClient.java b/core/java/com/android/internal/util/matrixx/OmniJawsClient.java index fdbaafb14b75..8ad343fb6df0 100644 --- a/core/java/com/android/internal/util/matrixx/OmniJawsClient.java +++ b/core/java/com/android/internal/util/matrixx/OmniJawsClient.java @@ -49,6 +49,7 @@ public class OmniJawsClient { public static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; public static final Uri WEATHER_URI = Uri.parse("content://org.omnirom.omnijaws.provider/weather"); public static final Uri SETTINGS_URI = Uri.parse("content://org.omnirom.omnijaws.provider/settings"); + public static final Uri HOURLY_URI = Uri.parse("content://org.omnirom.omnijaws.provider/hourly"); public static final String WEATHER_UPDATE = SERVICE_PACKAGE + ".WEATHER_UPDATE"; public static final String WEATHER_ERROR = SERVICE_PACKAGE + ".WEATHER_ERROR"; @@ -62,7 +63,8 @@ public class OmniJawsClient { public static final String[] WEATHER_PROJECTION = { "city", "wind_speed", "wind_direction", "condition_code", "temperature", "humidity", "condition", "forecast_low", "forecast_high", "forecast_condition", - "forecast_condition_code", "time_stamp", "forecast_date", "pin_wheel" + "forecast_condition_code", "time_stamp", "forecast_date", "pin_wheel", + "feels_like", "pressure", "uvi", "visibility", "dew_point", "sunrise", "sunset" }; public static final String[] SETTINGS_PROJECTION = { @@ -160,6 +162,21 @@ public void queryWeather(Context context) { mCachedInfo.condition = weatherCursor.getString(6); mCachedInfo.timeStamp = Long.parseLong(weatherCursor.getString(11)); mCachedInfo.pinWheel = weatherCursor.getString(13); + + int colFeelsLike = weatherCursor.getColumnIndex("feels_like"); + if (colFeelsLike != -1) mCachedInfo.feelsLike = weatherCursor.getFloat(colFeelsLike); + int colPressure = weatherCursor.getColumnIndex("pressure"); + if (colPressure != -1) mCachedInfo.pressure = weatherCursor.getFloat(colPressure); + int colUvi = weatherCursor.getColumnIndex("uvi"); + if (colUvi != -1) mCachedInfo.uvi = weatherCursor.getFloat(colUvi); + int colVisibility = weatherCursor.getColumnIndex("visibility"); + if (colVisibility != -1) mCachedInfo.visibility = weatherCursor.getFloat(colVisibility); + int colDewPoint = weatherCursor.getColumnIndex("dew_point"); + if (colDewPoint != -1) mCachedInfo.dewPoint = weatherCursor.getFloat(colDewPoint); + int colSunrise = weatherCursor.getColumnIndex("sunrise"); + if (colSunrise != -1) mCachedInfo.sunrise = weatherCursor.getLong(colSunrise); + int colSunset = weatherCursor.getColumnIndex("sunset"); + if (colSunset != -1) mCachedInfo.sunset = weatherCursor.getLong(colSunset); } else { DayForecast day = new DayForecast(); day.low = getFormattedValue(weatherCursor.getFloat(7)); @@ -192,6 +209,28 @@ public void queryWeather(Context context) { Log.e(TAG, "queryWeather: settings", e); } + if (mCachedInfo != null) { + try (Cursor hourlyCursor = context.getContentResolver().query( + HOURLY_URI, null, null, null, null)) { + if (hourlyCursor != null && hourlyCursor.getCount() > 0) { + List hourly = new ArrayList<>(); + while (hourlyCursor.moveToNext()) { + HourlyForecast h = new HourlyForecast(); + h.temperature = hourlyCursor.getFloat(hourlyCursor.getColumnIndex("hourly_temperature")); + h.conditionCode = hourlyCursor.getInt(hourlyCursor.getColumnIndex("hourly_condition_code")); + h.condition = hourlyCursor.getString(hourlyCursor.getColumnIndex("hourly_condition")); + h.timestamp = hourlyCursor.getLong(hourlyCursor.getColumnIndex("hourly_timestamp")); + h.humidity = hourlyCursor.getFloat(hourlyCursor.getColumnIndex("hourly_humidity")); + h.windSpeed = hourlyCursor.getFloat(hourlyCursor.getColumnIndex("hourly_wind_speed")); + hourly.add(h); + } + mCachedInfo.hourlyForecasts = hourly; + } + } catch (Exception e) { + Log.e(TAG, "queryWeather: hourly", e); + } + } + updateSettings(context); } @@ -382,6 +421,15 @@ public static class WeatherInfo { public String pinWheel; public String iconPack; + public float feelsLike = Float.NaN; + public float pressure = Float.NaN; + public float uvi = Float.NaN; + public float visibility = Float.NaN; + public float dewPoint = Float.NaN; + public long sunrise; + public long sunset; + public List hourlyForecasts; + public String getLastUpdateTime() { return new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date(timeStamp)); } @@ -404,4 +452,18 @@ public String toString() { return "[" + date + " - " + low + "/" + high + " - " + condition + "]"; } } + + public static class HourlyForecast { + public float temperature; + public int conditionCode; + public String condition; + public long timestamp; + public float humidity; + public float windSpeed; + + @Override + public String toString() { + return "[" + new Date(timestamp) + " - " + temperature + " - " + condition + "]"; + } + } } From 9dfbf8984d405966025ce9cac5ab47855491a02e Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:07:42 +0800 Subject: [PATCH 1224/1315] services: Add system server hooks Change-Id: Ie2732937474a9daceddea6d6365ea67c7bab9e41 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- .../server/am/ActivityManagerService.java | 32 +++++++++++++++++++ .../com/android/server/am/ProcessList.java | 4 +++ .../server/am/psc/ProcessRecordInternal.java | 7 ++++ .../com/android/server/wm/ActivityRecord.java | 1 + .../server/wm/ActivityTaskSupervisor.java | 11 +++++++ 5 files changed, 55 insertions(+) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 1631f20cc942..e72d21b6733b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2923,6 +2923,38 @@ final ProcessRecord getProcessRecordLocked(String processName, int uid) { return mProcessList.getProcessRecordLocked(processName, uid); } + public ProcessRecord getProcessRecord(String str) { + ProcessRecord processRecordLocked = null; + synchronized (mProcLock) { + try { + int currentUserId = getCurrentUserId(); + int packageUid = getPackageManagerInternal().getPackageUid(str, 0, currentUserId); + processRecordLocked = getProcessRecordLocked(str, packageUid); + } catch (Exception e) { + } + } + return processRecordLocked; + } + + public ProcessRecord getProcessRecordByPid(int pid) { + ProcessRecord curProc; + synchronized (mPidsSelfLocked) { + curProc = mPidsSelfLocked.get(pid); + } + if (curProc == null) { + Slog.d("getProcessRecordByPid", "pid: " + pid + " is not exist, return!"); + return null; + } + return curProc; + } + + public boolean isPackageTopApp(String str) { + ProcessRecord gameProc = getProcessRecord(str); + return gameProc != null + && gameProc.getThread() != null + && gameProc.getCurrentSchedulingGroup() == ProcessList.SCHED_GROUP_TOP_APP; + } + @GuardedBy(anyOf = {"this", "mProcLock"}) final ProcessMap getProcessNamesLOSP() { return mProcessList.getProcessNamesLOSP(); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index a9a5aec15ab3..3defe011e332 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -4351,6 +4351,10 @@ ArrayList getLruProcessesLOSP() { return mLruProcesses; } + public ArrayList ntGetLruProcesses() { + return this.mLruProcesses; + } + /** * Return the reference to the LRU list, call this function for read/write access */ diff --git a/services/core/java/com/android/server/am/psc/ProcessRecordInternal.java b/services/core/java/com/android/server/am/psc/ProcessRecordInternal.java index c64f0759b4eb..f3ae7c346232 100644 --- a/services/core/java/com/android/server/am/psc/ProcessRecordInternal.java +++ b/services/core/java/com/android/server/am/psc/ProcessRecordInternal.java @@ -1867,6 +1867,13 @@ public void setRenderThreadTid(int renderThreadTid) { mRenderThreadTid = renderThreadTid; } + public String getProcessName() { + return this.processName; + } + + public int getUid() { + return this.uid; + } /** * Lazily initiates and returns the track name for tracing. diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index c7005be33892..22af3b98e4ec 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6274,6 +6274,7 @@ void completeResumeLocked() { // Schedule an idle timeout in case the app doesn't do it for us. mTaskSupervisor.scheduleIdleTimeout(this); + mTaskSupervisor.reportResumedActivityLocked(this); mTaskSupervisor.mStoppingActivities.remove(this); if (getDisplayArea().allResumedActivitiesComplete()) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index d3b83eba4c5a..1fda1d528f8a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2232,6 +2232,17 @@ void checkReadyForSleepLocked(boolean allowDelay) { } } + boolean reportResumedActivityLocked(ActivityRecord r) { + this.mStoppingActivities.remove(r); + Task rootTask = r.getRootTask(); + if (rootTask.getDisplayArea().allResumedActivitiesComplete()) { + this.mRootWindowContainer.ensureActivitiesVisible(); + this.mRootWindowContainer.executeAppTransitionForAllDisplay(); + return true; + } + return false; + } + // Called when WindowManager has finished animating the launchingBehind activity to the back. private void handleLaunchTaskBehindCompleteLocked(ActivityRecord r) { final Task task = r.getTask(); From 91261c2350bc08723f40d97fa16a8503200dfb22 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 3 May 2026 18:18:39 +0000 Subject: [PATCH 1225/1315] SystemUI: Introduce new oem like clock styles Signed-off-by: Ghosuto Signed-off-by: Mrick343 --- packages/SystemUI/res-keyguard/font/Ayame.otf | Bin 0 -> 4472 bytes packages/SystemUI/res-keyguard/font/Bebas.ttf | Bin 0 -> 36796 bytes .../SystemUI/res-keyguard/font/Blackout.ttf | Bin 0 -> 18712 bytes .../res-keyguard/font/Electroharmonix.otf | Bin 0 -> 25980 bytes .../SystemUI/res-keyguard/font/Fontarian.otf | Bin 0 -> 136240 bytes .../SystemUI/res-keyguard/font/Getaway.ttf | Bin 0 -> 43584 bytes .../SystemUI/res-keyguard/font/Gobold.otf | Bin 0 -> 20104 bytes .../SystemUI/res-keyguard/font/GoldenAge.ttf | Bin 0 -> 9808 bytes .../SystemUI/res-keyguard/font/Hollow.otf | Bin 0 -> 31012 bytes .../SystemUI/res-keyguard/font/Hollow2.otf | Bin 0 -> 29676 bytes .../SystemUI/res-keyguard/font/Kenyen.otf | Bin 0 -> 62740 bytes .../SystemUI/res-keyguard/font/LightPixel.ttf | Bin 0 -> 35128 bytes .../SystemUI/res-keyguard/font/Longest.otf | Bin 0 -> 44584 bytes .../SystemUI/res-keyguard/font/Neumatic.otf | Bin 0 -> 56940 bytes .../SystemUI/res-keyguard/font/Quartilla.ttf | Bin 0 -> 33260 bytes .../SystemUI/res-keyguard/font/samsung7.ttf | Bin 0 -> 177936 bytes .../SystemUI/res-keyguard/font/tallclock.ttf | Bin 0 -> 6720 bytes .../layout/keyguard_clock_big1.xml | 49 +++++++++++++ .../layout/keyguard_clock_big2.xml | 36 ++++++++++ .../layout/keyguard_clock_big3.xml | 50 +++++++++++++ .../layout/keyguard_clock_gateway.xml | 45 ++++++++++++ .../layout/keyguard_clock_ios.xml | 18 +++-- .../layout/keyguard_clock_ios10.xml | 67 +++++++++++++++++ .../layout/keyguard_clock_ios2.xml | 46 ++++++++++++ .../layout/keyguard_clock_ios3.xml | 45 ++++++++++++ .../layout/keyguard_clock_ios4.xml | 32 +++++++++ .../layout/keyguard_clock_ios5.xml | 45 ++++++++++++ .../layout/keyguard_clock_ios6.xml | 47 ++++++++++++ .../layout/keyguard_clock_ios7.xml | 68 ++++++++++++++++++ .../layout/keyguard_clock_ios8.xml | 45 ++++++++++++ .../layout/keyguard_clock_ios9.xml | 67 +++++++++++++++++ .../layout/keyguard_clock_miui2.xml | 62 ++++++++++++++++ .../layout/keyguard_clock_pixel.xml | 45 ++++++++++++ .../layout/keyguard_clock_samurai.xml | 45 ++++++++++++ .../layout/keyguard_clock_sweet.xml | 45 ++++++++++++ .../android/systemui/clocks/ClockStyle.java | 17 +++++ 36 files changed, 864 insertions(+), 10 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/Ayame.otf create mode 100644 packages/SystemUI/res-keyguard/font/Bebas.ttf create mode 100644 packages/SystemUI/res-keyguard/font/Blackout.ttf create mode 100644 packages/SystemUI/res-keyguard/font/Electroharmonix.otf create mode 100644 packages/SystemUI/res-keyguard/font/Fontarian.otf create mode 100644 packages/SystemUI/res-keyguard/font/Getaway.ttf create mode 100644 packages/SystemUI/res-keyguard/font/Gobold.otf create mode 100644 packages/SystemUI/res-keyguard/font/GoldenAge.ttf create mode 100644 packages/SystemUI/res-keyguard/font/Hollow.otf create mode 100644 packages/SystemUI/res-keyguard/font/Hollow2.otf create mode 100644 packages/SystemUI/res-keyguard/font/Kenyen.otf create mode 100644 packages/SystemUI/res-keyguard/font/LightPixel.ttf create mode 100644 packages/SystemUI/res-keyguard/font/Longest.otf create mode 100644 packages/SystemUI/res-keyguard/font/Neumatic.otf create mode 100644 packages/SystemUI/res-keyguard/font/Quartilla.ttf create mode 100644 packages/SystemUI/res-keyguard/font/samsung7.ttf create mode 100644 packages/SystemUI/res-keyguard/font/tallclock.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_big1.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_big2.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_big3.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_gateway.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios10.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios2.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios3.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios4.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios5.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios6.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios7.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios8.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios9.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_miui2.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_pixel.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_samurai.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_sweet.xml diff --git a/packages/SystemUI/res-keyguard/font/Ayame.otf b/packages/SystemUI/res-keyguard/font/Ayame.otf new file mode 100644 index 0000000000000000000000000000000000000000..352602c46ac6d54f16dbdf7397122696a9214e7f GIT binary patch literal 4472 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 W0sl82!2fytlsazUyXvFVkLO=;IQUlp literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/Bebas.ttf b/packages/SystemUI/res-keyguard/font/Bebas.ttf new file mode 100644 index 0000000000000000000000000000000000000000..eaff48668af6966334e29b7bdebed5b8316be693 GIT binary patch literal 36796 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 Z1}igqiHDAFh>GDGqLg~S#&6Ei{wIpy6{`RM literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/Blackout.ttf b/packages/SystemUI/res-keyguard/font/Blackout.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ca3cdbeff08a54763331a829a1bd0fdb5e1b817e GIT binary patch literal 18712 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/Electroharmonix.otf b/packages/SystemUI/res-keyguard/font/Electroharmonix.otf new file mode 100644 index 0000000000000000000000000000000000000000..5574edf19b338bd1d0c74d68a3d320b3a9204a30 GIT binary patch literal 25980 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 G?*9M`&x2$D literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/Fontarian.otf b/packages/SystemUI/res-keyguard/font/Fontarian.otf new file mode 100644 index 0000000000000000000000000000000000000000..f1efe5a90a8abe032a0a1369b56015aa43bb0f96 GIT binary patch literal 136240 zcmcG$cYNGrwm&Q-F^Cq*VwPpIU_$Q@>OyEq2uXnS>LgBF#@#da^xj9KJ|j)<#bbNi zOB~xNq(TTap#_#2VB3XdFL(FuRT9DSo-zDj@B7~Set!Q%kED4djh>O7a?baB&v|CW z+O;c?*@zvPiELlEcyYz9D(AZhg58avpZ$8#nx#)82!bN$*X{6$J-z6u#WSv)c`+R8 z;dt)TD_5*>hW@Y+j*|#-{m|2E)-5=*_Md-8W?c3x-1hm3HFwQh%xruG@+;x{b+v8P z?a|wA5D=uSfFLtk8>&_9$ghxV;r{J#{gOsmb=}op{eyx0UtoKn5l;N#_Vd1g`>%)N z>l@qD`kRk@*Nve62IbGXyscW_j+T{*Mwh_u9C>w{EOGuf9ylIHkeOe%D^%+5Z$-lJ z=$F9t3V0K^gdoepSKYPk;`#qTF2LXff_(7YyL*59{Nwi>-yXc+Aa*AlpELda(gnEZ zT=Bb^2y)@2-*C9d3tEOCE2sBJ*{sV^KLW>P@v?j2%Dq2)&Wt1VGcJR- zhGKtOW|ki7|13+Mt*Af`AQdNFKm69Y;*9U$TIrdQnd~_m;G+U=N520aa+e{W{PYa- z;duI6urL29KLa@zng8SYnaErOS_!T}wf)O?%HSKaAJ3nI`~u1Sm_HA>6dC_9e?IaA z@*?yObk=!rw;tZ~hdheh`cr-eG8?u0cz!0b2&I3_&-$0|l)=5nems8;a^sArf6SkU z%$?EpWBz=^KEtt#w+lR{o`&z*PQ{jd2)mQ~9&)k;If zym_}z7yU@-{)I|ewOXO9xM$v-cSD+Y|9$sYJaFF~HBIWDN}qnd>BoDvsk-7xWmCn5 zrsirxwOrk_wy{Z7Q4hsb=&Dr}wWYFTbrl*_lf0qg|6O`Ty;9Luu>{`aj%O7OO|^H{ zD%z06hysx#YNQ%bB2BQDL*uVObkJNfI9q`%fzQ&lQoa|qn~@c8B!ln$aK|mkop5#@ za`*IB%0G#;LaIPkLwOBIBiyN)K9a$w4DQv!z7DR+;Hn0`St{rM{wW?t)=!tBg3?P* zUV+>LPjEM+|Ly!E$g=78(oFx70q&Rw>GtW*AD;S=pMHPg^t)BVGb7`$| z6Y}>X_rX?yJOH0}AT{s|>YwiYxdwl(zn^RG+3DIX)lR9NE07JaZ-yr_Oh20%j@Ck5 zHBHx5{f})@0qsydeWv!O=PK1*1=L&V{pC=D6+iv{|Mu(^NIl%GK-%EDOW>|jo7{ms z3uhYOs21L`^ie!-iWDxlt zy$4-`R-<~>WaA)HcQK;yt7`S%s+{@>3^r<0!?{bb80mQQpQbcLtF2JcpS>eq9A zbM-Y9*Ur7}`WtS%>E>H*z3uip?!4>nd*uFiu0iY2dbAO3Mq5!i+KwtwHL8U!j-x)5N5klG^bd`O_C}e!tx2v? zJz3kNtktyDx61TS{hVF;b9Tkg*>yi>tAEbQe$F=ioK^gs)%=`os#B<2WhzyJvRW$x zg-}ynt5Hv9e*RABw7RLaZu)dvb*)k%FP(xrt7~gza`p5j9@HMO=@m(JCLa;9ZXat9m*o(wQDOYYieq1 z>*{2(hKAPGwl=w3p=fVcD&YszMq`~qQ`0J&e&serU5!%qLmjj>mvr~tKXtwTh7kU5 z@WKCh=MSA61#C4F&`}uxv~xiep9j+P0_c7hpyaCf8<$p*cY&HJsjPda)zkyXgV3FS zi#&`x0{H7ufkx(_A3Bft^(D(29(h{P^cS_ zjW84{ktP~HN+2t@h#*S${}yUpnMzkA@3qR$a~0#$oqhbKSDl0 zGDraV4EY4*k(0>hFoeHA1vG#L(GU!{2pUCWXdF$TNf_oNC}j*(Y7?kiEhuyY(t@<2 zDKw2{&@2)|y3rh(M?291+J$zbJ!lc_Mf=cxbO0ShhtOek1RX`k&>iSFx)Ys1&qpWG zDRdXQ8{LEMMW09aq5A=Cb09_zARi+~kl&*R(L?BAvF1i-ENQYds6PbM_GCPS}auagtAo2?VDW5`qIf-1g z3Yqg9av9DB6B}Pt}`RoUy5Ach1_r-awCr1{2+3h3Ay7R$X%1jJr3kv3b|iG z9{dVJm&^r)q5Yhb| zF+PcyMiA>IhoGQZyiacOZSONZ(t?zyf6O3S{tO zWaw&S_$V^+CNhd4qlb{OUn65Qvf~3}=bw?u|3G$Mi|kP$`&`Ju0P@0n$kC<9t8K^| zY2>Zl(20-}A0hAWLOyr}`Dicl$(_ijIpnh?$mee$zyARF^JB=DS0P{h1Nmzi^36iz zZ}X79Hz5CbA2}63r00--&M#?&nYW@d-$iHr9X-d6Vi%(4y^3D23%#fkz4%=8QaSpI zPPF`G^p}4{uUdl6aiPDFp;s?Pui1iDtU<5cie48*Z+Hy7@eB0kz38nD^!A6*+uuj; znvLFbBYN*+==~J>;3WFnkI;v4bpAf{(OUG;PV~{=qmOMuANv-4d?EUH2m1IC^od*0 zC(`JX8__4j=#yWg3qU;y=z`DDg-&$QwdkUK=%R1X#h0RsUq+vHpi2kPXLxk^A#^2+ zuKFHb{V2L-HoE3_=-T7xrj_UxCA#geXzjab!`EobK~!-es@{wmXQS30)agKR5v6ZK zeIy#_Lc@E}m=;aVL9@@Jg(akb9 zJ5=s=dEK;|_OKqm2Rpi`jCTu!K>KOI9}HrLUoH~_E$ID2 zyRq5dnP-0|6Pr(6Rc_p>QMVHcQkBt8w7;eo&$EdS!|D|u_K6R{8WCq^j>P)AljF=7 z)oqNo1hm4l!nGKGtzWu`m9FNbt10O+uY?iOxvocTb(nOYR9~LzDa8Bfex_hews&o& zH@9yxx2r93qK!~SG(DEAyOZjnd-y^mkInwJ^;FxOvJ1*x&1Q|(uCZ%bZB!ezc6oPD zky4@$I`$cNn}>;>U^bAA#WH+GNK+{%mT*Vy5zEN7%oc7Vw+Y{1-GE7?tlX|O+YF?E zG6qa(Tgsky7F|8=9#0n;B74wNQ|3-cjUS+JqW{Z!XnRj|k`G`E&s%_Ra*J_@#!X;x-K;XIX^Nt0%Ba@am9&b|#mqgX9&$2x6w8We85?1PY|xix!i10T z*%&?Pq8zjhHWxIp12!BAB}gdMOL{0LM!HCc*J^lL`?PAQOXo0JjT)uWDGw+DrVKGa z?PbP%y*1TTwAC;-X1p>$YG{GQpl%r z$?iy>fb|D@{9SB{jWPk6r+qZba5M`$NPbqJeT)xEW@wgX{7EK6`Y_VxVysl@Y3&Sl z17Eh*FH35BwM9=~s6RN6&kx66OdSoq%B7hE9id@k0&IZe1HOna;?MYt7+(yHM-TAD za91Lq$ah8u*nTQ+i>qR)Ks(#UZuf3;Y$L6Nopj+k%I?E4pEpM4;qk&yUYHJ20Xjm3 zN-xRy{9J$&xTr71#eFG09~_VENQ`3Xv7Y>}Fd`H!x%zxPy;O6DTS=MxE}tvpiFmwprU!ui5CtbcCMNkxt4%IZJH}snpg)sjZ=vVe=49%1ycm2W}+nlnbNW z49*f9=_h$IOvc?=3nmT!sT?ZH$8ubl;VG74J&c3GX%EzuemZ$*595WA>7}R3CMg0d zbt2LYzf?+|=?>+FdkBW$C_lx+YewOv<5Y?ks8Z`-0_BGfj$v6Anuvuan*L25n|-I4 zn=>n2ZnryaZmY*iSU4ls5z(d0aa)k~g}E@BhM~xpL?aCk^3?WA0cd%AikTFV$|D@MB+7xY~x^ad*< z^BY2Jh)X65eVFu;x}5H&yXa2(J^Ca1B$IN+^x>ASwyYvy30eY{;G!e7Z#qA8e$@H) zp_dQ5aBTc!0n7Ts5jxFH`X_}+E+5XM6RBjkFhultbB?$%VGQV5J*}j5l$kWqR@RBJ z4#5@igxwiWxBGx?Z^vY9Z+o$d3_$v#cj@P z$!^K7=&c(!?sIqhGpSfA-sKy_$6XVSUA6$p6FiB9y#dk>Bah|jC^lQ@60a#2uMh{t zIbyGPo!I?1vGT8C<=5gCj5#-)iX>wR9u~nL3B>pqHyRjp6;)kre4|-rZ?&|V)oa5>%)ROam=OGF>W z^bx(Fv z*~n0`Nal1oZCFJs$OmD-e~i0C%)Dp+yx z>OpaC=H1@cb{-l(ym!xwJByigG87NSd>L;Sb}B0Ml<%c?x`(Yp))B*~HttF~lg^AI zZ%1aT_~NeJFAu#kbTsuc z3s~hD5MgC&%P9aaAPkL!(QERWT%hzC>+8w7K~;}=z%}L`wD#+IRDF&j-@|vuGo3N4 z5G^tT_IKNMt_{s+?X-=yftUih1dwAjbNc*p_66<*;l;Q(cR;-QdGS}UzA4WBgIJa; z7SlsPD8-XEW(;91qheIJ$_5Q&RhpI9<5Kx5>H1pfzFO&CqjZ;Fx|OR)T{mz=d^^*rPEY3nkr-1>g?v-b??-@)B36EEA>~#KRJHy_=ERt<{&(c zTl`qMs9e2GUf0xM(5lTHPNh>xD}$|ZeW!cC-Ou(#`aq`hEFbEP6l2Ag#Y;xSJ0s#U z3^tdztW8`h7nf}jZ+}*tO))e>)4G1!AU;g>h70*jCJA5^A8>aYvmFDno|=3Wc3iq@ zpY+=SX;Z&c8Im?xrAk<9rTI&wIoNC|CtfgT);s0us3xvY=#rMSGv~@O$xtj5PYOAD z7cuFcutvNgB0#3y5sG7d92+Ln7#*UbbetY%`+WtWH=57Jv$0OHTbDJ&RQ`4>_DmUT zXPvBz({Xkm=_5kEL@3}7`J+NEk_n~)J>ed@%h~0K=%^M?gR!+;iB(m%D0C*P-QmJL zxQD=L92BhE=M0)cwV`EF<$ z?XcKQdc8u^q?MD(q%z^?;m5Gh&TvmS<_Ek0(7})MPQfXdLTZq%ubzJF^zL$w2H-=F zF@tnB*+Y)w2VHR@>WRAcHYICK+sswl6;IigxSz(K0$pw+a2)e`Ne5$rt_oNPP^g{) z3_OFwSDmxY+@zM9jV4z~`I~~46s8!lK?Ff~#$WA| zF^w>G?6d=L3Sblla48GV#1cN*2lyjEV?5w6_?Z3O+&_+$Q*k`v33`I=fKzY@uAnQV z86f%+#dxtdIzhbR+$$fd9NZjT>weZ<>#A|oVvbsaLS+P$OsGQYu&LWQ>Fov>yMyYY z@?-{^-639j>R9<^UrWv~Y8Yciv!j`@$?WTXaXT+=CdKv6WB6`OSACa^saHeVsGW~X z^IejRmY4txueZfzP31K0@!Ny8pe16AK`P;fge7i|>x+a@0FLuD_<9{I4+it9veG0ek@){<+HhTCLitgcl!%?!Q5$zX?-1-UrBU0 zJIvL_YJCk@KN`DQnk!98*TZ&$G_hKmxk8%ptb~-{KSj20sIRK3uT!?*$^@oN*n7FD zz>(w&oghWNIrWCQ{7de`;%kRqJG}3$(a%Re>;6;h%lKFR-{YTnKeT_U|6KP)+s8HU z)x5v<<;8m!?|GnbJ%`;yJ=C^z^^#Q^7S!D%yQlq5(=Dc3JoDJ!veKn#>HJ}-Y(y$M za@WTTKVB%#tQW5#G3ou&e=qlqhV~?0OuU$VvFnAd7yEaOj0_L&jPLfmYB{ideBt=Q z-bd2+rSB7-Bvz6&?n?Vg%(l|7QVprCvZ1cNcDrsP^&GpQvu3Jhs`aqu4ecA2WA5Xg zWAqVzFDCw%R4A8zap<=P>h{&`w@oI7>&FL`KEjmCo|O8bry|5ZB!XFkhCU)&0@89+%%1)&jg@5kepY*1w7~p zc>sIB7Qw+l#M8cbG#ZJ-{V5_!ggky1@8aC72V(&KGZYK5PVn)*00)qk2~!a~?he|0 zM$Y5{tfV#6npfGO$Ng5kdCsi=EU$OBhxJ86k?zk7#WK0hzMlT>-mc!xWGogAhemsc z{R4&peX5z;!b(rGQavN#7;L1};FXq?MjF_aCEK8c`w5Vklt6}vK4;Dl?TEHB9nN+~ zyUHMQV70DocqLg$RvEf1N+-5TeEx?)6R-u%;pssWGbN01OUxeA6}`m> z44NzqnoeJah{2!HQD-0SX*{G?~Y+buy-f~Qg zeOYdN0pBTf@!gR`!UxYuL>wU~g>!O_lKnt|%|j6-Ba@{CFgimd7NS6|IKmdzgd1^_ z&2GRQq@A+SR?zOSl_V3~=>-i42K&s@kBZLnq&Gtrr~;MaO2k0~j6$|Q)SK!a**O-U zuuWJCZNV+9hSGSn_F7xLwHdQD8=6(k2JKeM6YeKm4-mI8w=?%~kND^N*ZbT3n?uc6 zeZRiXI}jO+4EA(SrLl?FFkd9|@C#W!D-=>)J>yg3uk8G2=SL%7^nKa)W#&(T&xOw& z?1$zv)yD-QEEyP(8t@lRNaPNeXnW2 zG>n-?*wM^LcC>GJ=fKXvUB&0SemC}h_IKR-+?$5i>Rzqe|J?WkJ8zCjWftjDSX-q_ zwn!IYu&uvt^?lkln;N&^7SeH13065l?%Bkff8nT_R@1Fc1C!PZG- zg3(YcDkMr)CQ*78zC^-!I4~ud(!%=uU=8qCz#nF!WXcBzVBo|3ULeWr;AK(UkliYsx05{1DQ=MR5lV91Q z^oAAKh853LKdyXS`55;|@1~4A>&f6*t}D_XDhPf2D7(wpTOXIj8eJ_d?X8+-QzK?- zwA5Scalj{Dt4r_IkV;a;YJA#AL#K5#0%khJksQgEC>fxJ7z%rF&8t;!HNMsKzU33Y zcwXe+M?ZP}lhcAHCf>t#1FGuMrR5RK;#d1xd@UR#wgu3GnU?MqqfDzdZ_LJum=8l6^H^iYw=tBMqURDpf0ElYD31LIi$#1 zySYBDKVAUdCY<&q@gyDvnTk!n97Xd$Y{h)hKr|c+N0Z^a&=UaVr|;AUn%tY*l?_iX zeGpqNehcLjoX6(|dFue!09()*a0T3gJwYY}NRd#|7h}Uz01tS0un#d7#4#k$03eTf zuq6G^Z@3tpprY=eC1fRaHkD1;D&N$M&8|IjvG_oF${NzL3bx&&vKTs)b+&cXI(jQt z$FGUBcH=wn@m&ADd;n1PG+V>448}sA1lhxtP!1)~VJc4L=``%)bd&<&5BnGugEM*N zuzScF#;lQMZXFJU4!}Q5>L$xti58~Szb4YqZJaPo2)p|Bh*jdaCIy~*2ro-+GNSQ9Cz~q?> z*UcvVLLd5?c24iACuVt7tNOhp)XrF&PI20)+`tq6HKyRSOUC?%Eqjl^W zjFp-RsmU%ik?Xzd@eQPfa)5E%<`8S-){<=!}^#ysYz;5 znzTA?NSTxEBc74?XneH0Fc$9PyNI+c4Zu5S!+f9z06YnNI1~%U*A0RLDkH+jgg6~{#a)7tQ7{TioqRnu``=DqJg4lL@-P*m z!obUc=y7vi4sbOGFUxsjwzwnd9B4|?0qluYen`|G%aJFVV{ev${2WSiN*Bce&C4wP(xul0B;&0 zhO9+ZCuR#<1S8kxYj>&i9V)AwX=HRXB&v~V;u?Vs)Z}zoUCxxXpQ!sl^{E5<+V++3 zRo9nYA3y)bi5HHIyr2Fob&~(w^O-{|uM=lH^@qiOocB^izl8JyB zi)fiM>$P&}H;FsCH;ir=RgL)|rHlQk?pT4(lX)_YC!nK+-F_TcaWJL0IG+q>uy8t? z=nahphg|&~{T3*7{haXFc->2veUIeV|_0S?j4yJotW6UcSjiXT|5*I$AuUd1e638 zBL!X`0#pjroR0)GLt`8TrWzvv<6aVzJfvJY-3xz`cB}SA&s=>-y`{?`B*lpjwU9Olg z?egx%cSm>iPWDYs^zTnYV)mQ&7=~L5RekHDtMKQDTC=R9QQ6qq-e|x!sMZtflk1b! z{VjVi;s>z=Hr?5Rr_cWLA7@u7iS44cM2gY|+-!%%j2me=Ee|WQij1zy+Gp)!`V#%U z2mAL8>`8Pc3W>rCA0Ll?v>Myh@`~{z@5;porxr@9^1h5O>BCaKj6Wxg1UsETwRJdK+GMJBxxUH1&9co>We5Kf2?i#3 zm?*qKnkkpc)Y7?_yUMc7Pz??T5Go0xWYtGK0lUxaSK(Td(PYxwRrn&?B63ks&1yr2 z1g1~A@=THI4QDcubT}szSaOIN;)nP_VK6v|y(1RO3;`Xl;WR#j)2db1x0xG-CPCF{ zpKu^w{XP08^M~6$*tYAb*sc6i_N9vF z8Xl;9sOk~S+(Bx>hOjYZC>Q_`5I*mxPrv-w*nK4U2Y)^G(%?%&Lwh2x@W)NZWpB#f+Io2H&UHI0quWd?J2q~uUWRR7s(p-|hl%5- zFMyg7_(V7!PQ`P{TnbVq2f8no>>BZpnnui{iq5)F3s56^tKMb>kqopNQR<+ACk(b` z%p3!rJ)#s^>3XWoq-oZ)a9ev;^{jg9X|ZfR*81I@XSXR#B6DVWd304#K0!{9dougR z_wSrMH2T))TbU2I&&dI6s)Orb6?m())uyx=9ov<)t@U-7tWLgNw^>_>Zw#%&-jVi} zFW{f;S^NCD=Nn%#y=D3$_}SnmL!azBaqRWuubtTU>By(UA16M-U%_`-ikcoxN*-*) z0*$^pb~~eFq5QaXUR1gwBwZVp=2FttR_Rw(>8d8FY|UM3@4RoVbe?{-tCFq}ssmdR z8@jNzxH@R|o4~v51n;qnaCuy~)noRwIF+0+VTij5>;PK~jzlM8laY~dFIIxS3>zvW zvmkjA1z(pl=ZM+C0s!NiGLag;CZvvOdi8rudyV_NhXU9kVX8Q|Z}*4Yp9)_DzM%f- z_%l)T#O@zRnfYn zE~QJmQa<1sLXkKh@r9WX6(R+%0EpKIbYID#4K7sP2i63}2ZH_>mt;ZgStC}T8P^j| z+~KkRm>eDmz`)()v=#d88{&$yOECsc?JB?j@VdR43C#pCkSN3pxo8iQqto8FGv*3A z0``Ez=VCqJ00y+f2Sb3nu~;a@brMB)))-Yp6okT{uv@Jbli6%GSs+<0;1#wzac>EU zeEYrVkgCet+uQ6-T$4|ebB+^PCe0`Ll#mQ2;$5L4KfnYDaF@E-l54jFRJ=4M^Q+?a zE?YO-mFS6g7n76x^ZqH@n0$w#yEa-GT}4(Js*N@6H8L3{J#$(rrz0$Q{C#2Y`iJx5 zgV{-DiW$=9Wyv<7jc%jBJ*!f*E846rObe@sDhrsZU>l&vvDtTs@0~K1_v-rey%43c zSe*5QIOCB{Lr#@x$+WWIiqpvzwnnyrYYnz18Zyg=@85s_z6TCIcnEvw@R|ulzkJL& z8Xk#^!n}bR@++BTH9){IINVNvWIP*7Hqap{O#$R%NtpU8VEH+b$Mcl9848Q^FyiO;0Pn? z1EPTxP1XWgaCDkFjS)57?rGAg8?9J_rNz@mNlmV4LyW~v|3dn@e2^LR4~B-L=}vBt z8#EN_I-8OWLEr`TLCHW!W-Cw#P9-qqHVvcjH)YjBR&2;R;O?cwCYRVmh|L^S)TznS z*Oa$qTGOp*342p2dsjj~RHZbXx_(_B*&FZ86ni2=b4(VBcz~LZO!%Tg{CoXqS{cE&6JGU3*iDTBlc8+ARu=vDK?4 zwY)KEh}u%_tP@;g(Y`3KdD%R63z(c6*h1 zon}Mp7DaVibxZBG^>wS8R$zuEYrRwE)p@mEwHH`7S_cNGn}Lujuw2}{!|wnl(#hEw z8{2GA$xWErqHwghTikNCBcutL;!fbBsa!Dci|~-ZSb;tSyutJal7zv)M)iYd9E>0^ zT7ysp{*q&dW-#lwD@r3F3ZnoLAMTzU&mEu-GCOp|+H`BMm1?EhED#FZ($;8^v2{#y zOx}&jyDWqB1o)-&exl0^&{U z#q*`}{)k=wzBF^EbRl0_F@;q2oOJo38|N*!c0omrbeT)KiCiVDj<1QY$*k+#uwO#o zyYHJN-z*Vl%ESvC*qfeX+_BIb;n#964-9Ae(>LD(QX2e6-^QYo{)*@VEIo+*+f?;8glr*k*a-bM?#X=eOT(zTJFl@aBF@ znmIoI^)0{O@`d3qk-tU$zT>MmK0o&PpI;F#82zm4Q_sisy_Rl$m$INr%7ZPDwd6X} zI&3<=iy)nZ9k+NbxXn}216~gUDeTEW8-t`Xal6wDzD@G2xj1}(gqGqD}G%Au0({&uc(?YB2cmwtPPN9WeL^tc_^PM6#3 z@w&Yju!9h1;`BN_B?ScZr_bY=jGEj*R!d zN+0*V$-YVNpm%`z?JL>l4A#pIP{a5T-f51u15e{%?LIwi^q4$mt3!utyauPy>Tr7O zl$C}-#Jeya!XE;7uW`UN#Dc33IGY5Wq!Y{#J;V-jV=TZvA;>~g`l8?w<^@cE#wNo= z1P?j|V+2@77RDj?jTKQt&eOYHyrZVH-U&RmpY+#@bL+&pwc@o{i#W4UMC(NaoYN2x z36WSpb>m^8#D?U3*>ILEIdnM^9K*nv_&uD5#X3sfdY_6@aw^uym>5&Zze-rZvut-y zD@ZG2#%llhRk@Xez@jiXfK$G#FUzO+6pVHtawEf$;plMsK>9%DK<;SgONEz$*&xIc zVw|4?FyRyYyw3+lw@-kmG@yOJI_Et;sB+H5x*2F(n(|pn3Ck|bF3c<}+?$`5o98oe zM%KXU38UNWvN>Hgz-u-FEF16vJ4?}oVBTX^+6r~tOf}QZOg&xC)H4=H))J!(m%)j` zxL8cwK4;d^v&xkmm?LdIyO3Ml4FOIw%1P)2etTGzGYnb>Z9|Sxd=&5V<|*+4yI98l zBk_;K-=_Zk#D9K%RGbwT=R`zI_vel8V4L24^u_CTT6h4jJ_WB0S>l#B+nbuo?Moht z9*G^HUa%bM_;ACEj}1RQ^kiZYjxF*&VScm&P%_~p+wBT>yVppXp({IBt6&MYX4)CG zUahya(RB<_3f#kJ4ciW+dNbb^R0I`KeaaQ~hFGBV0zyy-2NRKK6uciPh-i%3L#qQf zV#1An={#2Y6|6L{hZUrR;7foeQaX&l1vngThZjP1y*Mm{o3>-(>!(uX(PFVMnVrh+ zkG$wV;ydguXlFEYmF2E_bftFbiYy83iM64MIR% z%sWE9M7%`5%)G+B>f0&gFmdLoE6Pi8Y$WA!IP4a{=wL&8EI1752;nEgR02{GxVJQn z))*ZG=FX1+@eR~B@jCrF_bNS_&JFV)1`j%W+l%ed22T~f)wD&qxnr}urlkoJul#P^ zSq1ms>gUWl1K_?ku1jlE)8IZC49EE>_>h7C?gIo5@ZRSIZ^4h?p8EmZL)ZzIl5vLtYhCM^t__uJRfkc9T`*loQR~R+rq~ zVQW|G8y$`I_1uEw9bHRbsQUt|`@-}ef_SbVVqN0QssEZ3k(W-s``WINU7dR(dm>|G zud&w{X>x5M%wD6{V6DUJxn_)Q>d_y-V@x(*oG4EA?(aR?cQpTE@?i3yZ`7XC7qv<7 z75Y1Z|8NLRoNjorguqrJp%8T+z(@S3QDo(t3T%7Sc zapniV|Lbw_qGRGkW8!5&@t2ri{8gKH)lzZJGI7qmf0JguBh7q6x@b%)4@;MYrAu(> z5`BsEC1?W)@ocyWw?dD?POXtT%eh{m5Y0xiosj|NP3mRs;hMv>Q=0}?eI)&6M7q)~ zU5YPK&EJ0SwtLq-x@_??Pc5llV#XS9Rlt_B=iS|8zw2%M?}T_2DgK%kf5nRBtaye0 z-vTdS;?eJF&n^k}Zne^}9fQCEGiUHCXt}o)Z_~FoYMZpn9S<;1`!*F?b~W#^yvPAq%^n*& zK6?D{OYiQ*COU_sy%05@^e6b;m+S# zj&7XTI0=M0aUXeq%Y(}wT>k8BHPRL4_2iaNO}Hjk-B-Ig;t@`u$r^J@9Yk>qBK=u0e2|gLT2Q2AlzB2jD>fs3CAOm4f8O zYiY5fv@eCH1&Bee7|daTd?CIg01hj`VSR8f-U~|kt+9(T+Nt=9S zr8syuxc)jgW6%(C6v%PREo#fWJGkCZHkj!OPk^Us_?%I?%M-Cj9brRI?X&ZMGJ^V; zCFMvtlKyxmo)3-jhxkL@{g%C!J@T$94pT#%h#JE0OL2zKx*)N(dN&d4i1p-Vx*AYr z3Hnw0tC&sHCaTunrp4M-#%6boyOxkKO^kFGSBhM{%O~B+O1Htv+)qJQf(~DrZQ_Iw zc94G%az*d zObK(?ELbrXhsX%RCj^2Jj|G#ea)E5Hi|O?YySg;-hIoU0Tiu4bjSsDtE?bL&io~j22$A7Q6+vlP~a{LLTNMrTH}f_uQwZ*VnzaZv2V(ZHe0~w^ZFw zb;HB=O4tHS!j?)iRCANpCGURz(T^Yfc=dlaisg;svz{V?HTy&!Gy~a;ce|+o6{_!K3*E6qkhs}E|yXE<9R4dWp(poiEtH#>`3ag|U zDV^8gGP%qKkKCvAs}jnhvZx&}jv2>X!`vVy^5R`*7c-Ej@w2OG#Cy&h87}Wn@dZPl zC*&4fez)HZW-_p>elTbSPs9tME@T%o$V@Vy(Bh?xcpfcY#)`kj7;z3Op6~m{_YMDo z5cPp$Tkr*iP#_cv26?cnnW!gbNhq_m@wLHqfhvD9gRSCf94d`c>riqEu05*lG(-3d z#0O>VF-O25ID7`GgEG(tsuWPHqxBdi1Ju_<*U;6}c51b=-eAz{>)q?vW~M}(R)ddV z8w_)G!rEgkx`(J87`1~Q<%SuEPI8xjmwyNb5C&`nq?|t!NQP2jAw_hN zNiUfFkOCgT9dL&{Q83^;$uVM#+CffW)CAp6=Su7W&_2T~gzXB2e0sg+>XJNBt4^HwTzr@fY^s7x|wdF)s)TK=vI`R@m>?m95VCzm^Pv7 zwNFskPHKvu3h#;x#R>^bT0iHUg=bY;O0Ru8RlYh=m(pi+X?F@DHWMK*VSpwFzJ1z+ z!NETPCJeBPz%BkC|AY_%|3r$3c_P-Zl{4W6?~ncox5I4*hRTJz@gGf?Z})c3V%mcG zv?Jxrdy3=^?>IHiCYe0f?C%Q z2dI}Rv6>UB17fvLEaSl3hV^;hP#_z~1Y)6(&ku>md^|+81o>WbXG6L^wJp36CTCW8 zWM-M!(81OJ(33#4mjaEZdlCjbSS9-qvxZ&9J;qB9_@pN}=~-BD=~kO`KB4nkN-Eav z0b9J}wSvy%F?p4kn4WWv?(D+Z*;(<#DQ`JFWXUVS@-T$^7~6EMO-+@}m8wc}rK!^2 zRL})I0irbhNdVh8#74-d7mMLZOIG!}`#;|Jd*c_zPa+@eI6k)TVEMejNPR z{~rA|6QiR{gcGkI#Vg$66&U_sGn%BaO1#Bn?y$5|s+gX41>Iq2DF|csLKKJB?~U8SYVfQ#`Wo4Kww_ji$Y2(* zPqGV`N@f#N$!?-n5DSTgu0@u`mc{CIEj8P=Y=K!Sn_@M^mdTb$+eBa|=8yYhLRbj% zF@GF8_1K(Q{Mj7_y>reSKFdN~cWU{ZvIFIvU3`zdXzOiCuJ%Ki05E1vY$GdUWK0{W z@#tJ8o6TT1;yOmdnEf`vfdw2v2u+5`f_8{cvofV%V=xgJZ(RCHkUk@&_n61=^>&D# zRH^E99Tua`-Hv%Rya9M7bIO_V=7=F`H@BPHcZNk#Vlqq3~S;G#& z^J3M$>Z!V^+TNzPf`urK>6k1R?s6L423+gW`L$6U22LPbk?bV_sQF4T*-sB~W5Q&3 zGCY*%PKSa#1l}@HDoRB0oFik3V@8<&qfB&P#G>Pijy>__qi@y@P(wS0 z#M$EI;(6kg;yL1PO5t1FbQ~u5PGhM=G68Ekl=p`rP(BDT=1Df|&+_RAoKE?(c&94~ zHcz%OBTLsMvD&0Oq7zo7wt5>o8ao_~(P{=N1tyrlbRovUI0>8EjN7$}rfrQ?wUt%t zv8weoTkGp(GL->pE#S_1NAWQd^aH55E?}>xL(C{}h4>0wA(V@!v1k+`Py=B;9Dw=p z(yijwv)Y$WJ^9btv)h%QKQ&uCQO>Q%HH=$!SdQW!`}Z(|KuYKMXn+s#L6~g}ZXYhf zNBKxJl8sDe$LUcWHm1+CvrUw?1R|Ikvcc2lk(*T=My0XFv5Htmtf1CWE9fVQ`@Of| zPx0H6*!E;cw`0sP>fOT}0mlpU8d97~ir1221t~TYVk<7nsXlMFJMYOm<2C_|PS(p} zIxwK@5nI%oVSD_;na+{eNNh?tz<`sBIz*JBX?c2xD!TLLoF%M)8JA>-Q*Q>yQ0lH0 zugwe8yT|1qthj-c(>0uQ6(e27NaxW~m0zk3N$@`di{_M-IOnryRh&L|&MDp5ZN^iy zsMu02bJlt5I2qTL(T+H?-YlC3K-2|swX#U+$@FDqVy zv0_E|51Ac(yPxkq#=pV8iN6hDB!~6mx^eY@E^p}3bXoJ>PME1M(H$?v;fAmsUE^I7 z`JJ6p$z9tIrjR%2NpXoVNInQng*n+F9R4N*Jbu&jnM#f)n9o#-p7#fQ zAr?di74^pPu-$L+o7@JyPOp<0RvTd^&6!ibnRCu_*-2f{m&1ITN6>jtx%?733S(?iM zh5^jsv3eRK9YsSFu;vJb4b!7A<8lY+ye$0f1ccwyMW)|37#IrXGQnPd(E|Z3hL|>_ z7Mg>as5Ob%lAdmA6c?|8*-eai8GW37n?6zU^MSiP!KBzUn--$sQgk>Kb;cY~TiA?6 z%pt2_XJ7&bLyDJ5%f$<%HR3F3BSeM6q!frQ(?(Sh6|bW$q}6N3UAWWZ_P8-{zk;6t zx6vj}<2Q!Q347e0bS3eEXPB6xcF|MJ6gTA?@^$))LOL2uVZjue!qa%#8MB07=BnSu zg9A&j^LD`nPDmm|93jA4`Cp`XIVs*kiZ|2ZH8k+YjQC6T&)gw4&&NYTNQl7X2VgBB z)8{rPXmwT+OQN z9fmGNPp7=o)t!=2O>rPH1R^m$P59%!3|Vj$oh5t8UUuePX-wYW z_9Mi9`RmKFv%|-po_TxrouPNL@8y0Gdft5yauUh77(}V)Z{<4CoHpRRtLX>WRk4Q( z1bSvL=NjF*?Y7$FqPC zXIBs2m%TH4XKW?EmJt8@qGNeCa^}AtUEXEbNg5B(j?UUm&DUe1ZPp=0tGCz(GV@{12+2JCKhgEU~&x$LCX3#|!b z4Ok+Yw58-heMO-#@K5jyVPYY4AOU<(EoXAN4znEx;R zTG9#NhD~F+DS2zuknboepsiN>?PNX=alCZeTIF;e=<0uXM=dAL2Oyx>qF@J=4`?StRKazELeu9` z+wR=4X3dVD!dcx;^RKy%D`)ZQ>m%SRC%aU zs7PMuSR@`v1d=S#w1}oFT<8qtv-r$d6ig69*)do0SS8CAR0dYGj$9#I%!%>?t3v{h zf5~|Q10C2*z6cc|;oj9}H4zM2S&d6?+`3D>KJ+lulREHaTuE=5B~nZhgoU&tYsmw@ zkufJNd2LwZ*0|Mntx3P>u`Qk3eA}qq*~Y2;QyoY8&JcZP3}>8Y48qq#!qt zr-uVQJ@^0991FK!e5@`Dca?OL?n!ct8iuk-@+wNwxF9o2&G82#^O2!)dOB3|6ii7| zQWsZ6iKgO?r#F7E@q^9(zWu|tb3I2@6MZwD!4x*^6Y z>76FL*!-Esn||}eYj-?-$H^a!To)CuO_$YHoFiU+|0nNccV*S|K^s zrOq(b=56?G-8bHL zsO_NQoaJrHo59!0FPC3F@XWb|a|d4<{CVW((Vsb9?JYUVu8Nx|lURiK6rW}#l@WC8 z)`WpKxD9Tj%V;1ie#L7okwXC+Swm-_R&9BXGC~d8_>=)fM0x! z7uVp$nz0pv702#$T3k9i@J)KN&ZFg2;l8xKYA-p)+=uDI)M4&m_)usho=b-#p+q1} z01=2P&?i0o;p;|q7;@r71n0b`YG6xAL}#CZxWg#0du zgO4VMV#DE)@Gv*#o$!qt2D)>^hU$~a$2qYlD)z8qk83ApH)^4E82bD;2#Zw6n2D30HcB%RRyu z9KuGAu$C28GV}bQ(2?+w)O>zs=u~(djrf#2x|3f*NU`04{oEFY*uw61H5;3a=I+?; zfGKRl{Dh2q6J8V`9{i0ATLXr~YTI?K;x}8wZ|)Ya+9X~@Y!*3vO|+>w;Wx5l=`HuIaPja0K$tsI^6q_RBXiEj10oRh=*@e-GM0J67I>?1E9U!8piQ;I4 z1ud9y8=V%T)8(~$Z9bdy0Ybk+a;$-ZDfyIyw-;A8Tz;L*5HR?4OXLeL1C_tOo7u>2 zWHwUpwR?3=z0+>;sTo7SBn?MX&WxAfC20^Eb_v*6l32^S>}ZI zyyLv%uo0AC@BukBuX1Yj29Klx^I7apC?32H+Q=)zO^MyvZA3&7QiZe!#k=C-{TlH> zyw-`g{80SPhPxl#aj))P{hjGsj$zn^xe25m0qmtQl!S^*?=nI`VM52Z-FUbBA>Wg= zCu`g0_n&J#*Zs2QP0gFOpHZ(+!nIJF7KASqg)a>VUmq9#nc$LeQ6z(9fQ}@<0xWxy z#gW98!xM5xwG-?}zE&6*N=#B^YQ!_^B4)`M$FyU{KIq8V2MtLzrp5hMt-@mQn7w9< ziu7)S-ATe-t;L}xm4r)4YB84~0ebK>i(ly$zp51fQ7OtO@f$_)>)6b-C7Re__Kg$NmV~2=u8rseW(;broF$-$8Z%C8IOs~K78@%SM#CeK5xx$#$vP7*IVWcTgGm!s zMBxL8^}`*nM)SA5Y8>TexS3piv^bEe1d8ATXUMpV2mtTP(OC84flL63m1HKFEhYz} zW3f@PuCA-%`&^GZAK!Z$B-q5_C7<24%op_~i(48l|EA0o>j-ZNZV7JYHZz-Iq&7xF`D z5R~RrVC_Yd;Y2VJ2yr~|k?_EZcp;fjWwA|-X8ajHrr*8eo5nVcZG^|MCnv{U16>x6 z8W7`o@S1(nz7KL3NtsD{Jn$Uj@RGQnEsQE=C|ZlQBAJ6i6g~!Knm))LWr<_{gQ4lz zOstp-)ckd_V2B%W87YIGn*fcW{(+9e?T1>PZaUX^w)ITsNuu+(YSL6P6&yKV#+wP{ z)1}`E|N1N8JHHgZm#*c9Dx=l0Vy%!1htTrIIv3;uEP+4p7aVxfMj)+#+bK>1C+W;! zB*Ed0^c!eNFY039WMw;OVjsPa-S0O539fVN+@Pfzyhhd{p-rNWq$_KQpbbM|p5ipx zwR*XtiBL2tn){m-ExKN-LDQwv8QgleHmjWOINfo!^SouiJYcC>DozM}yh$d`g$X_q zh=d}MSR@lE1k>)c9)01i21)j^jJ4{gziPPRjpe;H-dmJ6T>cGNOwYkl=hm7m3Q__z zdud#~7%N-Bx%0S;t>jkjQU99Qn*5sFn%ef!c4DG^!aN?B2#n+gM~6nsbGc)sSCcPu zFY_-mC#Z1-id1jVp0pM4He&0-mkF*^O3eje_eA?;jGyA>m^o&Q957dmF+FNRy-Q~U ziO{8^bwLrEjrFX!75Jqsr1+3kyu~Qq^vHL_uWS;(vQfN^7I&P`1q>|EcCZ9kNQ2V= zCJ1ifgsG;R@*Pc{C6doo4^7mInR2K?SLwVt)mPd**}Y&oYQ$VU`BH9SZ1CvN(ah=K z^MUjJ^S-kLJ%r&|jVXBJZfN*`DwhOQc){`m-%sP-9r8`4ji#qUj}1ONzU6q!%S6k| zil3R^vApemox8x!Fm)d$O`as$`60}%0=UH>PVOl=2IFJ#(XryG5K-f%*dsU&c@Mhg zUBl+Iin#T6%Ufj%moj%uMQBc!c)b20zE+$rlNKjz)8DZZ?%}= z%j;)aLqE#?D0~yj1j(33Sb+YvGG@lkxq@VX#wwASJN+NO7ry^{;Rk;duK!rL5wF+( zGIu&xN!2AkI+TjT1UanCH-~?e5Wj>8yjOaC*?kiUOf4w=I!5X5;dlAvMsusF&9k4| z#qSQa#`=iT`kD$n;COAQd?=ENN3b3R(p-9}59nuK55LB}MoBpm!%KZZP=F*jScb|_ z62&EGXRL0E89zek86m^?7^3aw6GL)x&7# z9cAmG|H}V5{0rZ|QNMQlr?KiSc(a}gS@3099MOC(pR0`24-u8=%5-U(5dX7bMS6L! zgoheFIVAO2v(icWf7;13{H`c`5cnV!JwYJ8B>UI@r=1L;&zg|>tbfx^y7gErU{{s; ztk{b>V6^?8KI^A}#qW#v%Epu#In_?J>v~&t{kA?=uW!3^2frhAf2H}D?``Gcq-}AM zTbu+&u*8*vbGgx>GBH%1h#c@PxEAb(%>{SPp0*74CAvKw-VRNFtD)Pbaoez+ld4NE zg<8r%AZfq^WvZm+ce}(Muh>J0fCTjLqCAckMLVb9OjsoxXdZj<<4G0=D(xjw-kh@p z8iDhZe-<8sS0D1nmuGflasxH_hZsReLoh3|@U0ywm%#7obJmz@1JAQ`AsYuhIF z&bLh|hP@ETR?@K{-@NOL@oCjba3li9@ose}jdFjxzdheOsjJxrT!X%&&INuU^+sj- z825H%aWb|zNlEV(UGkO5Im2jAndm9EMfQ1jxOUignssiy-DK(Rvv%_xfsSJTq+!@r zbR#o@3DN+!Q5-@P2nfUQZb?XJh=9bNfY{?->f?AsxgGCZJEicMFr;w6sqIBQ4{L*s zGIQmN_Icj@83PxltA1N=BQ78}mg z@*`tUAAM`#&Er2m@dk0?jk%-s>eS$5WWq6FA65^v!}oq8!~;M2SiB{zOe*62pPRf> zZN5%zkG7}V(HJKB!>W|Nxbf`P=XbuO8<38kS?RDRU19Lj7?;aof>-<=;j7}?vfX@_ z1UsJePvs}`lLv|y_@DVdW`%!cg{v9q)j$bX(|@L3^S$6cq^-7A;rvy%5nvx{?)8e7 zc*Xl@(XjN|;1{nC;IteC)cbOo>|oE4wj+%TjkEhk_a^lmwiqsx$)WHmsqSb?>E5HO z&OCNOQ?Xb4Rjv}t<)cI)22qa|7>C;8U58{&Y>J$|9%m0JC*|&5x{uos*;nbA>76wn zqn@SSPropBWbE*v(P#3{YB^XM4@Ss!tEp}U}~I0n51E*ne7GU+G~R$L+yN=?0R;`j^GCk9U?p7lKA zJ*u4AM~v++Kb3eOzA|(-eLHhIb+>P&?+I!f)5tdR{b73q@<6CJuWgJ+ZIU`6%&K!oJl6#(oOeq9v^veH|{qot9$BGAw^Zqeo#aLA*dRY~t_m~}K z=Mrw*?s4MV2bq`RfB=SBi?Bp>oC*ag%y)jGDk};=E8H;qJ>~Uudq7SK! z@nhkdh7}uRYobkAYt0J7WdX-vb}aHi;o0DrW6YV;`Fpt@Ux%~V)#hnsTG>0YeRd=y|aC@<^}4Maq(O9;VpCZqO0`TiT8-A3BGf!dbJ)OvaD$&zirraqX(E|IyU;PUv$nIg zCBEKAJmuS@-O{qPrE#77u8tdQ*G0b@y>{wH7k)akcTPTUnm5mpbL^~tk{<~IlES7* zB4^LiQe!lh3Y3C_Ox-i;9&rxA7dX?GX?L}g%0BB3igY3#!%XQJHBW3zG>xe6M72H3 zUSKaoPvs9K>*dmDVkBO-4z@u0H@~a~o8Hf!T;@}C_2Mg^a5C;i?iK&*&;%~f6%RDO z9u|b$pgz=>G!?u99z;YT4X9j-)j^L*`zjuy45d}oU2$jO2`Q0jW3(^QAM9q^*>=dE zx?tS}Ri6`|;>0bSxYI8_32_A{t|HJ0z@{^Z?WUx9a@oudjTtZ(x7;nK`q({zo~#YP zy)?v;@eqJ66GULrKf@fL571*&jZQ#wgV`DU_;w%gmXzDlVud=;(xGovH*L9PW+*b(=bIj>Cb&nRX?C;0^h3&k50n6;~|ti9EY#ct+OTxV>+k@k!H@ zk+oyn>TQ#NavpRX_dmm*OB^ju6ebD-xoj*JT2g>9L3fZ?^}Oz_;a}&D)#v6%4~`zq zoeCV_4*3@BM;x=Jam|<}+sAgXeLlPu9)nG5P!l$@m6QTgAlgf6D?OR!2slM-2iZw# z%^;2_IYZPML8Bw(Nqds$;1i(j;25{%)xqvym%Xo7Wpi1b7DvBH34N(bXI6WZZdJT* zpeJjplA|m!3NO)6VlbJ`Kwmcsv{AB@C~+0C?8+LVJ-iu`g(kL{X{2!YdOSvg)PtD- z34}RdjvI28qN57S7Eg1i&4N%BvZ(9u43gm0gm_LowXEl1@#`0_mNgA1%8r6Pk4H^B z97i_=l3QShJRvV)hHyX_MoEk!5K-aP2?Yok(_mRidZR8VP7v1Puz1|=B~L3Xt@sno z6Z{CUoM6ckD~p>X*n%B#1+G4_&!hI~jX!k$04{mBlfvEz^rmqS1Zj-0C8q$c1>~vj=YA*X+jE5 zFRBbwcVxS>$+pk7*VwM@GaDgtXAP_+X3L=~m-1zN;Gz411XvXe@OW;t?lgpWcf%*k zd+Q26Zn)z9<=viLDc<<;r+<~Hdvta~7|AR-a>zZzj)cckPiIf&PNt`dm7(Zh6q{^w z$sAF7J5jIWkw2k~Wu`KbA5Kr^hBC!WH4if{ou@J`X=W6X$T}UKX6HWdk^q*Vj8cA3_Tk*Eh419A zNsE`p#4q8+h?hCOZU1)vE&EpOTfJuA&E{Lo;+HGp_s>hc5;ucrF%K;FU{^}xLTM49 z3E{ve#IjbG*-t-N)@RL?;(8(6up%d;Y?zj>Wgcf%vv;%i_#a}|;iO4B+E(5miT+Eu zi77Crz$6?SpB$Pg5$Fnpr(G}!ca3Zqd$f38=+^M9^exVtT{qi*YI%T|>RgdD29WUN zGuo|sM~|h`)ke3`9c&l3FW6tQPur)H@4= ztHROcXmm6=I(+?sj+nma8zsCW$$@$)6aarLLuY9)&?tcEskB|V-YfjjFI>+G-|-9A z;3Xlg@F9t?!UR3$pXZMA$0LW5GpU(!euS?2hR9LdsI6j5X(DD06BdufX)`$`gf{6z zj2sRS8m|5HA7#7#L;WhZ;9p1$*OCLtDpR%Q?I}yd6vlev2jq!CB9JssZH^qH`MFgdWoXi0?U&>gg7#Fb#z9%d^iPdf*lgXy8^I)SHYkdJavHU(vmaHCte z!6V#&CwmC8=zs^nvZevkaBMOP>imJ|smOwF#!SqZYr2Fgp&~VXdWhsYyia7h&kbI=}lXPp!DRCFdX8=Fi_VAzaHBzw488xD;G2V5mh zNfVUYTL>hA*kKUH3{N@(0eAym*m2w@$vy`bvae=djs6?=8}7IChn`=0f9ZMEbBe%K zpQ7WwS;wFuZAk0l>X0U+k0D4Y<*4~4nW1PABbsC>TxQ4o2dUE@;*{rrbx>c@`ZY+D za8V}cXq5h5DSd+P3H0%OoEAm7oWo+C5dSVL{sRwf1t(r*5x*iAFY6Xpth?>UkMHc> zVcF{6#_g``KZwLI?6r+0Q%(w_!SvrwEdI1%#f!^&{;m|S63h)NcFJ7e^@*4I#jmj9 z-*e*M1;xwR9#+Zf!P?MBeB?+xU5n*1mBJ7rPBN)Ns*K#51i>e{6y{YKz8V^hPnSw# z!ST?LrP`V6O?M<(;>}#EyWO^@-_n?J1W49LOZUGA%_=Xow1LI%9@Q>WJE?N1I8DM3 z*O!%J@`O$5DkQB5Pl6+$A;8ECY62Mj;L~L@)Qr8ViFQOgy*LTQ0K)VQ znhKPdS2uiOk+mJvPGDbH3*?X`6HRhygU=L7C%;qi*N)WFBI?? z&WM#Y_(9S$+g*St+Hmp1!~>Oy15aI-Z%JDsG<|YznaiP1FA7J0EgM7QtWB}8^GVyI zk-Z^R(pRJkfl7QJ1`WQyO#PY?zV8wC_=HD&!hHy9@(H)nzh@_quZKZ?5T(2X4-Ar$ zX)Q{p`2v@TB$M%EJQFJTUk*KIJl>SDg>8rlW-tiF4~wQ9)Iu6Y!)gNJDpp+W5m)<` z3|W5hdu$Wkhyxua9A>|nw-P|R*rSMzG&Ed(IhF`Fhpj=#6xPS|QGHy8;kYSoiK!~i zflxhE&lU#~*#=iYS> ztR#drjk3PumTAnV2Ev&X+9S|@17;EimJ-#I1Y`YQu@LjWVj*wLm4to=_%nmUYIH$t zfgQ%@QlTZW5VAM^CKh_H;j&-;MK)xPU_5HqDGW`fCeu!G1JlIzM)X;Imab+-F#d$p zC5rTrND$5F!`4>_>nqk*%&%yK%QS~HCXK0QUHiA?_v!9-+)0@MgN4&AuoO>|uw|f4 z=XEwcVbvL}8nV;5-?QDfo!TGhh_|QpHENQY$PLaE($OHmKfW3A076j@IwqV|!gAPJ zmPA~pZtXhdqsGS_>m3`&$2_aOZ8~+kL&}1{92?nI(lklWs2HN1Rm6=5>F}090A3U_ zqp?glHqM>r&(W`W-u1ledewOr$CwMce?!VyB3wmQLgo}Q$x>)MG|o>jC8mg;$q7?L z-nkEb46|RwO4Q4KP6g7W6q^gfHI_NJxFeBFCSR$*Qkk4$rs#3=u)L_usADQrA2vKZ zToEP~BI4mxHa9f++>s;CPQE?+n*GA#BOT@aneFNA(Vg7x;2qq(>`G>kD_yJeutTamYFB7zmdurGfMa9K(~rNp8|#fcg~<8cZ~$a}0wffkGaI z?NR|pchh~W!jI5_aCf{vX5fJf_ImBCfmk$_Wb8`UL*_?KYkHp8w`uMDtJ?1h+!a_g z_W1LUKi~46^&{u|fe&)O%>DcL+b3Tw)$7&C5;0Yr^iLY$PNfobo~gO(_F>0}a=h)i zhbQk!{wRP7&aZ@R6i;YE{KIGOEo+4<{C&fUo0oZA7VkZCaZJ{oHk7DA-%z4DP#Dfl zg&}39rtNio!JKwTcoihxVpAJ~;2T>{e~2aikbm3w+}5*Or&gzbl>U+ZhL#_+e1G+A z*FM;1=rMO&yKQ~&YO75)E#1%TWjYuofd@JCbc~)>(S1}O)#BShsTd=;AFv9-*6D)- zn<@B*J+la;3fX8KLF-AQ-PGH)uYbLEy>^>@AGM0zlQE7O$6|HF*(Z|_5C!6Fo*G2A zDojTRpYRi}aJ5(Xl1KQOPq>m2zT^|WjCrA7xQWft2?>hq4%&G_coT@O3quFkDTM0A=(JApFG}ack zCX7*SSQ}BWJ??gwLSt?>wa52OZJXNu&Q{?Yo6(TmbW!)|VOhZ%H!)C1IZPUh!qjTs zY29gQcK7)AhWg6RSbj=##{?3QVb-8uze zMVnh}c8P1KK3dCJ2)`AxeS{)P9a{>0FZq@d@g*>c0gW|mpg+U_Fjj^ zBJDUGMz7IhM1}04pda=6tr25L6CkufrKi{2sWHfXO;l65b3&1`l_U_Qy=s@z0E@1y zCyOn>Qe$_!@H^pBS=yU`{2c>IJ6a1Mc{{8QB(GS#a%VHS!@I-NNOiIMg36q0*fo+K zz(XR$;SB}S8pN2KL)c^!9<{)}G3Ptroo5oL(W07~7nu;LS}ifj6zBp>8$`*C`GhTF z4O>_17T$V^mgByr7#L0q{JqZge-> z)!ivq@3K5@d7NBJKSofG`FHT`pZ5sR;&WmKg+{qVUz6DV2up@Wn{ZrwA6GNUS;IEO zLmOiHI5uNRL&B7>#nn~UU}Pv#&ld(`)_w?ko0@lNZ-YsZKLsa3#0^XU_BLovNqZYF z7HS;m-6MdO0I>mDo|GeO1-INlTFf@=Z9LN61|MLkVC#fS(EEA!enL2o48#MrdI0xf z(OC}hCcg(MC zJ=Qw3E4TqGIrKDsNL!J|k;Zp1rWjXDD-Wp0^>uy4`1<slZZH5!60jM#&h1uB0pBOK};3%LY=R zL^PL#J}H+g#LK}_pzJECzqu{7Jh~7`HUe-sXaB0J3 z^UJ)leR`E}`zIP1bCjI}Rbzq~XUCa2c7b^YcWo3P`Vbkg@itx;><>y$DKFcD@L6Y` zaN!n9^tnc^m+OSjT;X% zji`G`3~UZ+Fi*jc<7L~Erebg?FjUBmMWht;0JLh!Kopp@K!F)1-fj4#u&kqWQQxp) znTP2Q3l`Af{d{4s_FG7;f#DH$xZcfp8U%hf*az;-B&_c%F4U=QwDrtJ21x+TBNZ`mB1b4fa|e ziS>r0(4X;2?HNkiqxSiqU^me_scr0o{_9y)z!-57VHccD-k1-y6Hp&vOHj-A+tq#A zex2OdY3Z~ook01KD!(eKjG2ngVZuEb8YtvLkzfEe6r@!wJ?d;vxIiW z@H4Y;iAngPL->N{W6#IjOVN?!NF_Z6k9yHPjM96^S#zemF&O-cu9%HA5pZH#?T)P- z+FPRHcL`y`XUCQn1N23JYSu?|P{Qb9x|lI$iDCd-3k(P9nNlrPisbznH`>VLQeOno zFBG~MQB-UQ13+6Oo&_ImAXp`f+L8`y`cC`KrnRf@1)i+ovf(9FqduqmA5~*2kdP9q zs0dg?lB)54-Q!Zo#Q(U*kyz!9S>jgCyrgObtVW_ie(oNJ*$xHC(whHeVd`R|tng6r zsp8H!REdd>um*NY>^MV_aHevyu#ldQ4JF3XQ{gP1g3?ejt@8xBPaG1Pfp3%!1Z*JJ z1{lM1u#`yW!?{R>ACM+7zF~6MU3KPd#FzrTt{$aI*Jjys-bA&l-FF4D<-|}F|Aer40yG_`6m$2&rVJ|#+QYtXEw{FCM z`w~pbUrl6-aiSEf@`G5eK_{aJsbSx^d(2aHWGrQcPbJr4Rb0x<@H==r9&wn<6FR}T ztoQulHGf&wmE+2p#cvA#ElX6=ICW+;s-?ChWQ8| zi6rAW=*Ooj)5VbjjwLTO&xHaZY~bKejBurJCLJjg(XtQiP*tflxr5)%H!@wmCbze8Epm|cGeDH)bO7!{gT=#*z}Mj?50kr zj0k<%P%+gUNhcB~r4p0&f1a>RL242R7skk1N1;E_64?>h8EEnK*@+H~xi`=jY%Qv$ z?I>%o#SB9^hL*h#gG;c0aJZqL#MZ#&iUM)S{=uiIWB&oHOLC&N!Or^qu@)GOt| zW78}}$D`jCBA|SOEGoJSqQrj#ZGBMm%MI)}yVM7WC!;~!~eU?nIk zjA1S}_`0F>d|S9NxjV7D&^7?i)GRYQS}Nj+xR6?i&IIeRQ-RaeX#%B46xx#nOz09; zE#%^QWBR;%h$@GQ65ceQD24_iL)?(BPSxyX0}*irt+-bVe)vL|huGEZDo$)*#ebs2 zZ{g(>|G_M-FpHP<-_Uk%>wR0cJkq;fv%$47NNfsj&TboYVa>%wgUbdcU+Du39$e8& zW+o4<^%*%^4B(xVJ4fhGQKwU^#KVL!V}ASm0l?}Ep8Wj zWkqMk9m6?`VmFRz2~QF{OHgKpF{DcIGf|`=8tcZmoZRc`Ft+!%5sJ2MO&g@wy?%8_ z9oB$P)R%8f?}&8=FuWuCNrg-0Xx6op-GSD)zQ7I<>`=6x9eVDMZwi(H0n;NRs$*>_$O-w zi4X}AR3zsCel|BlRF2M6o~8?+pGzND8kq~!1^NP2p>Z(t)QF|fUsNz1+I@y@Wn1?? zqI+N0z78;Rds~#9igsPU*|EJGx zlqrteX8bjFAd*Ria4^7I0Tu@~B6{Y`(#((cp*IIaKoG|HC?f#^yh!7)14Xv&+YWr#pe)4gE6x1y{D#xvYLJmWl%?iEhnVBcaoNzTSn1g8 z?6dX3R6RV*4zok90mK0%Z7~ZGv+@R?)}yfKG*-1uMJlBG3&2hV|SaZ zZSH-{c7opS-@W*%vyG>Z>{HX!n2;* z%xq`3hZ<8I@OZmEFQJ#X1Zbs1{owKg6q&(n6Z2aWuOm1xMC}&n+7Ka(ma?nc-_6Ry zy~%aMjkD@ws^hlP^m*zW_k8d|;2G`|bAq5xx(*v>T{*O$q>UZx#Jhqy3?g{Jjz}sh z)+#%|LNW>2W1$eM@Pvfnm2{5yoecmiAHXt;Jjyh2{kfkN29PiR5MNH48l(6Z9&Cho1k3)w^U>Tq?Kt*eQ;I=h{}*N%tY zD0$)S7Q$}z8aQo0k<^vo;w_U^X-9|yRg9$fMGr_?Xn~_MCE9}0X=1liTf7^*Pf<&! zDYz-Il{dq7Y!9mfOQ(tKwKN+xJ8_!mwcO)@wM1aOAE$}lPBxl5&`Oh%U9ihVM;I5F z8^SSI;yrqg5i7IBhV844@M(DL{9@LmE#nw447rB=z;%ZQlg|#%&fa(I4mA;HS zq^b3%x_P3D>vJeojV+2@woM4OR_Y8|gM#eh_D6b4=1KD;H65MHzczXH>0__G^6vPr zSmIagFFJnqFs`K5sl_w0%zUw2^AGr8K5MK!hG5#bcu!osn-}l$ia+&=ciF^Cb?YtN z{iY_W#owPc54#HFS@I*o{gFqSVq6XIF)Mt77p~w0BK$^tnh?WFR*%+5WE(Q zn$%eh$iRa()1=br)W*9u-Q4v9WV>liwtm9a@8}}iJ^iH8rLwjeJM^6fIS|3VUY|N; z9(L5J@xW|gHZ&2g$B&05*($+SsVr#xF-OD}+Fx%S@0xw=1H~Vb!lfzUhi8Sa3zz)q z^;bWb`%i4y@=M8_5v)f9s$1lALc@%TJU*TrPf5oRoDZXwT#c{sMKTNBQ$$VmQaVy@ z-A~x|TbnFRP(Gj+gCH36FllGNj@8|kHkLJcLspwsq?F-4y2ra)5AZS3VQLSx5B0;9 z444Y#vbYsK6uLf_d9QhgX^UyIWxIW^^gmE;dk3Xm>fH=+M-v1*HEpt{&aNi)F2#1o zW_}~r6IG-&x!!~ertxT`6dovFeWy@62}QiQO#E)FC|{c zoX2mtCl$)hbm@yEr$9>7*Z|8p({6klfOnaq(;#eRW zBOWm0n=!R1&KA#;&-$Q~@fsENZ;IHobIVYCjktD!i? zSu_MqF=NsacSJ2|l*=4p<81>@+`JtWL#qeAQnE;f!$&%s z9S4S+fOy;4rNMg;ij0`#`$R8!$wWer&?BBP$EYKx<+=%4VbiPZJyto{MVo2FzhjJV zrd*&*Lgiv&Fl7YO)0bz4{5$}hJcD>-^wJ=mNl__}H0t6p_LAa^GS-L*no+&WWU!!1 z?Y4WjX|`H7k%W>mVc3kBWsiTKU%~5xwvbJNY=_983qj;g-s0DD+l^gq<~Gw#*Hbhh zr+Wi^@xHjJUtonx{D0v8J#Z#FT`vw7QMu{sy6irF4G|JoGh!<#dWpZ*FY`6w^UX`2 zpTXx(;`4t)|8eQ_zktvGFmQV5^Go=A9X@|Q|0q6x)!#n8eXsZ~D4*FosEGBtx}04qeY3BR=?(OTdn0lw7ORWxu(znyCX!}FTZm1~YgG?Ac6;`OcIWrxS_XP1LjYpVSYM$ezbDH^t9<@$LUSaZhmId$t_3rP08!} zqIkJ*on5%;SHkVZscbEtNoGPxHbO8FPs*A{%_3T5kK97va^cpJJa5Xv`BO+$hQ|)a z4_fDJbDF8HaZS~jfc1p2k#;kxn)pyutQqBreVbc7Y#By zWY*Viy>ZKp58i*%x|`OCmv@N&>=3_4ieDZ0-gDPJci+El`)KP&DgjZZUyBGgPYJRY z{`le_KX~oq!^Fpj1)?N;4bBI*aAo^%*8b+P3%5^SGkr}`By4b3{8h!v*tZKW&pbQ( z%<;K%we!^%{I6&Z8-|paGj!3tQf!gJ+5y{Tqp#i8X6x_PZzn9fgDq8gMLwpUGtHT& z$w~C8lQv$>tDWEuX;tl}9prV+TdNP8S$U@UZSS9ni_cBTs0*f7`rhdM*{<`Com_L` zCj;UYtoVJ8c;&tu@4D%yo3AyCUp0&Ojf+>mf9(g?3Ri6r9(4Z~{XzBh1>*Iir_N8m zIsI1d9q)U1n_m|a4J-BmZj1&C%)^+pJ>ofHKjZ|$Kv5+WwXQ@Hvx(ke z*w@~pvdM#O0e#e-^rSq=Kr&IQ4r5`jL^7_BB52_Cem$WTpOn!~m)i=7E3OOP%DEAw z!KK0xOp*8m4VaxL=s-aEl}O?7EFP0Bv@diV?SNz5P%-}dy7wM?XBF}k4H$=Ne2~Ce z^#;3|QhL<9Cb8zwnuCuVAoK|xj_+Rh{wvqNv2Rj8!_Nf{l%}UAiN$S;x33r&7)?)d zgG||;Rp-0ePF)vz_;!a48P$+$>$F7gg7E9R~W4>6b`IA>c~osTYO)5 zKfnLU6&D+1;$_^8{`;8w*t?k9nT_;XW+n3k*O=A~>IRvi!cd_;lX)f}tO))G_ZIUO z`!@3~O}tJ2ff2sMOtN*C*47i zW4taWd(FKTlhWJf5?9mW>VWtV!9da-L6%#bOe4oT6^>>?@j#l#yTf5r2DA3Qn%B0-Da4~k!zyYbx{-`V^pi||#G zaR0c_@WG$n|C4alN#ViZe=#2@U*Ar=zWwyt>3gT|&HmJPJMrm&uw9l4r@^aClQA?9 zLT1Y3Fxre3v(f5>89_2=fq1tRf(QX;kVZ%+fj4gn^@u;gP{TWn&Ri%Rj}TWXE}m$( z{JXNl-eFS~Pim@H*Q@K>XMD^n?uYaX6sX9ff!%4vfMS3iDAh^>N7HZe!o6Xkl@VAM zo=I}S)o|JKvQD-?ZYb$XPEg7C(R6JfKad{|jyX>lD{a2b-fo*xr_<^c4mq@9R^h8M z@0#3(nVmB`PAOieo@Y)K=4a<;Yfpz>3<-a~`AzXllPhzN*w*cPa$m=K*Gl5ba~JRb zL@mR~M+;>Nl8PENK+SkgxRA}^PPmSAW|~d=%zO9uJ#1S;KI(ZGeJz{M;~~6W%E4Ns zdx~)&hg?sCPe$t~9o6Y+b+tHK%$@394B=d;br@5`k|mVm#%Xe%ocGSthiDA&s0zL$ z2I44UQE~yK%SqmUWPM2>lqHT5W5KbF-pXw8tNiMqCf1(QSG^;?(d1ygGMt}Fo{XG~ z91kvVaN2=mOh6Q}bOH02c;PyRZVLsS)ZN5CF}#10o8WPThNHFA^k9CDCC2EgDI-tI zW9`AFKqHR_I@DjlY5*!KDPbH8bWZ5D_$2iKynGCS0hRQ}rzif}E8DfiXE9E(ptBUF z<_Z@ALVG}HqJ=%q7d(eF)wU{XdNq#i+WS1>LoQLyh~Y5K>iNYlG+g$o%#hHA)yTJ9 zYANAsIs6V5`ZidcXzBZxrgmHqfVglJmlrNDB3+1AV`$xAs4(aq#AG|Sd3=3vouUVd z3{vh@hjcj;Qdg*AWXT%oCzkHvzg?V?bwc8gLPL%LzTb^qK5BH}EtZC14j34$2BN2_ zt)o${FxXW=c|aRCW$ih8-d$pzr!O$iv(VcGAkB+Lli*6msHi(aNU^An5Pqir7Gw=8 z&dKaxX4-uwkJ*P)N87^AlpVo4@q9d=PgcW6lJmX;hGBDF6GDCGmg~9*5L94~^+U-3 zH3LRuLHGQ2enab8WpjsWFRAh>X{BGmY9jiyJ?_D#hu~5GEhBHT9IA%$pj|=wiYX>e zie&PJP!0orX^e&ryH{GkSwhYmQ=YQ794%J!|Chb@fU~Qv)Bi0@IJ>TKQ7^1Ito{_R zAgFXONQV$9AqnYadc8Aq=l0&)skirjXYSm4r?(`NHmM|$5_(Zl7Zi6_x48aR&Lk)J z|3Bv@KtNFcuGzoKX3l{6@|Bx=@A;l@`Fx(|{fN4AmI?YrBy>0^Qu+Lt@BoM6tc=!+ zkFl5EUccyDl8w?0?duxWH#RnOY&L7Wx}ZLU8u1`m^1^jk{NbMG4nOf^0SOjli7Z(& z>L{U(%G_P3ZwPlEI3AF+4H_$$S!dh<8nTVEQsNh77ZZit zZa0o202IQMDF!xU!e_7r@!!Wi%xo4>FZ4z%8=L4g?1og=ux?m4>6~M6x+o_Hh6aX` z3!FeTAM7iPdo=zrZZUfkc+A9RFxtUh(LfaT#OwpUB7lnd zLUb61!zxwv07K|bI+Bi<717Hf6J@mNL|`Iqq-qCC+5m2bEqH?udC`j)q0aji=k+8a zy3jv0nZULqdXbfA5Q&@M(g00N8r@2Jm$TW~0qI5jqFIbS6fyo;tGY@iOt)(36R0 zk^T9c`$b#HQ?O=rSsjE`y`cS>QO~d$T}pBO!&eAY0qEESPMHqM95~s^o?J(te9*hf z*da6Y8TzC0NlV^2=&h0!s*J=#%u%@0NWS9h8U8B-$B&Wxkdx0-SqKAA5Vz6j)_ZJr z|F72E6H=i06uZK^g!HriL)*7P?tbT#%gST_f^ zgj)06fF14X_uW(BvxjpRM|oOgzrpeC_-o1qip5uQfb zB#~+8Dd2(#ebJd-(Lyjk$$2tz)w@cjZAAbqE{k3gt+E=$L^$JJ?zWKnl zOK-ZvV6^Js`9|JxOcU2;tVMFrUGx>=$96uo^Qk!kl1|7v|L?JXfA(Kq+BTJ&b#L=* zSI#s~5humneqXsW)x!1CN~=z(?xs7lU9fvu;S05(oNdbivRBZ)cGJ(l{qvi$$Y#mu z2ywH_&~aN3RuC5C5R;8KiQT?JFTN$wbE-8p_B2YHyK3kIQlXct`tf6sIs3dSzb2sr z9Lt`ki5!LHCHCT;=k`5&03<9Bih)Etg!_2UGsJ|$D7NE6@`Hu*n z{|#XQcnbyFm@bUNl3zz^?Y#z*oWf=w@5UL_#B`||eJf)Y8zuoa!H;3H5A`_~9tz?L zokcvWAmx6saP6KMiGN>oKDqk{zwQOT_ch-BbKd?FzUw7^WuyeI)*wS++ zM#*(q8f=}GPE!|gnc(_@P!P@&_S`4j@R0D;)xu{?9fnSQ7kWpyU`jBGaClS5mIbsZ z0%nB>rtuRCJNe@hh?C%Xz+p)=u8NRvsH20(cTX%3N9X|>yg-;VN>ruh zq+;n91vMl=f-Z046lnR`Gj<5w!Z?KI@prdK7j9YftM4)r==&;4%=^-e>lt`pF7aj44-CY?v?h3Y@+i6GV|DxQ%r z)cG4v9Kt~Gs7-GT1kCc{`as#G6$mYwnfdI=Rl{sS|fTQ<^2ZHcnzN!AaZIUcdr@d)23?+J` ztT{LrD~}iwVz-0KC30C{{xiT62JpmiI4avAt%kitfwdgU0on{qiijV=BpElN>lDZZ zaCcplDV$HH+oicN7C`@P!C2iGkDtu8A*+VNN_ zLtI@iu#&*uNNv7b{{r8G$qrNAPx$Va_!ZGotQ0?UzEfhp>$WynJ1kwM&bQ~g-S-JM zo}TY82=rYT0@&7`n(x3G5$C&8gFu|`0>r{F|6_@0>-Rzr>P1D3uFZGi7!l_?l>5=b zugMY4obOPMbJ|YNcPY77qrrUF`>y#8{lYX?@a3tBdyI;rONGA>>c!uk?*OBCYrdnv zh&esqp=cyF95?{6;^9Ozof*!Kc6Z1?#}xfS6NWQR?9mYJGOLg?AV}faZ4$UPZ!lk^l?+lO3e0c_w3AU z%}tlaMvE9viJ?TLkQ(+E**uyC8B+|Vu5dRgHTRf%bTYM)Sbq1NYi{WLj_g+Qwt?FQ z9-P~BtbbA`Lj1SV+oIc(2MY7UyUJUuvm-Ox_fH=hJ4|H8;}hX&lzj@mBEoC)Fl0fS zSUY+n$wmx|MGFH-T+m33CI%`gqL?U0MuH>33JJHEEoBU=!)iupS9&aHTwseQo*hM; z5&ps63s@tRPCan!qX`Ee4JJVpQN_~?MZ^~#l!y<3*fMonTozeFlWH}nEX~>88SRK2 z=MQE$QLR))s^jq~Y8oo0NQ(RWY@BZq>e3dsocM3V;E+WuQqQq`;+!OUK#>6n0PaNhqcc8x?Vb==Rr%N$OhA3^8mgQn}|)sC$jsp`*QmVM+c4!9tr2e zg=n6@fD_|3L|rQ)c=2CqW;$4MydvO1G!WxUtZk)FxGH}Y6fAlc z)nPAy7EY_pZV@?w9y3*wxl;tS$P8+LneM_)sEcW%+n6?-v2e;_@PCQtOb1KMocN`L z)1%dC#s&_{Ul@}cr)T;-a+^~4=E*M#|L5eFg$qtz;RZe3u62W*Y6mbK5Pj%qQIMc0 zfI4uY%nPBQ3wT}D!hx)ou%l=IZfik&9Q_2lXf!Yu91V>J$HU`M%s)^(#QkA>T(R$m z*%I_3gg0Q0g7^!HC?`kYV{`Z)J}2TToRC+d>ltHXTr_uhYxPn3Deh_ZX@=nchOHB) z3O_T-}8Rw?iDgR^;n<%_dB93(yg@Oodc2O^$5PJ};*r1lRGSCnC`r`V$ zbGU)OqKUtF9@e@IrPXQQ%9Cki~*jN;^hjDA+;IJco*DO}~7cM*Taq)TByk$e< z`j#4{&$YIxNh8IP#FSzoeJM`O%^lcxaPPtSa$zVuX zyoo4iD}9q~ldWZGMxNBh)nR40)28aR_FCJcjpM|n|6FH0BvM^V_=$J(8d%#2*9ag)=VMp2>SfEI)K_HUZ*@A{) zP#)+Xu+E~TwT(T*5c}!9>>h3pHyS{aH4?MMdY}ygc}2tswZ112$R&qH4(zHt#vlui zeTF8c=^17k(i?Q&Yj^?lj{tefIJ1t3nbV$X0z*(EB0O=C9s+_&x2T~)c%;SS0O#I| zo&lr}4K}px&>CR0X;lQ=kf-qPGay zcg%$TbDtd_$!US|-0rly2o$5d4*GK6LhA2B1Ewx(_ki z{KJ82CSQ#bmDp}SB<<`m+vDn*h=7WE@PWN?045-`3`AoA&}X z2*eWkVyxn?`i6jf{RR08<`*nr4WP539tle^0Zjo6`LMH*e)>`WY#}?7n`UPW(}tOz z!WO^K8Wvh;q1jHJ-m*ij$`CqM2|Pm|r-B+@MJru$i`-(=yH!9j8IXQr<_r`(r&dH< z5yJN0enz4V*rF(YrF>9J0=tzA#YHhb!zKr-^E+SM@#3!McRjc3xv}lpR3VY{g`5Oj znnrMB(W7THLE-b9@I^|vgudV0+HdSPHal0bCaNEziZ;4kOk`mcC<+oM?gN|VY~=%*rlVtL(@j+rpCo_sJ#Ms8W89>`oAeV&e<;NbaV6I_^HIxg z({AH#^O&xv6mjl8rAN%du^P+@*zvlMt<`509eGM!hqc4Hn!7!HWohZ5mY=r#)bJlc z{(StF`19sopL_lAE6+bZH@S0QS8P{onyl)Hsy^D~TATIpB#`Y9RkdZL7-iE2h2{TBc2p1Z7fS#N8< z`9b0QE3X$mvR3#MetU(Box)cL_f5VV5;w$eC@q=3ed_jo_dmJr`E}3tK5r)$K3c!% z>?T|bn}7TG*&U{fw=Ha}KmRVt+?HL9yIVrFUd%jtF~g^OG2W`O){9xWcjbIvL55yT zpIa~XVw{SE*o%3w{=~W(Dzs(*%tjL7cs!V5FwJ`5ju9aaE+61RklKq%J;nz_N!U-| zxn$yG+5?e^A&5Xft3gcEfm#ZYZkX|rQ{!~Q)*~is2(1gf_zJ#yweZP}!j%oe=i|a> zpB6s7?UsG(A5}f7eAfPB?gz}#(AM~Dd^9)3oP4w=-r?+UD&^J&s27Z!mhj8yUJtsjT3Ls>L;axbI_4hVy20)p zT{}#N5N!2i{;BDwrk*-@`1#%21}EbchDdX1Upi2YXY=7oxI&giAfqbV9d7e?M!PfL z-KKeD@VSAfM)%BCW-BwX88m3kkz~`Bjg9MC5S$ET&l>aM5e!70qPb9KF8A{~6{Yn(cWhj#CYen+l=_2_oisZx9 zP!O%oT9N#bO+>L)B%eT$Jc}YZ@~}}PN7{R_pc&uLd+_^jc7kbA}?k3;XQHW=ZKSoF4 z64&liI*NS#>qDov7kgXgt=mOHL8&kiss>5`!sqR28&J4VR{ESZ+L%88gl#%Sq>=;S z5oU%Q(G@$3oouVF$%-^|y;7&pDM64>gGhrIo+%s=n4YjE3jtrzRUv2Gv(zlMq(!dm zFZ)ZO0W5Kev_H=xx850bB9+G)B1C1XHSP+#OWv*Ct>j+nQHpQ$@r^<8-*p^c$MSW| z9{)HD1G#WA97g0FHv3?oaj@zIs>Xr#Y+GhabS<}*+w5vJwi&HUZTk8hvPcL-3w5RaqLif&Y z@F5lp-I?9s)aixpR5!RIdTOD2uWqmdF`dw&)bpP_i?^xzLA*^bzrFhWaorENnqh0E z;lU7X&0_aM-K*`Tds5QusnzHHdSJeUPaU{%U(4B;pFV(hflpWJ7kx~kX`x#)Em3%! z^4@|MvRXDl$C((C>N1w7I-(9L5PT{FyG&K{Re1>d>-3e>H){Sovl)r3dI$i;Yl&yA zLD{G5V`Zhj!M^tUR=h0y{iN`BPT>Ogor)z5R})*Vd3edPyO-U4Z^J#tcDFKQDOwB8A#%j_ z&u;#6B!5?mfxDAA=G&giRdN-!qN@#8L@yp&=a&|F!{E3B&>lfWG z=?}H|*0Jl@CaTM;rc@q{OYPK~ts1A9vT!0E-G*!^OIR0Dg;iWP)l7s0juT8?p_{HH zN}(dibdd_T-UaxMT#NI0?Whh7v)R=V>#ykIZjcEmVgfMoX=aA0qN*wuNqqo%kNCzy zo3mF)lo}!@R77S@W^o{%l!UO!rgq1LltcB$dg$4v!spE>6BA^jvb_j`w zAdX(2ZeGAf}~Y`ebksjD0teDM)356ca9S6c5~dEZ`?QFuLQI4aE1tH*bK;R zS$o115ixe4r}(Y#`dMK~bR$H?d)Nyl;Oiv+X_Egc$$y3BKTq=)5e$D3`%l~fwirl8 zf{|bh($i4wDum*3L=b1aSzFeU>WysjZ}M;8)>Fg=WL#?~DI@d9?frI>%F{us{l>5( zY>ztQZjnU^%r@k#0fXosXDoymb`49O8O}l%eFyzrBr$!H78ZGg&xwjA=%n?WCmI%2MfCwwh`*>!yVb^IsSKfC^=H7^6p?DqsrFqImg^&eD! zU~)KJ{mFP@HW;#zark!i2NfW3bfNm=1i}bK&cpR}pOApCh>|#F1@JMQO0)6(H7rLf z`y6wpG%QJISi)~>SeSqx8kTGz=g$S9VF6+Kof;N@pIg+h^t@BU;smMDm4Jc;8Wv;K zdP>7$b!!RG;zSM0;vM|H`t$Ea^-NSUi`BE>TT13+$_pj)o7FQ>$t(-^LCJh(^^C_C z_%XCA7oAllbNTpP5?juYQpRvGN^6I%yIPr!9%$s%=oOyfEI$$l@a*h%dk2h6 zqAell^4XbIt`k)>9mchix`nwtdS@Qoeo?%y3u@pDRQmKDt3}=AToL$o;M?izhprjA zX6(+{hi6yK>GouZ1>Nz@lE6~$4d%e+s%a>eS|P#6h@Z>n-@)w6HapLpZkt^EtVwF|527rj)gory<^_pY6tI#Rr&c2>|< z`zC4v)3>Ej2X^vkPKL zjZ5nUeGOts%R9u9FZ#WPMsNWXqb*UVoRtw(@|M1GyB`5vL0jL}ZlP+S*#%TROA`wN>e8v3C3W3-Y{mI4~2Q$j^+0EqN1uOtNOB~q@NCsSF1#+SPhO^ zM{Q#upN+~Y@<9dccK8!wEE=p&4iWlN153dRhSDr5vqk8FZRA07S;l0`|+%_nD{So0JB4F~{P!)FJfaQUMRSXJrlP)A8 ziF7DCa_4j|y;JYC)I?99&=547cN{LT+#M+==u(i`#DLKWgAMNwg%g3GNHB@G<2Ety z#_uJ5u<}=Z{NJLPZ9aa1r0pI>x7}ys9MkUSYdn{L>uK&-^s&g1?30xj%P&qmz5RRJ zzqfnlKxS`jr(;_WQPrl9QOuYfMxzGJajU~DNQ5Tg0{rtap^;eVT)0{ShE5_FgTFHt z01hT34{s=38T_X68s|Own|rVBySn*1YnQIQ_r8wXysMc_spcxtn=@t@NSC4m`H?*@ z?|ymr4}QXb;Z^>Nukn`>!o6=GUW`6_@|$dZ-R9GUz4?~CmhGuC8xQup1C0lB478zl zG#-*fDs`&yFkEXqaHks&I&dj;DqLy<#H41E%UM&$JiKsT{rP|U9!Ilbm`wkOquD|| z|CO^=eEjK&Z%L%{#%UT*@L(nxPK1(Rl*Qa($em!66l1cTjEnNtv_S~ohRu>8!VD~n zjyO)oV zeRW*e9}}&yLHxZKvcWJc9zm;L<2100_!JTEW$WfWYQ7 zJM~5-nmitOhr_CranLbppY+ahv)p*Nnye<0S*A!(X(|Ei1!CI0QBT5?@fJM=?=Us( z;TL=O^WCo_TGGwm%ka0-bG}`n-ND_7?fF@vdMGp@%Iu_(^}ZUfk?Hm~un#j2GaDVv z`ewbcJG#-YM~4WRmVifj2?+W;h_`fx&3dr=q0os7|EvEUsdoQHuDnvdiaYt{_pS`;upn!0f1hdi83V=3J_$F zP7K9kn0fqR!jIHuSKOA6#@0kvM<4dBAXkvhW~IW8%jqO2hbK5N`4a*M)>{;Ws5*Hz<5Nae4Lnowx6}{os~k=5gnwe>ydtniw0~i@EaQ z;{|c({;4O=2hQfm{mHqJdv4^u|Hxg4*Ps8 z)C+6s7j3CYZvJd1zfwK_le0LSFF&{MIJfV3?|sKB$CdTxFaHDEjsyGNDHjch7>ZMJ z(X{ZiBnope`ip9Vso#mcC=zxM=i~**#O*-%y1v!9np?`<6J46cHMDlDotW&HG)(#@ z{G-|GSY>QrTXt{pxx}-+XSipWeclNGcsVz6`z$#VNbT-H-vGfCi^*+%oW6mH8uv}J z0CF?qjsLWU9Gmiuw_dm`)?tPS?hH1nx(RnZ$LBE82$N@z&AUam3FTZC_CIy5)3hlpnr=~QaluF~G{BjHEfyR?94OA{yn z=~|lm9_)KSbC>1YmTx<6CT}5cq3&llh=8ek*fs1*W;MN@*+4x=-icG2NUiIJ9irPI z)o8n%-CjjRSJV}4LsXS;@|Sz~Pm#Z-{=xezH+ZEla6bBJH%vT~y`9M2&faRc*{ufaM`KsJv|e;t1JFL{)FHTC59?(tna$ByKVw3V~7|N_912uc}8@= z_jBhl?VNUdZ@g98qTRTBCXVUZU03YHxMmZ^otdXgRQaqV_CZ0s>vUP>g3n#I3)Q{Oss~ zy@efxId)tRioP=5%P45A+h{Y|tya7i9CUd5460t7Q-n`O2vbDEDR48?I&+Vu*Vmit zjhYgsbd5ZjHl2{k&UaxmwtA*dV>9BwW+|#tVdSSH7^Ue6(<@J>fle(Sp9r0cJ_WUy;@{?gUj{2g=V2H&0IpmF0 ztRt#nbxt1c^=UjhD>B&)R*eVAwPMZ)T9s&7uDf&H#&xQ7s*Uz7Y#Y<%mxW|OMOY)I zG9x8P$LYK}o7U7}-e4itTkiE;pB2upE`N#SKS8~ku5Ch>NU*u=W^7DlY;Ux?AjQ7aUh(97*=UM5b7MMl z?7;pfXZBV0#}0T7xOXXM8h}do#2|f41#|IOFd0gRi6_R6#h-FM={hXixpCKq(o)~m zgm6^&7P@8ue=HOeiL;5?l}aZQiD)8M@m0(fOI4NW3CVq0FZP}`8>F;^S4^;{(0mO; zn;Er6;RKCn{R+C5QkwPsdO6oo*j(6jV9ie-AOv>d{_IE(SC+?J%L7e);S=aE#75a4*8Zv1bumc5 z69mRHR0B;jTbU`}Xka!}W>QWfYt2w8^mC(0e=$&@huveYQ3QVusb_oB?ap?myw|*r zL{_ztGCmQ>BFk>lk!o7m|itgv(xM z?4)~=@}xX(r~toKj+9G8ELK-26b6tNR7#birRXqKfI2y04Cy#Kf^)=77q_?$BS4$t z)bkzF>xYQ-Ll4K6dmr?!P_1rR-O~7g^u~^_S-u>(B=V){E05hUyLp>*&WKNl&q(lJ zJJE1fmF(d+)}Pf;xmb8~L3c#b+_<*y0sZ}k`@_q|*9^B!sdlJ#*!HnUe2>NF3zPZD zd^wwmA}!Gu@?j9U0>tf4Xnr{IPuac0+vdi0jLl~c`L}aBy?d;?ZCec!s&Q4OkLhCi zyr2=dbr!WwNmz_#rxE4{2+R}e!Jc$;xFw7TA!nykZFH+Va!vIhzQ@lhi zo|>8f-aF78=(6_pDl87O-DHy)h=p1llbdesP5w46TrkQRead~^(dcvu-Sxp{H^?oEGWK9KI34~eXzj(rA z019{pS#~auTQtuT!ehdrvw9yEK6(6NNmIFhz?Qe>@Qh%Ziv|*6DvhZ8MF^1?eh!f@ ziK92la~zy`LD08-An%e1Ps9;01XU{r5)!6$^V|IWcv7~Ke`L;4;%nL zxaJuBUi`0KE{6FtFD8IMVS3D>)2=6^5^*!Zsefyv+hWCmt z*P7IK@yXZtf8qaLlCXzNte%DO(P?lPY*;AOPMNoZ>YzH21B%=cT&p>0%3iToQq`GZ zBEW%j72zUGk}A0Pue59zHg=ee*JyU`+VOG3m5LV^yOQBbn9(4f1BLm0(*l z{iDVaQ|cbi@;LFElTS*z)^F)trNiZ0HQ1Kz zF85FNPZ*~?v+l>^her;Uc1(@#$?Q+0 zV&Ca1vS{C=JT8Ul3<&v~RpcTdhsTG;hGtVc{Rg=1=F#5K-htNCrtHeZZQ*Z(zv24^^-WsF zs96)>cQ&7s;1CWOa1SX$Dy!P6)*5Aw<%Z?%<$;F-eK~5J93LE=ALUp!z<@nQLo`Yt z4>v^l?T_1z*`IRGN&s;kT>Kx;JI9Fv0(Q{dpsQ@|wdRJ~<{aj;aS`X>C-`80|*8 z*=*OlEEJUYNTYFyZdKTNsX=$iHRNU~u@*$&D2ov2v^zp070%~Mma zF3t1}>JV;ZF9!z(i{;d41Z!(xiko8dbczgIr(#wTMUac!;*w2Csa~d^gJd8m zjme^VA8;}rH^OO&1$|M%-0HZ~xYTe@&;46gEc?#w9XI-K^xr=I-6xhk-ujC9H}+Tk zKh3_D{n^CJ2c9bq50|Ek#8hF5ozla8PR6M`RdNqohpl6ZiMFG6P2HHh(%MwV+!T_arEe?)gj1>;2vcLxyd}- zqcez&DEw)$5enhC|IMS~zh~`p^S@pAxP&eE^PzlpW+V@W3^S@NHW$`#4f=LNyIk36 z>$G)+q@~Vb;{-cP@TP@dN@&=RoO;#fP9?J7sgA5-TD6Os%g$!Ej^_bu_l_yETS97| zno$uayG~vu+2HTW>!)>7%v63lH?=K)%+I$6_$Gv*+n;prRF&FFi0W0gDcjbnm$-#H zor07WLP28j%?ocF|5u6dq34D30>b(C-?;ICdmFE9x=wX9HnVr6?;iUuJdX}k>>Ysf zdRPy{d+38rRtpW^0(Rf z&ph!9{`17_W5wtFKlcCF^g`z`Yu23-X;a~JrjRdXOX+gD;vcl-?Ri5&6;uY49)*!; zlPOddo!tW3i`PM#{kj+-ymqo#g6v5Wloy8+C}3PJ2Zb2rus2I)=nS2$srndZZ)|U2 zPjP;1dS{s!OjSZvY)nPWJ7DW{ltQd983v&T9$6=govhO$$^{X+UQ4zl?pdyR*{DjAc_AVrl z0ON%GFwk4vpgcFiPxPc<8Kl+Jv9|_V`)F?JT~Fch#Ra_*N6<|0~bN0ZbTAEms%eyAF&+kXhjD z^Q5RERS6Adis`~&Y?Pg5_jtZ%J8IpbA6BH$XmPUrd`)Amj}q!x-HS&4BS!wi zR{q28*WItPPen%)ql2k&#M>8KBlZzn)jniTB49E|7924Pa%mZZ%WSo+?oeM95xzw5 z552KhLM?*R6M>>4OWMH^%=so5B(-5pSQ8a5eZ&;)FWSof5&v+yIFu}gb8Om`a>bot zCkMGJOA$MAjieRXHLBKM>ksDVo?7VI64Tjefiv2`aq5){w3^#|Ev@h;pZneof|A4#K9u0 zSa_}eJm)(KPiJN(znN|DeubxR)C)ezvMqPHZyjGYvHDQU_nTkpf5G~!^DuoVbRcw) zKIA-1Mm%DG1tKf}WdIh%9VA>qZ-kEf$A=0#!#l&fsd@XnZN!jN1r4;$rFCg-Mx)*7 zaXG|TbO0I5gx3rJfYY^{c`&%#*A{M!qdcB(E6Zm(X6bEX#X@c(xhJ_NI_n>f9`Zdx zJwjlA5uxJXCSbn;_G8e&^+t6$XO$cb=F^!}IvXzpku$*!lU47KZ9q$e9Rahbmt>8! zmAR9?gSnjp2~zkhCHx&J{GDA`Y!vG7E4#dHN$Zk_SKr;cT=kG+Wq?=_csR4V;_$mU zugKD@CFj(buc8%+A}roIwb#IzA>OkmomuT6@{!~t;jwICC^{5@a7_~vHGNjU#b-rC zH{xI-Rw8W4Iftp|*@Km-Lz9OFpNPH?eSv$zb%=^me$fjEp^pfwfj@{4t`=VZZ*Z)& z^R2&-aNjcyehXUw=TIUght5x*4 zU!eHg74WySQV=Nq`F;@tdUiQRti<9sTKT&Ax{Z=f zrZ1w&sX>buX`E;u=?OZqYjE#hhgEY2QOH)y6O)ru<>BmLG8@Z> zi{VjQu{YWq?XWa-5|>_42PXjdvL>U|QA?!{T9U4iJz|aF%lb9{v4y1)_kbCxh#Jmd zv#LB+)H(E|0kC$R46^A2x{X-B2mkM6YE(432kTC&o)+4xA4;aCR{VE~m>Y60#sd;81cP44lEQ3+RKQ znBEof#!$9^@(5Rmg0P-g?B*|62-T{Wx+(oMGnGUA@;$4U%<1ao&8S{FYSl~p>4hby z|9E#^H>I0q@yB!1+w)KOUt(Wp_`j$5iy8i6im%5n`Jd$T-Zx93;)@ZdYoQ1mI1O9sibC0 zgA-9)j`99ITMBEP4?36kui3I$u8><5ten$FtXaaEVG8j<6ca;%U?}1bvq3iK#&)Xq ziYdIFI&CQesy&3)n<m*gZ;H{P=RmIB7TGH>2J`FZ!jQo)=zzdczb{R z?NC6~|GT&2?H9w>_rv$T9pkticyqPear({Sb|_fgo3|rz`gY*WiMKaYyS;C|Iow+ds2^_Gs#u^O*BM_qG+255<>RZ@1ju zwS3j48y>vTy=2#tT@62S@&vK?cs+k>Cwu}fzcp+Ln_~I|uB0B)5187M+KQ)|sKl#< z%y4Ls%XyGOX^Q|HPvDe*a{?RihoJk9#v;i`DxC8T&=sm^O82LsQ&enHZeFqG8xIo4 zZ@KV)@XIe2UgCobKadRQi~0dW){M)vwP&kzrVA1giXjK|gQ`-02_HGo=To^fCY{ls zv#V)BLut{)xOLBM`|da(%gb`Id`qRZ(o)eSbV)->K42N5hpFNCP^rj&mOqcbnEwd> z`PT`2nc-+S6-$RRzN{Z@4tvFrlrvqFlfo3^)L3)|qrm`fof$I^jB|uACdljbW~ZHT zG0XHHA%dy zv^`G~IV$VV#9rL<+`eZI3}(e^c6erZwhE{P!kz{OVtC)ouz9GzDByhyc;CY3e?y3a z2c(3UKCJcObCX)kDJD79MFU*OI)j>+E>(jiWXz(@Cg3J`@qMQeCPlg^r_G_a)ZP}} zH-FK>_xX=X9*)#G8BqTiE9*o<%8fWf+9%fAp`i-V5juwMPm)fMGweVp6bcu(36eXRx?$qs;+9Pt?5;MDbr7DDHUm8Y@CgAg;14GPy_TBfmgysj&tQuFbP zv)Y6b|4HL4FO#M5=q7}3Rz8C%Ad_NQ`*Vq`jSAaKw# zZ$_wU@8D*RkkblMPC{!$xoIcm!e6qaI;zz znvjvVmF z7Gtxe#nxhPcK0DS7Ni!+>oqVIw5_oDDR_v2cfvF8jgu*F#+`8`Tro!qqleRTI+wv@ zQfZ~;7DtPt%iZU-p=0Hs2|Z;d?H)TConooTqxH&2b-syuDDkv)u-)r3sqR9&);n+NPw>o|6RuDCnq zdZd4c0oxGge)3uW3_WOwNu%Ayj)sP^!U7L zs$b;HPI2QG}_(h9ybdrpq%@#1C2`PM>6E0wd3)!34CG0J;=*3C-Jn-+QiAJF_YbR+_OvTQK5Kt6`owT9no7lT(Y!mW${3@LaLv*gVPXhq z57HsR{S(hGN&Z9lrT>k2jm|KLn_)s=EfYKisLJUw#<`~8=I-@W?FXn=r<)`|_3>H5?L)0>i@Zf{XbWtL zEYEHzw^!QBnu-@-pLBdGP>7^r(O5JYPKAhEIG-qFi`ineFk3m|JEA(OKH7P3)BNW7 z%`;mnV*Z}X*Pd5r@uXx12KJYUOen#L?OAkx(VmUr&|0<+u!Te+k;n(ifgx8#J1m=S zD{aCM>)_VV#A^bzE-2N*xi0naR6gZ^SDAI(RjIkrq!O*vV#Ki(JW;fPCpmvWcTolK9TU)Qg< z^f9f0Edza9EonEh3LP<&gE6x{#H?c0fKR)Ueu%)7Am$Z|;sx478E_$83AJC$hghIR zzcfPQPkxafk?b~Y>z`326qI z=OzdiiXeKJo&g_TXabl(Vsy%%V|?O8;ENN#v=|rVf?P0&G@ytt#T4il*k_nyr(UR6O)ysUaf@M_~VhHq}V{5xN`=H^Q_2!HL=xr}a$3sPQ>lfdKcagcU= z+u*oiwUSXwJq>zwk48%MWKbX&)$j6%vrd93uo()T-Gnz_p){nH#1Xi*K)EP0Z4;kF zZ_Q}!#7WwQd?h_ydm$;I>7k@}r5^E>aJt2lB#L0-G|strrs8yj@>2oIM`Fw3!Pdn_ zFgDiWGo){>+&+EJ^gVl5JihVY@BPmn{v!eYBmUnMe?9xk=)swx?L%{c83WLFy~Tzw z(HL28Ufqtp(D;cv>d&i4Y&252BG!m4YKvp{0nLIVgkdfkWzD3Da9vvGg|XP7wDjst zN>V}T62`J|+%Z8-;Q_$q*+m{9pC+H-4uobRS44bsO;;wiHs4Q8a3S@ySkwfGVv_TPk z-aMH=c_yIe`mp=_uazmT;9op`P*T+nYpYuRukPm0yOsaQw+D0uc}|*>vQlG@rmxT3 z!L@VJuq@S{dtl<)`D^!HxBvPB#0>{mZtENAowiR!Cu5Vv(a9>ml>ZukKYt0og8%$b zwvw(SYCdXuJT*d~cdQA$I7TEWX2p3!`Ty%M}v`N}5)fsIy&~(%?U?ha~ zZ|sq5@O2g5U3c;GP1M_}u~r6}F|ktej=IYg3)@e0N_w)=tTZPOPYHD|2p|4|BBL7A z3~PqT;bb*eF2~2c6W(F_kYPe|wEdN}udMlp)jwHD{CL&DEoEt?IoxV(v$QF@+Ioeh z!qQ^!aq5s87m zjSyHxf;f58Z=3(*k213GjfKZ0*p6dAF73u8#dx$@Eo;a%+RM-{ZRalga%-@+; zj+-(1JYb3&bw({>?qtS(cfYGYq91e&I)>Tl_*{0fPzG2|{FxXYA08j!zvAJ)Lh)Z^ ziE(BuFdCVNXN%l8H>Rt!6gxBR;WlOow}f3n@7lF<*REZ}FMV}YdNep5ACKn-0;5dX zQdFc(VN=lLCnAx$C!XH{IVD`~YNOlj z9l92!u9xd1ILHSl&7<})*Esbf>PPgCP+qOCt788&CHbZ6AKIR~_t|^*+*tm4`EvKy zTfcsraMKrszY;E9DqMVzaJ5EQHiQEgEv{L$y*cE6UJqebX)H1d`^>N*Ybe;u9^7nl zlpe+T3r2q@{}jzX7380WttC#T$&5GcNfDm3JL-g*%Rz%F2!Dg!We1YL>k@H*I8k6X z=+Oh`Vlg`OE(2Sm>zU)$6ax5?o`i1RxYxYTH60x;7b=Aba=RSoY?xYD7P>3(Kk$7M z`~|P^pP%8cH2<6J#rFO8?Y}R1qw+HS8bhl=X~$ZCa|Kv@ac|69BuBCPy7J_C65am3 z>6U@kflbLZ{?+~!-2LSJY?EE5(Q56D%zbnt(2`B$bu=z!Llh4SQPhpQcLshKda?NY z~0&%sic7*jg*a#?ZMctTr)I4FI za!t9b_ze8tSdn!6o78Vozn=TI7k~505&pa+e{qaIU;EE%pC#5l`<+L=w9Vwl_#<(2uuTVKFRy*%JL9)Z+?Kr4L)_uM&3JP^Sfr%G+h>zGWF8%9 zqQ#>z-gQ)(lQBxILTl}%o5kZBgEUYzE91Jju0T(yFWi^VW$aNeLIOgeAXJjEa55T? zN8;fuRkX$}k(I&Ah|pzz;jdWXGx(*2k9)u16+Yy-*MUGoSeR_cByrk24iQv=XVL4R ztOS1y;n1;4r8JkD%g@IT`yTNhbihNV+1gQEJNDh+gPN7vRqg9nH?3}5-n~qHuj+pL z3Ljx&4CplL0v2o%@vu&kN4!U=@3D`vkNUO-3k3hsg$tn{7588ca2*k&0>h6BD2Vpx(;in^WMkIub5 z$A9SXE6+c+V{+%f&iJm_w704s)dFa!v}#ueMh4O?!KtzJhOX?mZzGjn^JJH1zxQLnZvx#NOustu+EmV^{G zWs_{mwYau_-+SdGyWjWizvD9_;~zgsGk5Me=RME!ywAh6B3x^q@u)Y#X0rYJ`}Y-( z4!lx)C4VAuEOCq*cY@4JpU}ctuAA<}EChsRP)IFZ4`yW?gksrXG=AVlGDEeg2v*XLgpFdjh#t!6nFZ^Ehd*6Tg{*eFEWWT?c z>xH5s$;Ny!ArR+!xE^eu?G@)C+d<15aWSruM2y5iJ#KU*o?&?J4WS^BC&`nQmDe?(ffUwY_W>2`Sh^%v5& z4oP<&l$PMqh7Ead4$lF2mchXsDem^0m=3zrxm&jmv%_NV!JUK)a17LIcbkz*{a2Tj zRsj8e`HioYbc$SVzgl1F8M3IZcrB!v`g=%omVlUM2x*4o5vWliq}dtLhx9%OX=XcV z4TLmfN=!3CKR|zg8`?^(-du4OIn z9NzTLtYv&fH)Jhei&|c{5VednVH&?P=UB*Ec3;5$OiFhW(w(&Q4OY5?lkVU@^PfQG zKZ5oeW&^u{*~rREd`ddXQ)~m#?$vm-9zAM8A$$+hVme0UZ%D2juRdiuWjaTmk05V_ z&m@m#rNysGWqdV|h?s-_qjJ zx4SFdmEm2*YD7dKsLB(zhpYjs-zsW_c3(TUhuwroJEg5S;T`FQ1$7nd$uzij==`c<}t_X<>9*4;}Cq8HqM_DFYnr8{xy z8xU(uN_ViI`A#5LidP;g>CE_icZuDfuH0FEa~gkxubJ5Ge9={5ud=kdOm>GE2S0e;9Cie4f`u_rCg|5eu8|dbx6Y%-R+!ef z){^UpjgiW}%D$Szt?yJxH(=7D5edn^H}%HLC!u0h+N6~(5uYCrXy{&Ng1CZhDF=C9 z)&904mRBrLy2Q`+Nw=K(!>9it-TXJ{&N=B}7jhCmYMN~s>WtUXmGpLRwY}ciWNkLI z!S0CyszlmA9AHnl0Kvr4WQNWnaNJj5nTUc)0KO5#Acfc|*cpf@qiuv0%=b}JzQ6wA zA2i4hpiO*hT!q9`dF`NiR(H^On7qc>PkH;y(asn^%?_1a)orMEy1_EhOz7n2;MWwQ(D5CAO^J|ZAdFbDHT0WcUJp0`Vsdbds>{1PDO^I zg;*T0M9`i1Qhb&{lss{6fe8vIG%qiT;RHYAne)s!=d1^<2W=ycboZbt((GyWXuF+z z5h$&#tWi)y4sko)Zs=;!HS0IHmN2XN9fh_7ZL`)B+#B2*nKvii9Dnn~sdo+{(}l4} zKa=DVeB4)zq%(d{S(1H@UPqrU*&b*VnnP`=b<@U^`FHYf4jr88N2ZDZ!C@2Lq9N4; z`bmQIvM#~qGyBYbSXTnJAQYW3G8T-bqnSu9SU^GrxKN?|`}SA1@2}jqD)Tt;II*Pd z$%-FUtbedZzQekWtP0fxYqK?jH3~e#5=hW4po+ISXhU28ECOVVT)stq{2F`fcLc1V z1@<;-iJN1#s55Hl$NR&;-e!Qk>s@3Y0(y1@;!mGzEqfgn&Qf4}}1yjRjM}0()x* z_O_dB)>Z4OUaEYo0=YDBxuK*xtQ;-Bu~^!Lc5kN zs(+A!p8p{S7YhnM_#nIRV3tsD&w_d90=EZ0Z6T4J1hWGG5U>Su&$2-7q5mZJKpy10 z#_dJ%G|>lat-|dg{Xp);*w3Ob=Q4xoYvkV58}5J>Fz5)gigwWvfbhSlC*eurz4QpZ z51u{$9HaCspm@q>^*==Jq?qLgxJho3ouJ2waiZ4~cgF2;Z;DAW>2Nlm%7=Qn0fZ|; zz($csx#9{duNOhC!Dw9u{WZSU34E;`_}ZuWMJQYuH?4dm*Z5j`3Sp$tMPLyD>ku`@ z_XSdcR3SJGtS!bYnj(k1c~`-eaz$NXcMuDZ01gXaA({>L`A5VNa?oD17dw(wzAAnN z{~(Jz$UdT6N_-m-{MQIs<(jiC^nJ=8XT1<#BZ zUbPp90#oqieFabhq?OEO;lFdASYF-w>Pu5g<3ET$XuH4qp6Yv^eq6rgc|^YX1^I^V zyAyXOA36B!N6&t=K!Mh*?w!HCDP8-x_7#F+Nun-`Wl_4+hZXeJg97d0mowA7!K-x1UxIXo>aBJ3ai5Q*y=QE_H5hSu*K`f*pZ?A zhi4A=pNza3dDVHQPmDnaMD2y2AGQ)TE$ip_3kdW8tO4vOB}cZ;!oW`^J$Z& zLB}j&pG6ONXI*b`hZBQ|!QS+EU@UOJcZ7{FQ5YA6H(aN>PIph#q&D|H8hQ}B2MCkS z#XFwd{=|RCFw<*hAm)_&LxaWvLX@lQO`%^~TFz0#NZq%R$i zu7C9p$ZP-gvv>b?<>1xe}?hP+2m#=#QkwfM4?IkYhUFD0GNEgevjoHR2+obIk z>?2{09p4^;8Map#O+$q_7M&4~GDn$X4Gr#0cB902DTEfakV&GRN7s3a875uEaGLourJ&EU@yz)2?J(CO>Q&1Bt1}b zdbmW-laJ%y_pb2OB(7Z{M7cyXTq58{;S&9rlAdOj$5u+(hD+P10le3n^W@N&12V2q z$hfjXE#nR#Xn;ttIbkf@JDHeB%!OX&Ugl5G$B6}(ZVDI^EQ@AbK@C+;bh&}^LaZ*c z`+qJJfXp@^8dMD6BR{0&@00R(ad|5%Zwbg-5b5|wC7rDEzmvaG(#d+&rnErFUpxNP z(fX2Mw>MuYzv+|RWe}=F)IyOGgly z?42r1=l13ICuYNAzESt6W=xxF4b?_h2IcDn`5vEqFDKti%lFap{gnI#;yH4KZ%d>O ziPT2gqS~NIv~WPX5LVQpYf@?Iob`@6Pc69#-{o&eH>CBw*f2hv9@#gBjU%y`#zH&aWF=2m#xbN!byYHU^QVD zw2rJ|E4e*nHMR$EiR-!Y;5S!va{&0cmrl zI0*VpDr09Sm~k{&Tb!+4|T9rkQ>QF7A6|_QIs3zGN+1tNmrm?@Szpk&gzcJe!YmRktIv1jM zTCHvaYQ)><8Zb|@P-^NSQiew}JZO0@Emy*Wm2VYld<`*80WG-q zy3YSA|F41v!(xO@VLkR9d%==3=8Z9vU=8eLc6v4=_#SExx0l@$t0|kej^8=Y+KyTF8;-iCeS^Z_XwPeDgi|c}7${PF@LAzL1KEgBR~r{m?V#Q; zdW>ERs35Ig3kDDqqw%$i-BCl{4hjiukVP(UhAu+N;Xxf_-V0N%IR2!)y^>C`wdo6aS-eV`7K*OSf3*zk@aQN2+bX+@C zi}+=Zm2ITlO3C*!^7mNzyKEcTjhVe>&?XpN9_SZ>P)Q6^M)kiIHcNMDqm{EUrlV z!p5*Jq6uq#2Fga-Aq@}|n2`PHMcfbzh&eGQW#;vQDP)a-0wC^A;swtLae$hk4=^+Q z3^&5(g?@i397rMo02Q$m28U`ph(uAr;Zugw=LGZ~;yVJwDMGpq9;9?TDcuhbTKacd z`Z^rczs`Qfz0Br((V)Ut3o%6$39w zxO^udEhyt;)Kn+YPO3ZxyBn%F+9e_`pF7}&uzD}R15ZfzJ|UGak#5`d_Kx=|e@X~A zydhi4p0VX^c~6E)&=Ef969GY1;9~%g1<@z^phpNIXh;AU+(D-wR3ld|md^*udfXG( zA?gr0O%HNeHp6Aaj4vJurNt-QS;Cz|v%~uZ*lkGI9xw`^ef=gQe--jQ=@;mg^fKs#gg!{dN}C8!?Sh|; z+wQe7_Mk0b4?AOsBj$>D!XTIgw9*sw20cOfR_R&!Mrp;}(z@;g_7OTq<@ih>Aw)py z4H980gBK9zs5)00tPSkpc4JkbiSG0^y7XpJLuy$~K(o+Qfcw<^GFH*yeuI`*^nxSUtgY+Th zH2W6(Fz~7Z9YPXx^cgk@p_mC@Fd!gd0sN~x0dD~HJ4N`RJo>);_4njk-@E(eXHPCU z{SfGEywHSYi0Y%7s7BN?W`wpwwvnQ|!%!U5kPxro@nUY$fm=9--{l8jA&MbGm%mop zE#bys`JxX?x+h%00gfnf1$<+m{HCYMZ>lIWk`N2ysnl1s)K=A2HC44&fo}mQn^b$% zTQn=XSJu>SaCBlu!39x0o+Oq-6l9qcj6dyuyY+0<&o{lkd~W&dlJq_3a>ome8(-Y; z;-2*#>wY3XStCD(%4@vxeL4BNhvk1mrsPGhKKS#ehE?O5Y2&nMzxx3D3*W`TvnS7- zIDCHclgUp8K285J^Q-Vj!rM$F8{97<`^2mVl&((N0Z{CMWrKU8_@m5S*}KLcc=?g> z7Z1MlO4Tb>Z)o3TU*?X+=lYO0rjMW4cYgZ(;M=*2y}wBPD){TbJM7!|JJ=hJcTFFg zKJIw8?#Do4e<*r4z1QCuLmK0)ed-CtuIeBE3l?z@)RGw? z6G=tV(ReJHPG;k|NIsOK^A05ENSK25|Km@ za9w)9S#)Hfwb5q_bcdRQEx~4g3nDgi8ep$Lc4qTHjfMZ?HSMN%`YU7G;#n4c*dLb0M!6=!&`>eoM-5?qQqwx%0ar1>!tcgBnS>@J}?&H-HeOh zP3(5KOBk<73`Yauj-{04rukxlwuy421o-4@hthWq}>i zQd*)5@5Z%Wo#jQ{#?H<98g)%uU0Yp4`(98ROG`_j=&5y$`a0_O@xeQ@I zxni1xHlaxx;?{&}*gY1W2v79%j7OpAkxaYN5Wl71TtJ#BIF|r8mp>kfC8Npc0?x$^ z*b1sbJ5#OOpsiT9?C!^qJ>{RjQrcSbbHnF9E5E6wtb=OzD3*0b4be<5*pc(llPlmN zq&0|6z*9^D2%gwxx|&(d$T#!y6VMQ|7J6b>`FU1ePRiGLYrWkLo!Q_9V?+QAy0cq+T6GUL83!JA88Z zRQ_D#&Ae3Rly0c{b=9v|oPY4pgNNiV^vbvK1`zx~#V5e;7VRKc*ZL8yzmeMkp=S49 z$-tW6p2ALGV4DLw>M7wsuo%cC;{{j~a=r}vTHu`XOvhQ(!9AlJM>mC6*`7g6PuEne zty^2S+OkSq>8~7aJ&lyhPh4`9v9;V2ynH(+m+|sloV*wwv|LWfw~+EJ*lL#vu9aQ6 zRCTX=^ptS!%sCY&9ZT!PwkcaL;0IVQ*3AnG5U%;Ye^GkW0i1p+XSeI`^7* z&U~ikY|X*Vqbo*Ngr2iMjToPsZZa>n}ce{E6d>hwcFz$;fwXD{EKORISrLru(5wzK)S^N60J1MT!y_ zii>)x5pTkqohnn;-YtRpSM-y%8TVm8&1S@jc-$|<+yLY{bM`#q3By4tK!k{K`Zek` z>K*zc<|Fz7eVUntGiV54q#{5TF`kF$1dku$)scWHWQb@*XR38@%fL&c>nB!Ct{i@` zxV5-7-VP^X9b+X?5)_8e^z0?vgbRU80yC#YUi~~-_GrhC@TKXc>8*1u@7jj&NnyWl zCOa?@m%cnE-C~m#t)1%}Y>w5&>zH~=leI~uuC~BTZv^Zs@V~Ay#2UasSE=iL^fa|^ z;VSnlSGk|)wHNF?hGbU=vGWcVT6rPZ9t6}lNT`**R^AiX{Q9=jyN>QUT6MJgsHV@A z5(0c68R!?0gpd&n0jX^6v(qnMIB_BSTZi;zyY$d*>0h4t^)tVg|Mj^1bVB}K*N}w{jh##s>#=Xjc@@1g67Nk$l?M$AEGf z7jeJW?+GBF{?1tkAP+E0!X{4vU@=SIH5YKn%v#Hwr8#Qf(_zi zYbsYaBJxj8lyuSgJbcAa(naU8x_r^9(ylskefgzdlzP?CLT-cf%`(sxfibehW${3l z25u27QA3u19-ruNYB)b!7)lpniIEBT&)`t7mrUD`j9t`v+R-{el`^D^{iaFF$BpM* zr=llgQzO~qx#Q&duAdo?w@&ZwtLopD-|1^B2i_09>w3E*=gPbDUZmhHP(6GMdJE%Jf{J=Vwg^N5L&G1?PEIpu zXdp`!HCwxNt@>7-s#}e8t93R#-N~5)Si~Ep5()|eCc0Vspe79tC;nEp4PWotrB>}# z?X~QpD-n8Iq`IduryA&gVg0JC{3F3v1JBQ=4+zdg@cnAmLU8yJ;he$Pbq~Xs?0FRCq5G^ai$&-Su|%mAB%c-I6Pg2s6O z4~(>lmzQ$N<2hDd!!JA-3u(ul2<}9I7&17`R-3_U#B`iSR3{8Q&YWumnmYU z+WVSPWfdc;WE`;YR^| zPeFb;0RCY%LPi#Hd%T z2*#u-PwZp+;^F>qpE$@2vprnam-HpW!4wC8I9JlOrTpgaU;dz^i_4YM5cpA2Wc13D zSJ{%j1!!DdQI5<=4Q1g1ku#=^>F!8}pb|75qtR(Y?QW;jXt!BCHn*Ct_b*N^79S!} z;9p!6VguEt4H`|sImqYnQ6V9b*ni?TznAX&y>$P7OW*&S^n<@j4_*Ar{*%dkY#}zy z!K(?4LrI{=ap+6+T48iy4a8F*&|3m-ffiUKkhORd?~ZsP@$sp1r+)jfbQ@f*zetNA zll14(Z6`jQJeePPTe`mZj&s5>*$`Q4TWpeVbjf8TBHuyCUuGd-q?K*wyLcTB&C8sg zGt*#xOoFGO&D!B3@zFgO|zlF(r9UgX@qq8O+B6iCh5DV(y9LtDLuqU5Ao6u*gvy>r2lJy z=jNCsxI|>gj3?^!cg3t};1h8W1gEm8Ofj;L4=Qd1%mg>VD)qw{I&3M}{1&ItWz^~6 zcXb)sybXxAf!e`WaJ9@%dJAm?D1S6gO=vCn zqo81jhAU<+em>}rhV#+k#Lvb*;4i4&Za=bm;4UQhJ;&XRVC*%xS0@k2W(9j)})(*1EZmv07ovk`v`Dq0* z-+O7ar1#Y2cQ1Wh+G#OUK7X{NZ{aJKrX*v&>_?-UPBeee@}c!(;b+`y(Q|niVD)yVRwc)OSN@(%5(rw;OOzX5*K(%fH zfvr{Spz7Q$-bQ>cRYUF(tK&#@tY)D0!0L}0K6Cxq_2=*(Mt)cPX!`WA$)gh|GN-uH z+%d;N^_+U7KDz^2g1{~Zddql5VEvpQaJ3{EMMJi*jn=x_on4kLZI@Q9)97@Hs~>@P zc6GWs1f)~Y#cc&^58WRf27?OdZsP?Gig(V(z^N7CF8`pUAN%EsOMffvohz4LRdnH{ zT+ko7bjSQ;SyfBpuJ(29>-4LgE1WCn_2HMmQ`n_wGePtc^oK-{9{Iuo*mezEEPRmt z7|DK|{Av6{P{H^?n+#?fPB-{Hfae1u{pLzp*`PaV<#ephu5It$+4z#S0#)NsmPE7> zTL$l^#^Qa$>E3MUDEH`XVfx%W=IT|s0!rcXI92-yePi97= z`}xD(Bi>0%UzZ={!Rrs97I0(2qYQTkv0mx$0S&hz=6;FU!favXyBPUP(AzC9W}g_&Th}=!Ig_ED<6c%s|C-33pot(TCgiO30L{)J71v!($ z19DNFP3>y)ws}=V2U{g{BHUy%+)G7CA1vn-;l&*! znC+2Xd%X=*s!8#AxE(y-qZ*W<&1 z>%R<+!^_|f$06I;&BA$>gB%Oxn9jT4tZNCnqflHY#AqxS7DH@+41%84M<9yPDoaDz z0PcD$+@Xp*D+u>zOo)+D&{+Av$B|GQ8(MZEmwZ>gRN_e*E0@=n7Vx>WO?v23qHOnB z`)ipuvTsfwJ3INo!B4Wk#=tR!3G@BIUcAQ=vjxqfP6TKXLJJ{znDrq38gHGk!EJTf z?G8I=Ld>L*w)pH3d&C9ZOp2h62tlMY1N;kAfHZ@Eb)SNX_;4R+;UJO(qt>~1wW&6F zoTLjHe>?&jM1!t?$FKMw5&;Scg70VqE83GfUmL6T=*%XwN$>3TZnW%Gs~gny-qv_W zymO!Xtr%GR1v7CZ8w-P9DK`@z7)T5xhS(w7ur<>j+yU*OUN|)+cC^E4 z(s>b`2a*a?=D0HqG8KR#7vzqL@h64D{qDxV@4~fo&obR|@_BO2NX45-^NeMVJ;EM| z9_pRzog12*7=)&0Mf0&>VFZ}wHUD6a;E~VYyIfONTf3vi*XlxPm+HNg9JH-o|!<`(G_xOV*GzgbFhu9z~A>E#)@e8Zjkp zVAQstlHp+062M%UAxO+aU|z2qMXo#w>6brz<)tIK1G*VvBvDB8=xgQ*c+s6Tg;kLbyu;ApwApNCs~J3$m0@$j^5OPkun0&u z%pWZ2F89|<@zO2_>M)0IIT4CN*nqTs40)dgzuiMC| z{owMQaD)B;VbWqE774))3zE_Z69;eQYjR;|+aX_vaSp__r^smvpaeJ<35Y|mLBJ3s z3~&~3%y6c$L1H-6lTYT8eZfHvTt`_*(&Shl+UYkZKo?x&8rdfVCU=HrfC>c0rOmhSC(G-)`OPdeQYFwvpbn5Vejk*rX83U8ERwgC8gm2s|MW zArHW=PQ>S;9hjZ;LJE-w2JQugq#I1iz>x|hyP*AGWr*#$9h@JnVBxuu-79t>VprNY zXhkmly!=L0nWGy8^K1_kpoGc%0D3=lKLeNqydOM>h4DO^b|xHgiwMeA!f3H~xDg9( zUoe42NetZ32n;+GJfPXL&`=RWReUdf4|NZv5bXp2S+EcxEGT*4fQ&}~;~8;AO zt1<1hwK-coZ8!|BC0HBNWiTiKLb-ej@%scH3j;bI0ygPu4(WE6R7UiZMWUaAIC_Ot zLOCFFSu+G-n*DHb`w%avoYKimq7WJIgH;8rOd`O}3riJ{L7*A;G2#M(F7ST|{x<$8 zlKA=9XD5G`8t5&Irbp7_kx3sMJhIeg^w)ba6? z@mDaoGhlN@5@eWGOgDmJFg!3c5+5VR$-E{|4eEDLXOJF{97C!XNROS6d1j_`2&k^x z_&Q$AxAQIG$3ypo<(otD&7us7>suN5R#d)Tf4||rrf0Wrd1>?J<`=CM)-~+1G@=*G zU}&cmO*{aFv>BxHW)yZW@YaJ)rxZC8IoW@qc%k_6;8;AF4&+&+Kxf<#TwoIQL^ok} z8|?;Dr^g7~ge#8qv&frDknN5Q?W?Rm!40II0+FL&4uPv`itkIlvTyoO=H*B+Ff5Mw z#+X4IDj8sHNyJD(AK5PC(nz`dFJ)Sl!3BnMen-k%^c0z)z*y|%#PQUz_++jC_8`Gt zBCB+6cH_<9NoKXL8(%r}~ z1$Vs1gSfper`@Vkt5i*`jjgrX-L@**4slz5#dzbK^@I(CZOLBDi(w)G zCB@pyv!$K&@($^@7s{v(yT04rZR>Ei;;lpnP!rq-=MENFO{#OVbq@npmzLS*iL`X$C`BhrJ zh1n$3`86<1ARl!@;JAx-LK$H|F5Q$a8||Q=Qs2`0qJ4FsO4P*Q7S0O!a9;=<8Gus$ z4Iw>fe)AiD!zm<&aQBxKuUvLjD4vGOj!wj*56nIr5AQc`U3&7Mv!x`B}4iLjm=;-=A|4V)_ zoKITAeZJxIzOw6X9ZQdS(r!BiBE3b6G+f`Su{nB8;>XM$UUfP=Zx zV4MtQ5lGia1`@@CL%k#7sBg$!)D7sf9g${8>Bbo%Tpj2JLf#t)M6=;^rnfkR^!D}T z2QmYh!RV0qUig%H>V=_oi6^P=k>B-v-~K4kLFrk@6ys5dM#n-Jgdu^sI>=O#pi*KD ztR6SI3{Hd9uJ>%VZ^SqFs>RN%ecUk~9vbfxcrE}fLEsr41_Iv)H5T*-fP(ieXb}p+ z9yAO!F1m2wwql@hqd0llio$wiyg7fls(54x&BZtl}Lb|0P6u(aoRlxw%t8=kLz7@ z+(jc`)7xrXgG05AJ9I0IYb@(=a1I0iUcguss}QKD;rt8l6e?=vcY-#a_wX*H9O)p+ z25bS7)8No|nd+>ymL2X*bRE+XFeZ&jswXidKnFCmlYyYaFOqrpoZ|vwKX1EWds`=c zK{uzh=qw#CHr>_!WBt?Uk4Pnc1UeD~RH2W;j)W~^;0<;I;xL%)I&U*t z;iiPKmmDLjluCLInGOu%Oa&2_4#g-F| z22GR8K`4q&R7s47-O@yBXrz(t3R^OkESB>Z#C#?>97_0uqx?DkRq7q@N7%=nx6yOx zS#;R#>kJsdw|7YxQ>h0PBaM`J)L(Ib|j_N*qM2|=5(ADj^UZt&;#BOx)KO7)MOIe6%m zv5%(TalW>GsHw0wzCE!$xRcxMdy;#GevYYRws4ymyU!KyBG5F3izKL8l{}y;?_F*w z>B4pOt5Ruq>uN}xFDY;N`EGYc}_5 zt5RE{%K{Gv9_AmRAEDHYUZKdLe)S+qPlpfd(fYMcy-RPjb)p+B8?lYzR-rQki^OnY z_%NVL;J6n6PrLvXL@1C1Qv&)b`nu~)&k@gI&q0gym1<_0{N=heUCYjo*JG2u;g!C@NZ(X~73h9o*!x*!WVxs>L#EwnPnt=El1MNahrr^T*hBZZ`#o7l z92^Z8y-niq(LB%`(t|b^KLfi$Frp{6?GY6R?^hNS_R31yfganxq zG-?AnjQSDicZp)Kjt29&OfKD<0K>g#)*lt%eUe#c)&*`{ZN3IihtcRoA+>-|XixaF zaL`xQQV-xU3vGKsvtST$uzL_-yNbYemCha*ot~RO<|YoMk1^BaxM8GmxS?l9V*P-8 zYeN14|2_VDx_)Gtu! zyNJBzN?S>ngZZyX-!5$#<0-$9DeZzFeLPdz1>w@Wm$3P}%DVeI;%%&+c6e|*W)IqP z;5GsCgUrKGTS;1RhfGOez_~I*k3OS`YvSPPU@|mwT|MSrbI~z0oVtPnX-sg zPL{2vUx>WeYX~}7uvo_^1KY}2d5zd6wuL(4T`_gWQDCRVp=6=A(3csAk1kARhK=Nf z7yx8GE2mz#B?%h2I`B@JV}n@HTaj$;?HtjM>W1iI>gc}lW0}`r_5+OVPqGmLGCr+B zH@8w)lU+A~I07!1BklmKERZb%Vj~pr02AJY%-=9rMzZbpicU;9O@ikb^p1O>PkxVl z^OFxYR9CfC8Y@kk*%!0&7iOM4U;W!Xzjgd6EnSbq{-gN*zPImI{&Q$3-8t0WZ!bX5W;zfFMk8Weah(RA z043}mmQwfxp*U9lu)F}K13mC(*k**Zq)5U;R!wzZm7XMIH4SX zzjEp4^C#y=%GzS)92hMYBZdC#Kx)!IN6e8E&H-b_7I%Tq3Bo& z>O;%fZD+PkfE|4NVaJ2@_toFGV##-xb(&O`HV5cxZCZ=g;?R?7x|(jLyAfIoiN!{U z`O}ge6eQ2#J18w}hPGvw;Dx?oaQlx!XZ-*^?M!(6cG8GYK#Mr6ovnM+8}%FX+a1;9 z^GtQpJZv5g4NUas;P9w8gs~a2AJV==I)LEPW4QEfT>2s|-G)nFCZ$_R>5H^f&Vq_4 zMMV@1uLlZs!GMmh`IV$Z~XL_KEwzVZ95kL-G4*OE1xmb5+IzHNiH z_{0dn2k~Swlgt9DmQUuQ*-$z;MT~2QwZrYnI<}3{pb*7rby~f41i*X{GvQ!c<_Oy( z=AhoM4|FjqZplp|DjblATZ!nXO=0gL#0w(u4(|`}rcDJ1(;b`kd6a*D)R$xH~ z_J)KzJD5(VQ@La=0f~=7kq0QjU3B-OeNM&BS5OR~;8n1hV1`P=CJ{7%psWS&#B^Rg zsJvdlW(QAT)}?n+Lv=)@qm!nh9j5GU%9gkQfV;Ir+GlH zYojd~L|H+Y6(otM&|2>fYzJ0DwK98!u9SPoGZgC^&mq8{2udUx6QNQrX^TTzZIhlR z_T&3OTMuoK?x3!Z=#9bXiB3SwU>1&vNY0Z+fi8jwUZ9)62+9nsq*uuT^nxufDz=2} zN$4lcq2ilN<1YkO;(X18sfeXV;fwHBdXV7Bv3 z3q9Qw7;Y+*6vVJ9+&2$PqH>?a!3+GzE_>(PyN_p5TMYcBVSh-348L^I_+a@XO(s`ws2fkL=rjsQ+Z_ zSJ{uPZ*M!&SllIUgyRAR1{?q~`GA@OQ&@Tci$Q`NWwe{MKIVlizKw_VZMd3b2BPcj%XI%p> zS~Hw@r#5idAuD}Xc$ z8N-4%XO4JHJs=!8&!@5<`3U8(j&dt4pW!LAO*oC>(FjQT`H9!>0^MV5hB=#%p$=hznJWB%B1Q1V(#tCuLz&Ob4Ul zVYj2LQRo=eMC*RQmtlPyRFW)!&Tv}@a_yWe?raZn+;gB^ zWq|HSJeM9=1cv~Ska$4`XBJ4X>7i(3zU=bzWz4VW_n5Qve&!f`j)_u1rG3pKC}KH- zp!N|8l1{6@iQb(?(`-7DLc+-mut&YzFf&a}QRCRKd&r%#aR&c`!lRIY`xJm(_tKpq zV-`rX0eq51hN!Fq5^r>j-l=oxboN$!H`xjact#hhgS-hNr6OyIpO8PI9;F@?D#C3D zygKL}P`tF2fHIhewk}fxT1iz>d)X#l1*Yv@FwO=AAGmr0uA<;jLIj|)9%hgpV=vMl zGXKGRLcLBMq%u?t6bM1@f)lMp(82q`VRb;7qL!?$DD-F2BLPT^h3HcV*A+s3u;@zD zL&)Xw`A17y^-Alno-gS(D^*?%mUfqYx*WN?v@?1Bj!XYK|6*A;)6FQ!pfDI6BuRs9 z1O;$|LQ?`o3C&0*cektCYSZI7+~7B+5Pi?1J6ftc>KsPG4waN!vA~FdRw<2U zoN)qz$oUvhNr)eAl~6Dig&Iy67W=$;L*5uvV@9@^t*xcnw2|6GRV3Gs zAu!<>lC}jM3#uKc0SPy02a8a}Vuz-n=?QaTU`yq(dAb~3xLQCuM19njwPuMzuwRJz zqd}0-hM@go_T22bgJ*fr<_Nw6r)EyhoPvoCIvu2EAe;dW28r`g=3Zba0LZMsHx6Ru(aoA&F2hDc|+ zKDHy=E~qF)8`X_!Ty@4KPn%F5Hf5M1!W09=n-}VCmlJG?JWdn^_eigWgivl?DRP_8KbkR-vz2>&=hPFLO+uoMymOahY zs(MX}y4j$%8XacT0T>2ES^&ibikTqT>Bh(u3Hi33f(yC4XTG_l)BV!hkPlT##r@^8 zmx{DAsl>Y1@;Ab4->8;|~WqjY13bbI$-DCvf*baPg^ zX%b}Pe>(lAcmCtkH;_+X|NX?@g3=RV=?S0o1SKt@r6)+~32d6$M@>`H*nm0(?i0T4 z-b!z!vArGDkcN;sk5~$JFvEnh7?@j7Ba0w729=~kX_I1H%2ya0*-a4et$f`G4(OnE z07GI1_!;Om1vwv7ot!7=QgX)LlOIpZ8(8pxplp;48mv4V@Nh8_i4owVK!so^yg~pD z!HVCzqO(+*Mu2>Pgm!Txmdl6oA>dnFNWq!Xh1&!>3!YqT3t5Zr!go0vOr181*#mnc zV-2Bc(B}%I;LW*XEZ#;m;GRyDD~9?5f;Qy?oE|mghCkX_mQ{`kzI_pX8QL zt~oE?^oJGF*E^=&v+SYBq4561SVp?;-1{HS{(km%Y3U1I={CR%@%b6aPi=o)%;T#n&piI#Y+k4f?GV~hYMF|Ap^LPGwsk@4QgN+DRMdbViy*L#G+1)eer;PKh@5-ueEXb1z9C z&djG%`D@O}d(Ly-^DfW+E_u&;dxqTGo%Pn5cdCB>_-|f${k3=A=7;QdhpStw9xvZf zv3*r*!PbIJ+3V)}D_7PPG%w#+(3Bs}3+FX0+qBpl8Q1326jqmo%GTFzXx{Vk{@p)m z<_!I$(0?@V)fw%Rc1^0he(p67Uw+3e-=2Eq)Cmj5FW3KL=-x3strgy@>Hd_gsL$QL z)W2;>+p;}5d$M;G>|FhLNn`DrR^-QgLwQr>Mt+=XLmhiEZm9{=fe8odma<~BYRT%% zl{1QFtav1UR$yk}u|R-L4^~N|nA2dpzoIH;H8hN3eq(t#zmCm}4F8A@N@%5*7hYX= z{jM9@?$|fwCnV8|agy?VOX+IbGkTS@1peOSo^WdU&Xqg!+5&Ca+ZMOn{lm%IrYlCK6s%>f-J0CWoYKjwAIhCM?~!@)=VoUt%UHQ+O@0Mk+v*a( z))exU6g@^JUCw^>{rg6pF(q}u)_GgzZOPn{wP9KH{OU!)Y>wdK(8BfeTW38rbx6u=>^K{F@uMZEbJg_2kwajV<+!>ziuAH9HH!nd>sw&CE}q?LU8P z@`@FOg+)cVxho1~b_M+885=gNUB2Gmdt}rH7qexJv^%GuZ_ApcP1>@iwrwLD*05#G zmi}ATGzI*d@@&hRX=~;-F4dMbyR(nZJiPqX`d90pY74)-x!;yGQ?}mIe0vjH)=b`f z-+(P^0>7yGS@YremNkdjvZkx%wE}O;nwFLR<|4MNsjBDGwFYfjQ@Acr%XmDnJZQ!pHOJj=GMnO**9a8kh(wyeoW&()SS zCEk`b8ILdAy>$1Iz02ZT*6g4E+|2lvHTBn(UBi|&S7+SZw`I-p8!FkdX45HK*7RKQ z{wGpP%97R3FDc{4+mrR}E?@2r_;$C%zTI8d_w6qKBjDTJ%2mlsxY56;T(f2k6VH6O zTWcTgt_{@Wl`Q24++!c^_O|!5@l#x>^S93bACzRr-0Blb@`{vX`zcDYB0O(^k`yon zFo=?DjOEhR-rV^WV zSy;Uw#H369f`BGnO2D8kKIyV_$I_PUpWohoQ}NY)W?jB}TiUH@nsu2Rn{=7Cb)h%u z0w!_@{IgEeE^NITEcGue%?`z8UD)QNYQu?1m&PXkvB$JaVK}RnjpO+anMoIRP-DY{ z6?_A1vo5v#l*W$b{!ISK{;_Mpuiw996f6D~lrFB$*^pabw5hbEvVBv_?z*Se? zY|Jd3o0GA0E(;p@Dx-A7meQu``VIB<)vQM>ZeQFoqi)*zjDp3P%Q8#mHe@u;ZO+@y zuXNqiJ34jU(u$=k*-SkrZ$XiLnu90JFAnfuRX*`3$evM@R&is+4eM@fJASSNGog@{+1$Wo$(o%njxiEiK3n=J2sWPHlGUlHJAo>JIwrezc{d zeFy)12&v=B??227PkAz9U-rK2{UtBg{j~ALUEB6;+qdzRH9xNUaruvn5Bf`Xlx}5* z&8Fh@tm}Dy%3uPUvo-2H| za7Ru}`RS;nx{qvLYUgkHt2Zw_pNo7ib#p<(O4jGCt|{ZgE5=0p z50o(x%h&8fzdilNM1`}mXW5u2BkK_sJ7$(MCJHU`#zY(LTgRBFRAZtA-k7NH(W)65 z6YcQEMB9J#ZhTDC&X{OzEnmBy7!&1|EiCb`NYQX8yOiC?xt=^8;y2a#xQop;Dgre& z9;#cl!C$bqa&P0FmVJ9#4)96rr&D6%AU0&xIOqwDgXWjcTKtfWgVN_bH0PmN41+SK zFPyz#Cc~g*{s1dWSj=Bk8pL;L9HbvfDM3k!H?M42?tkq->Vp0Gd&*I|8vg&oskW}D znh*3?HqOU?{A?cU#?_51XGqDZ zNnwfq?A~X3y7dD$4?cAN)O)8FF*{bc@}6namdq`mTb8$hl||c|pM3V&m-iod>A=3$ z&0A|)gIj|QIiZFA&|`TSvmf#I>>2TwW<9(2gPsGUl3P+|luT=!`qb2?=7vx1$-MAD z_jkK*I$H3{j$d}Xuq)ne@z?BJVf~g_^jjv~F?sHE5pr|SzBg0b_@DG63+|kKOa2Wt z4~8DAE8fgD*DWk=Zf0?F^XfNO_gon4d7!xGhGPAY%GZi}E-QJvtX+$nL;My%m0DSE zadU8O$%e|NiiQ)5o6EJhdH=Nf{ObIY0$bc1WO;LJaq}WAZk|}ydu_1);$|&v?zgzP zGT^Vs3oTz;#I|*R_NUU1TAfpsyJq?Nl^YA%OLni_Q}gt?Cu`fa+^ei&*`S^KMsw<>;D@|)lr ztKJA633mERHnN4w`r@ZoY+J_qFZN&NbJ3;ivg@DJ9XXlot^h_E4r){Y&vDi7^mk8H7m+&p;l3sEy@fq%)b1r2x@edH6Mi@i<8N}a3 zToCwA+<(UNCkV@UKQG#xR0#Yo?~ZX_NH~}9dqN{2iS)l9Z0G%6o&VfwFxUdiUfZz}LE=i9aCSWrXv< z2i{8_iIneqkLS(Y5A!Za5N~)E?@d;c#p?uk>U)d$X$0`~{geQYo!(J~@+LmU9hoLl z20R~X{BEVZlfoH)@ahr&`v>^{_mTl&lkSqy3I0n4{etjF+Co4E0|GKgTtM9U+!F>5 zNpBO648;Hb!FM3q?K=QH2civx_Y&VAU5x*dLI03+4)0ziNCy3bWN;0EyOkwA5fa)6 z=u$!^&)9)@=0h(Mc5#P~Dlhp3;D`G{LYVjQfZnDwM2{!@BXNYJ^@PRFow1XAYY5}K zGNkvW`{SJ(@}hJ89o(0Y4)}~u17AR#&$%-{>D*9s)4z@Q9`4ZX$4-ZU(-3qqCFCwm zp^TI#DEsGx?*Ol+?Ayqf3j7-39>O%jSl+`+?C_eDb(9B9DQECZTIs6n!u0Z8kKV)w z&>NpibbdF~ebOaIqYmnc(V^>^29cOF&X?ePzC>T5OY|lAlI*?G40Y$aFF9Yv)T~U( z!SbD>9KG>9I%A>Yys~_}OEB2)5=IYG%F&r$TSwc2JM#rHwlE zfG1he^vF@uN;Hym6XR*4w7A%7SM*S%C^{xGI-12ZJ)P+H&5lBFvXfZpvV55&+a7I= zyvf`4xMbs2Cf{>v?c|tXeqb0{sr;SM4gxlUj;bc%-5za-?4_o?=<6Kl$uj%xCPgQ; z?d82Vc?e3-Y%8OiQh00B9E0b0JK}kF>JghYcY?V^E}+-UL2>3@xU2v>*WksC}-UC@*T_3hNt zL3|gX101rzXN(&j*-t&gDC-!{$H1a1@}~F16*+U9E8XHg)T7tv>9X{L>bhvN(u=7l z9S$A>6F4XSQCBzbS`9PtL{hHE1o&p{pUAgl7kn#>!g-+D*)Ka0Uy(lj>r*e3zMRq=g#Ob;zQC_;7-wDsa1xg z-z5z{^+5lWLS9jNqI_MH*9k1U(K~e7O6*XdfjT_?@SfP#NG;Ev)*UCW^tIJWi0hy3 z!_7uw{1}KsOvAe=<+wDS`~h*SAbFR)?@PYrujpK zDKW)(Hnot);usCYA(9QwlnoxDKo&Kc`a+Z`T~@vAc+*bsN~4TnlsCy@r2Ao7#xN;b z<*D(zvK85oEHmcsNRRx4U98_duzGnnbW# zoBWT`#JLMB+k+CXA}BF?BL0M3^+&<#RGZfzi_#n6>DeytO#dm*vwLBpUV+kgK^fAH z%s1|;O%NX?{|VID5$|n~E^bqU%Rk-f6iw0}VQxJSYB9cXHw)w)euUD*-EK+P=#>A5 zBc?5}-Za{C2;7X1q&kj;V%vS?slL7=lFgemEc^)Nsb733a!HJ1NRxK~%0lB}RLV=V zMy5nAL2o9KG60;-xj!;Ka%W^d(2da);7cMSA{DB|@HO8Qmq=gT_Fi_czLUJbA(OY! zYq8?DV&j~^I}@X{(p2m7LBI4se2cfcdMjPFT+w4l$Yd~h{gDySj^;)xq8;kX@HF1M zi*TAiZ6opiDIU(0srnnBHjLiSf!6jvzY#eaIT)$nIzRFu@kb+X5{?k|di1F@!+LQ2 z8|K+A7|VV|M+2wYp>}ywY+m*!yU=^lHOJm62Cs?S)D{Xa^%=N(wef82f#PFvF)n&f z@vB*v$TEjRR7yj%Hpjaanbx=thE6<@Ut|r++JDn zt9VR_vRe6R*@2CZ1$$}WMr+K5(0bzKmposAr0J2$<0!^>WE$kP=yAG9Z}|qNUo>W^ zE~t@J%bSVQ@+r^&xADg@Ot4L2Kb?4%5LSK=PKSZV;oT-tqSwlLBO|qtlAmNE{>b-BMxNd? zMpLcCozScPSWv4ZKr2WNwceg5ke4OD1+}s_O4549AE^%H%~kq$RJ9W17v&>k2e9c+ zoa;PWpCbOg?@l`TRcXL+Pg2l%$l6g)7Ajx#svS3syu7N7`uCe|%2Jh|!>BO)>`Ao_ zrNobFf9P+LXN;v4_Awo|`W0z4;=^Lz$>gszjISv3P~@OsrHfrvzel@Q(s6pYo+RvQ zDf?aI+hq4xEgG#Cq&F^7zIsA);JDc+utqnUw@O0~#^C=CS{pD1ofyXW{6o`@G@ck! zbBy)Vx7p!I0Zy&%vE-%cA*>Z9H6HyjZph9X$ud=?0U-?Z#(1# zmD*P;# zC-rV(dae8?AEeaCYovKU${>eIvfd-{YK24_qhMmCW1d$LC#3Q6f6xget5aHWpnsbg zFNxOT*B}=lQq^w32MVS(L%2e z?r~GSArH?%{SP_Jj!f5-L$+_0OnK5Q>6W!_+{J(4C;6(KVjRH8DDHjK?-cEtj}Ywf zWe!irEk1o9<=zwNvM4FVa5vpDNoNTMG()veJM^c?2NJ8EvGiNx1jeAmdOdE!Fv7%kjdzvADT;*|62dci-ccB- zm63;5dl+l8(O1(XQbE(HhvbhgO^~irvUsjJ2VtYo1umLp(EMANbRDFJ3zUhRNG<#G z+86r7FK8 znj!h`g;LGR2^(peu$KmyCX>UPv(h^oU7XAT(xy?*Fy+@fN)fi`s?{P7DEjZD{)42m zIXCL?Jc`%O$`_ufU->9Yy5P}Y498R=Se;zAp79?e$ukTskOe_MzF6Zw;b3zX^mpGh z`j3!0Oz*yy+R$=x;kD?0%G+qvqJF8_j^SftQqq{ca%VPgu=>e0&MV3Cn&+Z4({kda z@mMgee24rmFxH}SOwwNe4^88gFwd+=t0qynd`Z$m1ZMIc^Sz;Ua;m@++2X?QEH|3RK~QhWa2 z(*WdV^0!ie(G2XLp1$%KR+6^fGUh>ko^}g&1#A7O-R7K0lLgMQ2>NRM>y5+6&y#;T zDa6B8EMg8Ekblt}faar9;D0BS9_Kz%{RvVj^%(SjCn+@+qkpM*Y7_?Y1s?sHnCrdK(2rN7=*IuqOgC@Mi%{losRtgw70S4-VG1S5*T;R3q$d8#kKh*>?Q5n-)Ol~9$~2$>Z{YPitd%2H7UZ4u2v`?z4l4{; zW58+wKWhg*!I{8{gptI3(S6gM<*wqq&`osryNg`F6#}hvHSR{Y&TVj$U88Gu54tUG zkDKoHy60V%dyzBS9rgKK4&@~}Z$*MfRNwlBIFVZ4kmQnGiaUe)hrl~l4GlQX1c$T1 zau`^B%KZ%-Om!pNr`>1VXTkDwZj}2xJpFw&vRoO>t5{eYP$gJFLD>VOWdWb zmKf{Cxi7n~xUagexy#&m_jUIT_jhiByWCv?wcm2zc2`30U*kBFH5~ENLBX)tDc#U{ zj^RHN8gx3s=-4^Ns=vVXoel~viJjx1KOp#QlFS`95iTC3y8*3Km~NuRZA} zn*61819c!}fEEnI;4gdnBsv>E9lDzuJ3ou;b;=uj;l$~H&X1iJTZNaRv+>iJ_Kt;y zvEc8W38(15<7ycGQ8ut`rBKdoq1Mv$pD`A@lraFg8qF09XJp8V$((0;(xPV zDP#7`sc1ppdZi?T@kqYK@Mxl&9P`7*#V$sI-Z=Na@XpfLvh3()xn{Tk>3gSvq(H0WP}E;8s1 zps@!18_+m|egpJngMJJ26@%Uc`l>;10bOR$?|{Y|^zT4lH|QA9Hw}6l=vssR1L!(~ z-U0fKLB9vO-k^7ZZZPOQpc@G(?o8H!CAgd2AGqFP@BS0$R)hWsbeloPfo?ab2j~ui z-Us@gK_39!Wze61?l!0w=zfA9eqDqN#zIpH)O;Pk{7ufY!T3ge*DaKmN;!99>-Teh H-nst*Z1j_i literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/Getaway.ttf b/packages/SystemUI/res-keyguard/font/Getaway.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2796f64bc288fe652fcfb1d612b798ba858ad705 GIT binary patch literal 43584 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