Merge "Integrate requestScan() to RouterInfoMediaManager" into main
diff --git a/Android.bp b/Android.bp
index 019bf6508..5ada10d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -386,7 +386,7 @@
// TODO(b/120066492): remove gps_debug and protolog.conf.json when the build
// system propagates "required" properly.
"gps_debug.conf",
- "protolog.conf.json.gz",
+ "core.protolog.pb",
"framework-res",
// any install dependencies should go into framework-minus-apex-install-dependencies
// rather than here to avoid bloating incremental build time
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index 696c317..4832ea6 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -893,8 +893,9 @@
}
// Fall through when quick doze is not requested.
- if (!mIsOffBody) {
- // Quick doze was not requested and device is on body so turn the device active.
+ if (!mIsOffBody && !mForceIdle) {
+ // Quick doze wasn't requested, doze wasn't forced and device is on body
+ // so turn the device active.
mActiveReason = ACTIVE_REASON_ONBODY;
becomeActiveLocked("on_body", Process.myUid());
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index aec464d..e96d07f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -55,6 +55,7 @@
import android.util.SparseArrayMap;
import android.util.SparseIntArray;
import android.util.SparseLongArray;
+import android.util.SparseSetArray;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
@@ -158,19 +159,6 @@
@GuardedBy("mLock")
private final SparseLongArray mLastSeenConstraintTimesElapsed = new SparseLongArray();
- private DeviceIdleInternal mDeviceIdleInternal;
- private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
-
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (intent.getAction()) {
- case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
- mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
- break;
- }
- }
- };
@VisibleForTesting
@GuardedBy("mLock")
final FlexibilityTracker mFlexibilityTracker;
@@ -182,6 +170,7 @@
private final FcHandler mHandler;
@VisibleForTesting
final PrefetchController mPrefetchController;
+ private final SpecialAppTracker mSpecialAppTracker;
/**
* Stores the beginning of prefetch jobs lifecycle per app as a maximum of
@@ -355,16 +344,16 @@
mPercentsToDropConstraints =
FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
mPrefetchController = prefetchController;
+ mSpecialAppTracker = new SpecialAppTracker();
if (mFlexibilityEnabled) {
- registerBroadcastReceiver();
+ mSpecialAppTracker.startTracking();
}
}
@Override
public void onSystemServicesReady() {
- mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
- mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
+ mSpecialAppTracker.onSystemServicesReady();
}
@Override
@@ -453,6 +442,7 @@
final int userId = UserHandle.getUserId(uid);
mPrefetchLifeCycleStart.delete(userId, packageName);
mJobScoreTrackers.delete(uid, packageName);
+ mSpecialAppTracker.onAppRemoved(userId, packageName);
for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
final JobStatus js = mJobsToCheck.valueAt(i);
if ((js.getSourceUid() == uid && js.getSourcePackageName().equals(packageName))
@@ -466,6 +456,7 @@
@GuardedBy("mLock")
public void onUserRemovedLocked(int userId) {
mPrefetchLifeCycleStart.delete(userId);
+ mSpecialAppTracker.onUserRemoved(userId);
for (int u = mJobScoreTrackers.numMaps() - 1; u >= 0; --u) {
final int uid = mJobScoreTrackers.keyAt(u);
if (UserHandle.getUserId(uid) == userId) {
@@ -496,9 +487,10 @@
// Only exclude DEFAULT+ priority jobs for BFGS+ apps
|| (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
&& js.getEffectivePriority() >= PRIORITY_DEFAULT)
- // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs.
+ // For special/privileged apps, automatically exclude DEFAULT+ priority jobs.
|| (js.getEffectivePriority() >= PRIORITY_DEFAULT
- && mPowerAllowlistedApps.contains(js.getSourcePackageName()))
+ && mSpecialAppTracker.isSpecialApp(
+ js.getSourceUserId(), js.getSourcePackageName()))
|| hasEnoughSatisfiedConstraintsLocked(js)
|| mService.isCurrentlyRunningLocked(js);
}
@@ -827,39 +819,6 @@
mFcConfig.processConstantLocked(properties, key);
}
- private void registerBroadcastReceiver() {
- IntentFilter filter = new IntentFilter(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
- mContext.registerReceiver(mBroadcastReceiver, filter);
- }
-
- private void unregisterBroadcastReceiver() {
- mContext.unregisterReceiver(mBroadcastReceiver);
- }
-
- private void updatePowerAllowlistCache() {
- if (mDeviceIdleInternal == null) {
- return;
- }
-
- // Don't call out to DeviceIdleController with the lock held.
- final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle();
- final ArraySet<String> changedPkgs = new ArraySet<>();
- synchronized (mLock) {
- changedPkgs.addAll(mPowerAllowlistedApps);
- mPowerAllowlistedApps.clear();
- for (final String pkgName : allowlistedPkgs) {
- mPowerAllowlistedApps.add(pkgName);
- if (changedPkgs.contains(pkgName)) {
- changedPkgs.remove(pkgName);
- } else {
- changedPkgs.add(pkgName);
- }
- }
- mPackagesToCheck.addAll(changedPkgs);
- mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES);
- }
- }
-
@VisibleForTesting
class FlexibilityTracker {
final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
@@ -1343,12 +1302,12 @@
mFlexibilityEnabled = true;
mPrefetchController
.registerPrefetchChangedListener(mPrefetchChangedListener);
- registerBroadcastReceiver();
+ mSpecialAppTracker.startTracking();
} else {
mFlexibilityEnabled = false;
mPrefetchController
.unRegisterPrefetchChangedListener(mPrefetchChangedListener);
- unregisterBroadcastReceiver();
+ mSpecialAppTracker.stopTracking();
}
}
break;
@@ -1653,6 +1612,176 @@
return mFcConfig;
}
+ private class SpecialAppTracker {
+ /**
+ * Lock for objects inside this class. This should never be held when attempting to acquire
+ * {@link #mLock}. It is fine to acquire this if already holding {@link #mLock}.
+ */
+ private final Object mSatLock = new Object();
+
+ private DeviceIdleInternal mDeviceIdleInternal;
+
+ /** Set of all apps that have been deemed special, keyed by user ID. */
+ private final SparseSetArray<String> mSpecialApps = new SparseSetArray<>();
+ @GuardedBy("mSatLock")
+ private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+ mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
+ break;
+ }
+ }
+ };
+
+ public boolean isSpecialApp(final int userId, @NonNull String packageName) {
+ synchronized (mSatLock) {
+ if (mSpecialApps.contains(UserHandle.USER_ALL, packageName)) {
+ return true;
+ }
+ if (mSpecialApps.contains(userId, packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isSpecialAppInternal(final int userId, @NonNull String packageName) {
+ synchronized (mSatLock) {
+ if (mPowerAllowlistedApps.contains(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void onAppRemoved(final int userId, String packageName) {
+ synchronized (mSatLock) {
+ // Don't touch the USER_ALL set here. If the app is completely removed from the
+ // device, any list that affects USER_ALL should update and this would eventually
+ // be updated with those lists no longer containing the app.
+ mSpecialApps.remove(userId, packageName);
+ }
+ }
+
+ private void onSystemServicesReady() {
+ mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+
+ synchronized (mLock) {
+ if (mFlexibilityEnabled) {
+ mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
+ }
+ }
+ }
+
+ private void onUserRemoved(final int userId) {
+ synchronized (mSatLock) {
+ mSpecialApps.remove(userId);
+ }
+ }
+
+ private void startTracking() {
+ IntentFilter filter = new IntentFilter(
+ PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ updatePowerAllowlistCache();
+ }
+
+ private void stopTracking() {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+
+ synchronized (mSatLock) {
+ mPowerAllowlistedApps.clear();
+ mSpecialApps.clear();
+ }
+ }
+
+ /**
+ * Update the processed special app set for the specified user ID, only looking at the
+ * specified set of apps. This method must <b>NEVER</b> be called while holding
+ * {@link #mSatLock}.
+ */
+ private void updateSpecialAppSetUnlocked(final int userId, @NonNull ArraySet<String> pkgs) {
+ // This method may need to acquire mLock, so ensure that mSatLock isn't held to avoid
+ // lock inversion.
+ if (Thread.holdsLock(mSatLock)) {
+ throw new IllegalStateException("Must never hold local mSatLock");
+ }
+ if (pkgs.size() == 0) {
+ return;
+ }
+ final ArraySet<String> changedPkgs = new ArraySet<>();
+
+ synchronized (mSatLock) {
+ for (int i = pkgs.size() - 1; i >= 0; --i) {
+ final String pkgName = pkgs.valueAt(i);
+ if (isSpecialAppInternal(userId, pkgName)) {
+ if (mSpecialApps.add(userId, pkgName)) {
+ changedPkgs.add(pkgName);
+ }
+ } else if (mSpecialApps.remove(userId, pkgName)) {
+ changedPkgs.add(pkgName);
+ }
+ }
+ }
+
+ if (changedPkgs.size() > 0) {
+ synchronized (mLock) {
+ mPackagesToCheck.addAll(changedPkgs);
+ mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES);
+ }
+ }
+ }
+
+ private void updatePowerAllowlistCache() {
+ if (mDeviceIdleInternal == null) {
+ return;
+ }
+
+ // Don't call out to DeviceIdleController with the lock held.
+ final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle();
+ final ArraySet<String> changedPkgs = new ArraySet<>();
+ synchronized (mSatLock) {
+ changedPkgs.addAll(mPowerAllowlistedApps);
+ mPowerAllowlistedApps.clear();
+ for (String pkgName : allowlistedPkgs) {
+ mPowerAllowlistedApps.add(pkgName);
+ if (!changedPkgs.remove(pkgName)) {
+ // The package wasn't in the previous set of allowlisted apps. Add it
+ // since its state has changed.
+ changedPkgs.add(pkgName);
+ }
+ }
+ }
+
+ // The full allowlist is currently user-agnostic, so use USER_ALL for these packages.
+ updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
+ }
+
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ pw.println("Special apps:");
+ pw.increaseIndent();
+
+ synchronized (mSatLock) {
+ for (int u = 0; u < mSpecialApps.size(); ++u) {
+ pw.print(mSpecialApps.keyAt(u));
+ pw.print(": ");
+ pw.println(mSpecialApps.valuesAt(u));
+ }
+
+ pw.println();
+ pw.print("Power allowlisted packages: ");
+ pw.println(mPowerAllowlistedApps);
+ }
+
+ pw.decreaseIndent();
+ }
+ }
+
@Override
@GuardedBy("mLock")
public void dumpConstants(IndentingPrintWriter pw) {
@@ -1690,8 +1819,7 @@
pw.decreaseIndent();
pw.println();
- pw.print("Power allowlisted packages: ");
- pw.println(mPowerAllowlistedApps);
+ mSpecialAppTracker.dump(pw);
pw.println();
mFlexibilityTracker.dump(pw, predicate, nowElapsed);
diff --git a/core/api/current.txt b/core/api/current.txt
index 3fd67db..386396c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10733,6 +10733,7 @@
field public static final String DROPBOX_SERVICE = "dropbox";
field public static final String EUICC_SERVICE = "euicc";
field public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
+ field public static final String FINGERPRINT_SERVICE = "fingerprint";
field public static final String GAME_SERVICE = "game";
field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
@@ -18053,6 +18054,24 @@
method public android.graphics.pdf.PdfDocument.PageInfo.Builder setContentRect(android.graphics.Rect);
}
+ public final class PdfRenderer implements java.lang.AutoCloseable {
+ ctor public PdfRenderer(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
+ method public void close();
+ method public int getPageCount();
+ method public android.graphics.pdf.PdfRenderer.Page openPage(int);
+ method public boolean shouldScaleForPrinting();
+ }
+
+ public final class PdfRenderer.Page implements java.lang.AutoCloseable {
+ method public void close();
+ method public int getHeight();
+ method public int getIndex();
+ method public int getWidth();
+ method public void render(@NonNull android.graphics.Bitmap, @Nullable android.graphics.Rect, @Nullable android.graphics.Matrix, int);
+ field public static final int RENDER_MODE_FOR_DISPLAY = 1; // 0x1
+ field public static final int RENDER_MODE_FOR_PRINT = 2; // 0x2
+ }
+
}
package android.graphics.text {
@@ -20363,6 +20382,54 @@
}
+package android.hardware.fingerprint {
+
+ @Deprecated public class FingerprintManager {
+ method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
+ field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
+ field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
+ field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
+ field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
+ field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
+ field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
+ field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
+ field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
+ field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
+ field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
+ field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
+ field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
+ field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+ field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
+ field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
+ }
+
+ @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
+ ctor @Deprecated public FingerprintManager.AuthenticationCallback();
+ method @Deprecated public void onAuthenticationError(int, CharSequence);
+ method @Deprecated public void onAuthenticationFailed();
+ method @Deprecated public void onAuthenticationHelp(int, CharSequence);
+ method @Deprecated public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
+ }
+
+ @Deprecated public static class FingerprintManager.AuthenticationResult {
+ method @Deprecated public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
+ }
+
+ @Deprecated public static final class FingerprintManager.CryptoObject {
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
+ method @Deprecated public javax.crypto.Cipher getCipher();
+ method @Deprecated public javax.crypto.Mac getMac();
+ method @Deprecated public java.security.Signature getSignature();
+ }
+
+}
+
package android.hardware.input {
public final class HostUsiVersion implements android.os.Parcelable {
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 1b0da05..aa5e547 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -391,6 +391,10 @@
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+HiddenAbstractMethod: android.app.Notification.Style#areNotificationsVisiblyDifferent(android.app.Notification.Style):
+ areNotificationsVisiblyDifferent cannot be hidden and abstract when Style has a visible constructor, in case a third-party attempts to subclass it.
+
+
InvalidNullabilityOverride: android.app.Notification.TvExtender#extend(android.app.Notification.Builder) parameter #0:
Invalid nullability on parameter `builder` in method `extend`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: android.media.midi.MidiUmpDeviceService#onBind(android.content.Intent) parameter #0:
@@ -1461,7 +1465,6 @@
New API must be flagged with @FlaggedApi: method android.graphics.text.PositionedGlyphs.getItalicOverride(int)
UnflaggedApi: android.graphics.text.PositionedGlyphs#getWeightOverride(int):
New API must be flagged with @FlaggedApi: method android.graphics.text.PositionedGlyphs.getWeightOverride(int)
-
UnflaggedApi: android.hardware.camera2.ExtensionCaptureRequest:
New API must be flagged with @FlaggedApi: class android.hardware.camera2.ExtensionCaptureRequest
UnflaggedApi: android.hardware.camera2.ExtensionCaptureRequest#ExtensionCaptureRequest():
diff --git a/core/api/module-lib-lint-baseline.txt b/core/api/module-lib-lint-baseline.txt
index 42c4efc..79fbc13 100644
--- a/core/api/module-lib-lint-baseline.txt
+++ b/core/api/module-lib-lint-baseline.txt
@@ -497,6 +497,10 @@
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+HiddenAbstractMethod: android.app.Notification.Style#areNotificationsVisiblyDifferent(android.app.Notification.Style):
+ areNotificationsVisiblyDifferent cannot be hidden and abstract when Style has a visible constructor, in case a third-party attempts to subclass it.
+
+
RequiresPermission: android.accounts.AccountManager#getAccountsByTypeAndFeatures(String, String[], android.accounts.AccountManagerCallback<android.accounts.Account[]>, android.os.Handler):
Method 'getAccountsByTypeAndFeatures' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.accounts.AccountManager#hasFeatures(android.accounts.Account, String[], android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler):
diff --git a/core/api/removed.txt b/core/api/removed.txt
index c61f163..3c7c0d6 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -35,7 +35,6 @@
method @Deprecated @Nullable public String getFeatureId();
method public abstract android.content.SharedPreferences getSharedPreferences(java.io.File, int);
method public abstract java.io.File getSharedPreferencesPath(String);
- field public static final String FINGERPRINT_SERVICE = "fingerprint";
}
public class ContextWrapper extends android.content.Context {
@@ -146,54 +145,6 @@
}
-package android.hardware.fingerprint {
-
- @Deprecated public class FingerprintManager {
- method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
- method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
- method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
- field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
- field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
- field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
- field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
- field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
- field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
- field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
- field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
- field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
- field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
- field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
- field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
- field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
- field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
- field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
- field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
- field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
- }
-
- @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
- ctor public FingerprintManager.AuthenticationCallback();
- method public void onAuthenticationError(int, CharSequence);
- method public void onAuthenticationFailed();
- method public void onAuthenticationHelp(int, CharSequence);
- method public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
- }
-
- @Deprecated public static class FingerprintManager.AuthenticationResult {
- method public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
- }
-
- @Deprecated public static final class FingerprintManager.CryptoObject {
- ctor public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
- ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
- ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
- method public javax.crypto.Cipher getCipher();
- method public javax.crypto.Mac getMac();
- method public java.security.Signature getSignature();
- }
-
-}
-
package android.media {
public final class AudioFormat implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index cc7d97a..d9b5d56 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1146,7 +1146,6 @@
public static final class StatusBarManager.DisableInfo implements android.os.Parcelable {
method public boolean areAllComponentsEnabled();
method public int describeContents();
- method public boolean isBackDisabled();
method public boolean isNavigateToHomeDisabled();
method public boolean isNotificationPeekingDisabled();
method public boolean isRecentsDisabled();
@@ -10422,8 +10421,7 @@
@FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable {
ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
- method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String);
- method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilterToAutoTransact(@NonNull String);
+ method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String, boolean);
method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean defaultToObserveMode();
method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
@@ -11531,7 +11529,7 @@
public final class PermissionManager {
method public int checkDeviceIdentifierAccess(@Nullable String, @Nullable String, @Nullable String, int, int);
- method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public static int checkPermission(@NonNull String, @NonNull String, @NonNull String, int);
+ method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public int checkPermission(@NonNull String, @NonNull String, @NonNull String);
method @RequiresPermission(value=android.Manifest.permission.UPDATE_APP_OPS_STATS, conditional=true) public int checkPermissionForDataDelivery(@NonNull String, @NonNull android.content.AttributionSource, @Nullable String);
method @RequiresPermission(value=android.Manifest.permission.UPDATE_APP_OPS_STATS, conditional=true) public int checkPermissionForDataDeliveryFromDataSource(@NonNull String, @NonNull android.content.AttributionSource, @Nullable String);
method public int checkPermissionForPreflight(@NonNull String, @NonNull android.content.AttributionSource);
@@ -14204,7 +14202,8 @@
method @Deprecated public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsForPackage();
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsSupportingScheme(String);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean isInEmergencyCall();
- method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, @NonNull android.os.UserHandle, boolean);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, @NonNull android.os.UserHandle);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isRinging();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUserSelectedOutgoingPhoneAccount(@Nullable android.telecom.PhoneAccountHandle);
field public static final String ACTION_CURRENT_TTY_MODE_CHANGED = "android.telecom.action.CURRENT_TTY_MODE_CHANGED";
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 1923641..e7a0ff4 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -509,6 +509,10 @@
Methods must not throw generic exceptions (`java.lang.Throwable`)
+HiddenAbstractMethod: android.app.Notification.Style#areNotificationsVisiblyDifferent(android.app.Notification.Style):
+ areNotificationsVisiblyDifferent cannot be hidden and abstract when Style has a visible constructor, in case a third-party attempts to subclass it.
+
+
InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceIntelligenceService#onBind(android.content.Intent) parameter #0:
Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#onBind(android.content.Intent) parameter #0:
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index e1e9d09..cdf232c 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1711,6 +1711,15 @@
}
+package android.hardware.fingerprint {
+
+ @Deprecated public class FingerprintManager {
+ method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
+ method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
+ }
+
+}
+
package android.hardware.hdmi {
public final class HdmiControlServiceWrapper {
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 658ddbf..fc49565 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -511,6 +511,10 @@
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+HiddenAbstractMethod: android.app.Notification.Style#areNotificationsVisiblyDifferent(android.app.Notification.Style):
+ areNotificationsVisiblyDifferent cannot be hidden and abstract when Style has a visible constructor, in case a third-party attempts to subclass it.
+
+
InvalidNullabilityOverride: android.window.WindowProviderService#getSystemService(String) parameter #0:
Invalid nullability on parameter `name` in method `getSystemService`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: android.window.WindowProviderService#onConfigurationChanged(android.content.res.Configuration) parameter #0:
diff --git a/core/api/test-removed.txt b/core/api/test-removed.txt
index 2e44176..d802177 100644
--- a/core/api/test-removed.txt
+++ b/core/api/test-removed.txt
@@ -1,10 +1 @@
// Signature format: 2.0
-package android.hardware.fingerprint {
-
- @Deprecated public class FingerprintManager {
- method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
- method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
- }
-
-}
-
diff --git a/core/java/Android.bp b/core/java/Android.bp
index eba500d..1844215 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -126,6 +126,7 @@
srcs: [
"android/os/IExternalVibrationController.aidl",
"android/os/IExternalVibratorService.aidl",
+ "android/os/ExternalVibrationScale.aidl",
],
}
@@ -527,17 +528,6 @@
],
}
-// common protolog sources without classes that rely on Android SDK
-filegroup {
- name: "protolog-common-no-android-src",
- srcs: [
- ":protolog-common-src",
- ],
- exclude_srcs: [
- "com/android/internal/protolog/common/ProtoLog.java",
- ],
-}
-
// PackageManager common
filegroup {
name: "framework-pm-common-shared-srcs",
@@ -547,13 +537,20 @@
],
}
+filegroup {
+ name: "protolog-impl",
+ srcs: [
+ "com/android/internal/protolog/ProtoLogImpl.java",
+ ],
+}
+
java_library {
name: "protolog-lib",
platform_apis: true,
srcs: [
"com/android/internal/protolog/ProtoLogImpl.java",
"com/android/internal/protolog/ProtoLogViewerConfigReader.java",
- ":protolog-common-src",
+ ":perfetto_trace_javastream_protos",
],
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 251f823..57c67be 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1002,6 +1002,9 @@
new ActivityManager.TaskDescription();
private int mLastTaskDescriptionHashCode;
+ @ActivityInfo.ScreenOrientation
+ private int mLastRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSET;
+
protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused};
@SuppressWarnings("unused")
@@ -7530,11 +7533,15 @@
* {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
*/
public void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
+ if (requestedOrientation == mLastRequestedOrientation) {
+ return;
+ }
if (mParent == null) {
ActivityClient.getInstance().setRequestedOrientation(mToken, requestedOrientation);
} else {
mParent.setRequestedOrientation(requestedOrientation);
}
+ mLastRequestedOrientation = requestedOrientation;
}
/**
@@ -7548,6 +7555,9 @@
*/
@ActivityInfo.ScreenOrientation
public int getRequestedOrientation() {
+ if (mLastRequestedOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSET) {
+ return mLastRequestedOrientation;
+ }
if (mParent == null) {
return ActivityClient.getInstance().getRequestedOrientation(mToken);
} else {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 16c7753..39900a0 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -74,6 +74,7 @@
import android.os.UserManager;
import android.permission.PermissionGroupUsage;
import android.permission.PermissionUsageHelper;
+import android.permission.flags.Flags;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -99,7 +100,6 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Parcelling;
import com.android.internal.util.Preconditions;
-import com.android.media.flags.Flags;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -2077,7 +2077,8 @@
* @hide
*/
@SystemApi
- @FlaggedApi(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+ @FlaggedApi(com.android.media.flags.Flags
+ .FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
/**
@@ -2243,7 +2244,7 @@
*
* @hide
*/
- @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
@SystemApi
public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS =
"android:access_restricted_settings";
@@ -3432,7 +3433,8 @@
}
}
- public static final @android.annotation.NonNull Creator<PackageOps> CREATOR = new Creator<PackageOps>() {
+ public static final @android.annotation.NonNull Creator<PackageOps> CREATOR =
+ new Creator<PackageOps>() {
@Override public PackageOps createFromParcel(Parcel source) {
return new PackageOps(source);
}
@@ -7410,7 +7412,7 @@
* @param userId User id of the app whose Op changed.
* @param persistentDeviceId persistent device id whose Op changed.
*/
- @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
default void onOpChanged(@NonNull String op, @NonNull String packageName, int userId,
@NonNull String persistentDeviceId) {
if (Objects.equals(persistentDeviceId,
@@ -7480,7 +7482,7 @@
* @param attributionFlags the attribution flags for this operation.
* @param attributionChainId the unique id of the attribution chain this op is a part of.
*/
- @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
default void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
@Nullable String attributionTag, int virtualDeviceId, boolean active,
@AttributionFlags int attributionFlags, int attributionChainId) {
@@ -7534,7 +7536,7 @@
* @param flags The flags of this op
* @param result The result of the note.
*/
- @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
default void onOpNoted(@NonNull String op, int uid, @NonNull String packageName,
@Nullable String attributionTag, int virtualDeviceId, @OpFlags int flags,
@Mode int result) {
@@ -8361,14 +8363,26 @@
String attributionTag, int virtualDeviceId, boolean active,
@AttributionFlags int attributionFlags, int attributionChainId) {
executor.execute(() -> {
- if (callback instanceof OnOpActiveChangedInternalListener) {
- ((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
- uid, packageName, virtualDeviceId, active);
- }
- if (sAppOpInfos[op].name != null) {
- callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
- attributionTag, virtualDeviceId, active, attributionFlags,
- attributionChainId);
+ if (Flags.deviceAwarePermissionApisEnabled()) {
+ if (callback instanceof OnOpActiveChangedInternalListener) {
+ ((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
+ uid, packageName, virtualDeviceId, active);
+ }
+ if (sAppOpInfos[op].name != null) {
+ callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
+ attributionTag, virtualDeviceId, active, attributionFlags,
+ attributionChainId);
+ }
+ } else {
+ if (callback instanceof OnOpActiveChangedInternalListener) {
+ ((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
+ uid, packageName, active);
+ }
+ if (sAppOpInfos[op].name != null) {
+ callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
+ attributionTag, active, attributionFlags,
+ attributionChainId);
+ }
}
});
}
@@ -8613,9 +8627,13 @@
try {
executor.execute(() -> {
if (sAppOpInfos[op].name != null) {
- listener.onOpNoted(sAppOpInfos[op].name, uid, packageName,
- attributionTag, virtualDeviceId,
- flags, mode);
+ if (Flags.deviceAwarePermissionApisEnabled()) {
+ listener.onOpNoted(sAppOpInfos[op].name, uid, packageName,
+ attributionTag, virtualDeviceId, flags, mode);
+ } else {
+ listener.onOpNoted(sAppOpInfos[op].name, uid, packageName,
+ attributionTag, flags, mode);
+ }
}
});
} finally {
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index e6e46dd..301fef8 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -1448,7 +1448,6 @@
*
* @hide
*/
- @SystemApi
public boolean isBackDisabled() {
return mBack;
}
@@ -1862,38 +1861,38 @@
};
@DataClass.Generated(
- time = 1707345957771L,
+ time = 1708625947132L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/app/StatusBarManager.java",
- inputSignatures = "private boolean mStatusBarExpansion\nprivate "
- + "boolean mNavigateHome\nprivate boolean mNotificationPeeking\nprivate "
- + "boolean mRecents\nprivate boolean mBack\nprivate boolean "
- + "mSearch\nprivate boolean mSystemIcons\nprivate boolean mClock\nprivate"
- + " boolean mNotificationIcons\nprivate boolean mRotationSuggestion\n"
+ inputSignatures = "private boolean mStatusBarExpansion\nprivate boolean "
+ + "mNavigateHome\nprivate boolean mNotificationPeeking\nprivate "
+ + "boolean mRecents\nprivate boolean mBack\nprivate boolean mSearch\n"
+ + "private boolean mSystemIcons\nprivate boolean mClock\nprivate "
+ + "boolean mNotificationIcons\nprivate boolean mRotationSuggestion\n"
+ "private boolean mNotificationTicker\npublic "
+ "@android.annotation.SystemApi boolean isStatusBarExpansionDisabled()\n"
+ "public void setStatusBarExpansionDisabled(boolean)\npublic "
- + "@android.annotation.SystemApi boolean isNavigateToHomeDisabled()\n"
- + "public void setNavigationHomeDisabled(boolean)\npublic "
- + "@android.annotation.SystemApi boolean isNotificationPeekingDisabled()\n"
- + "public void setNotificationPeekingDisabled(boolean)\npublic "
+ + "@android.annotation.SystemApi boolean isNavigateToHomeDisabled()\npublic"
+ + " void setNavigationHomeDisabled(boolean)\npublic "
+ + "@android.annotation.SystemApi boolean isNotificationPeekingDisabled()"
+ + "\npublic void setNotificationPeekingDisabled(boolean)\npublic "
+ "@android.annotation.SystemApi boolean isRecentsDisabled()\npublic "
- + "void setRecentsDisabled(boolean)\npublic @android.annotation.SystemApi "
- + "boolean isBackDisabled()\npublic void setBackDisabled(boolean)\npublic "
+ + "void setRecentsDisabled(boolean)\npublic boolean isBackDisabled()"
+ + "\npublic void setBackDisabled(boolean)\npublic "
+ "@android.annotation.SystemApi boolean isSearchDisabled()\npublic "
+ "void setSearchDisabled(boolean)\npublic boolean "
- + "areSystemIconsDisabled()\npublic void setSystemIconsDisabled(boolean)"
- + "\npublic boolean isClockDisabled()\npublic "
- + "void setClockDisabled(boolean)\npublic "
- + "boolean areNotificationIconsDisabled()\npublic "
- + "void setNotificationIconsDisabled(boolean)\npublic boolean "
+ + "areSystemIconsDisabled()\npublic void setSystemIconsDisabled(boolean)\n"
+ + "public boolean isClockDisabled()\npublic "
+ + "void setClockDisabled(boolean)\npublic boolean "
+ + "areNotificationIconsDisabled()\npublic void "
+ + "setNotificationIconsDisabled(boolean)\npublic boolean "
+ "isNotificationTickerDisabled()\npublic void "
+ "setNotificationTickerDisabled(boolean)\npublic "
+ "@android.annotation.TestApi boolean isRotationSuggestionDisabled()\n"
+ "public void setRotationSuggestionDisabled(boolean)\npublic "
- + "@android.annotation.SystemApi boolean areAllComponentsEnabled()\n"
- + "public void setEnableAll()\npublic boolean areAllComponentsDisabled()"
- + "\npublic void setDisableAll()\npublic @android.annotation.NonNull "
+ + "@android.annotation.SystemApi boolean areAllComponentsEnabled()\npublic"
+ + " void setEnableAll()\npublic boolean areAllComponentsDisabled()\n"
+ + "public void setDisableAll()\npublic @android.annotation.NonNull "
+ "@java.lang.Override java.lang.String toString()\npublic "
+ "android.util.Pair<java.lang.Integer,java.lang.Integer> toFlags()\n"
+ "class DisableInfo extends java.lang.Object implements "
diff --git a/core/java/android/app/admin/DevicePolicyCache.java b/core/java/android/app/admin/DevicePolicyCache.java
index 29f657e..16cb4ec 100644
--- a/core/java/android/app/admin/DevicePolicyCache.java
+++ b/core/java/android/app/admin/DevicePolicyCache.java
@@ -15,6 +15,9 @@
*/
package android.app.admin;
+import static android.app.admin.DevicePolicyManager.CONTENT_PROTECTION_DISABLED;
+import static android.app.admin.DevicePolicyManager.ContentProtectionPolicy;
+
import android.annotation.UserIdInt;
import com.android.server.LocalServices;
@@ -59,6 +62,12 @@
public abstract int getPermissionPolicy(@UserIdInt int userHandle);
/**
+ * Caches {@link DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName)}
+ * of the given user.
+ */
+ public abstract @ContentProtectionPolicy int getContentProtectionPolicy(@UserIdInt int userId);
+
+ /**
* True if there is an admin on the device who can grant sensor permissions.
*/
public abstract boolean canAdminGrantSensorsPermissions();
@@ -92,6 +101,11 @@
}
@Override
+ public @ContentProtectionPolicy int getContentProtectionPolicy(@UserIdInt int userId) {
+ return CONTENT_PROTECTION_DISABLED;
+ }
+
+ @Override
public boolean canAdminGrantSensorsPermissions() {
return false;
}
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index cbd8e5b..10954ab 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -117,6 +117,9 @@
namespace: "enterprise"
description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended"
bug: "309183330"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 7505372..f77ebc1 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5068,7 +5068,6 @@
* {@link android.hardware.fingerprint.FingerprintManager} for handling management
* of fingerprints.
*
- * @removed See {@link android.hardware.biometrics.BiometricPrompt}
* @see #getSystemService(String)
* @see android.hardware.fingerprint.FingerprintManager
*/
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index b0f69f5..81e321d 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -83,8 +83,7 @@
/**
* A class that coordinates access to the fingerprint hardware.
- *
- * @removed See {@link BiometricPrompt} which shows a system-provided dialog upon starting
+ * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting
* authentication. In a world where devices may have different types of biometric authentication,
* it's much more realistic to have a system-provided authentication dialog since the method may
* vary by vendor/device.
@@ -95,6 +94,7 @@
@RequiresFeature(PackageManager.FEATURE_FINGERPRINT)
public class FingerprintManager implements BiometricAuthenticator, BiometricFingerprintConstants {
private static final String TAG = "FingerprintManager";
+ private static final boolean DEBUG = true;
private static final int MSG_ENROLL_RESULT = 100;
private static final int MSG_ACQUIRED = 101;
private static final int MSG_AUTHENTICATION_SUCCEEDED = 102;
@@ -196,7 +196,6 @@
/**
* Retrieves a test session for FingerprintManager.
- *
* @hide
*/
@TestApi
@@ -255,10 +254,9 @@
}
/**
- * A wrapper class for the crypto objects supported by FingerprintManager. Currently, the
+ * A wrapper class for the crypto objects supported by FingerprintManager. Currently the
* framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
*/
@Deprecated
public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -332,8 +330,7 @@
/**
* Container for callback data from {@link FingerprintManager#authenticate(CryptoObject,
* CancellationSignal, int, AuthenticationCallback, Handler)}.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
*/
@Deprecated
public static class AuthenticationResult {
@@ -395,8 +392,7 @@
* FingerprintManager#authenticate(CryptoObject, CancellationSignal,
* int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to
* fingerprint events.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
*/
@Deprecated
public static abstract class AuthenticationCallback
@@ -459,7 +455,6 @@
/**
* Callback structure provided for {@link #detectFingerprint(CancellationSignal,
* FingerprintDetectionCallback, int, Surface)}.
- *
* @hide
*/
public interface FingerprintDetectionCallback {
@@ -613,8 +608,7 @@
* by <a href="{@docRoot}training/articles/keystore.html">Android Keystore
* facility</a>.
* @throws IllegalStateException if the crypto primitive is not initialized.
- *
- * @removed See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+ * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
* BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate(
* BiometricPrompt.CryptoObject, CancellationSignal, Executor,
* BiometricPrompt.AuthenticationCallback)}
@@ -629,7 +623,6 @@
/**
* Per-user version of authenticate.
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
- *
* @hide
*/
@Deprecated
@@ -642,7 +635,6 @@
/**
* Per-user and per-sensor version of authenticate.
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
- *
* @hide
*/
@Deprecated
@@ -659,7 +651,6 @@
/**
* Version of authenticate with additional options.
- *
* @hide
*/
@RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT})
@@ -707,7 +698,6 @@
/**
* Uses the fingerprint hardware to detect for the presence of a finger, without giving details
* about accept/reject/lockout.
- *
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -750,7 +740,6 @@
* @param callback an object to receive enrollment events
* @param shouldLogMetrics a flag that indicates if enrollment failure/success metrics
* should be logged.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -821,7 +810,6 @@
/**
* Same as {@link #generateChallenge(int, GenerateChallengeCallback)}, but assumes the first
* enumerated sensor.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -836,7 +824,6 @@
/**
* Revokes the specified challenge.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -862,7 +849,6 @@
* @param sensorId Sensor ID that this operation takes effect for
* @param userId User ID that this operation takes effect for.
* @param hardwareAuthToken An opaque token returned by password confirmation.
- *
* @hide
*/
@RequiresPermission(RESET_FINGERPRINT_LOCKOUT)
@@ -900,7 +886,6 @@
/**
* Removes all fingerprint templates for the given user.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -1020,7 +1005,6 @@
/**
* Forwards BiometricStateListener to FingerprintService
* @param listener new BiometricStateListener being added
- *
* @hide
*/
public void registerBiometricStateListener(@NonNull BiometricStateListener listener) {
@@ -1172,8 +1156,7 @@
}
/**
- * This is triggered by SideFpsEventHandler.
- *
+ * This is triggered by SideFpsEventHandler
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1186,8 +1169,7 @@
* Determine if there is at least one fingerprint enrolled.
*
* @return true if at least one fingerprint is enrolled, false otherwise
- *
- * @removed See {@link BiometricPrompt} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS}
*/
@Deprecated
@@ -1221,8 +1203,7 @@
* Determine if fingerprint hardware is present and functional.
*
* @return true if hardware is present and functional, false otherwise.
- *
- * @removed See {@link BiometricPrompt} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE}
*/
@Deprecated
@@ -1248,7 +1229,6 @@
/**
* Get statically configured sensor properties.
- *
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1267,7 +1247,6 @@
/**
* Returns whether the device has a power button fingerprint sensor.
* @return boolean indicating whether power button is fingerprint sensor
- *
* @hide
*/
public boolean isPowerbuttonFps() {
diff --git a/core/java/android/os/ExternalVibrationScale.aidl b/core/java/android/os/ExternalVibrationScale.aidl
new file mode 100644
index 0000000..cf6f8ed
--- /dev/null
+++ b/core/java/android/os/ExternalVibrationScale.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018, 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 android.os;
+
+/**
+ * ExternalVibrationScale holds the vibration scale level and adaptive haptics scale. These
+ * can be used to scale external vibrations.
+ *
+ * @hide
+ */
+parcelable ExternalVibrationScale {
+ @Backing(type="int")
+ enum ScaleLevel {
+ SCALE_MUTE = -100,
+ SCALE_VERY_LOW = -2,
+ SCALE_LOW = -1,
+ SCALE_NONE = 0,
+ SCALE_HIGH = 1,
+ SCALE_VERY_HIGH = 2
+ }
+
+ /**
+ * The scale level that will be applied to external vibrations.
+ */
+ ScaleLevel scaleLevel = ScaleLevel.SCALE_NONE;
+
+ /**
+ * The adaptive haptics scale that will be applied to external vibrations.
+ */
+ float adaptiveHapticsScale = 1f;
+}
diff --git a/core/java/android/os/IExternalVibratorService.aidl b/core/java/android/os/IExternalVibratorService.aidl
index 666171f..a9df15a 100644
--- a/core/java/android/os/IExternalVibratorService.aidl
+++ b/core/java/android/os/IExternalVibratorService.aidl
@@ -17,6 +17,7 @@
package android.os;
import android.os.ExternalVibration;
+import android.os.ExternalVibrationScale;
/**
* The communication channel by which an external system that wants to control the system
@@ -32,29 +33,24 @@
* {@hide}
*/
interface IExternalVibratorService {
- const int SCALE_MUTE = -100;
- const int SCALE_VERY_LOW = -2;
- const int SCALE_LOW = -1;
- const int SCALE_NONE = 0;
- const int SCALE_HIGH = 1;
- const int SCALE_VERY_HIGH = 2;
-
/**
* A method called by the external system to start a vibration.
*
- * If this returns {@code SCALE_MUTE}, then the vibration should <em>not</em> play. If this
- * returns any other scale level, then any currently playing vibration controlled by the
- * requesting system must be muted and this vibration can begin playback.
+ * This returns an {@link ExternalVibrationScale} which includes the vibration scale level and
+ * the adaptive haptics scale.
+ *
+ * If the returned scale level is {@link ExternalVibrationScale.ScaleLevel#SCALE_MUTE}, then
+ * the vibration should <em>not</em> play. If it returns any other scale level, then
+ * any currently playing vibration controlled by the requesting system must be muted and this
+ * vibration can begin playback.
*
* Note that the IExternalVibratorService implementation will not call mute on any currently
* playing external vibrations in order to avoid re-entrancy with the system on the other side.
*
- * @param vibration An ExternalVibration
- *
- * @return {@code SCALE_MUTE} if the external vibration should not play, and any other scale
- * level if it should.
+ * @param vib The external vibration starting.
+ * @return {@link ExternalVibrationScale} including scale level and adaptive haptics scale.
*/
- int onExternalVibrationStart(in ExternalVibration vib);
+ ExternalVibrationScale onExternalVibrationStart(in ExternalVibration vib);
/**
* A method called by the external system when a vibration no longer wants to play.
diff --git a/core/java/android/os/WakeLockStats.java b/core/java/android/os/WakeLockStats.java
index 69e70a0..3769f38 100644
--- a/core/java/android/os/WakeLockStats.java
+++ b/core/java/android/os/WakeLockStats.java
@@ -23,17 +23,21 @@
/**
* Snapshot of wake lock stats.
- * @hide
+ *
+ * @hide
*/
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class WakeLockStats implements Parcelable {
- /** @hide */
- public static class WakeLock {
- public final int uid;
- @NonNull
- public final String name;
+ public static class WakeLockData {
+
+ public static final WakeLockData EMPTY = new WakeLockData(
+ /* timesAcquired= */ 0, /* totalTimeHeldMs= */ 0, /* timeHeldMs= */ 0);
+
+ /** How many times this wakelock has been acquired. */
public final int timesAcquired;
+
+ /** Time in milliseconds that the lock has been held in total. */
public final long totalTimeHeldMs;
/**
@@ -41,26 +45,34 @@
*/
public final long timeHeldMs;
- public WakeLock(int uid, @NonNull String name, int timesAcquired, long totalTimeHeldMs,
- long timeHeldMs) {
- this.uid = uid;
- this.name = name;
+ public WakeLockData(int timesAcquired, long totalTimeHeldMs, long timeHeldMs) {
this.timesAcquired = timesAcquired;
this.totalTimeHeldMs = totalTimeHeldMs;
this.timeHeldMs = timeHeldMs;
}
- private WakeLock(Parcel in) {
- uid = in.readInt();
- name = in.readString();
+ /**
+ * Whether the fields are able to construct a valid wakelock.
+ */
+ public boolean isDataValid() {
+ final boolean isDataReasonable = timesAcquired > 0
+ && totalTimeHeldMs > 0
+ && timeHeldMs >= 0
+ && totalTimeHeldMs >= timeHeldMs;
+ return isEmpty() || isDataReasonable;
+ }
+
+ private boolean isEmpty() {
+ return timesAcquired == 0 && totalTimeHeldMs == 0 && timeHeldMs == 0;
+ }
+
+ private WakeLockData(Parcel in) {
timesAcquired = in.readInt();
totalTimeHeldMs = in.readLong();
timeHeldMs = in.readLong();
}
private void writeToParcel(Parcel out) {
- out.writeInt(uid);
- out.writeString(name);
out.writeInt(timesAcquired);
out.writeLong(totalTimeHeldMs);
out.writeLong(timeHeldMs);
@@ -68,21 +80,98 @@
@Override
public String toString() {
+ return "WakeLockData{"
+ + "timesAcquired="
+ + timesAcquired
+ + ", totalTimeHeldMs="
+ + totalTimeHeldMs
+ + ", timeHeldMs="
+ + timeHeldMs
+ + "}";
+ }
+ }
+
+ /** @hide */
+ public static class WakeLock {
+
+ public static final String NAME_AGGREGATED = "wakelockstats_aggregated";
+
+ public final int uid;
+ @NonNull public final String name;
+ public final boolean isAggregated;
+
+ /** Wakelock data on both foreground and background. */
+ @NonNull public final WakeLockData totalWakeLockData;
+
+ /** Wakelock data on background. */
+ @NonNull public final WakeLockData backgroundWakeLockData;
+
+ public WakeLock(
+ int uid,
+ @NonNull String name,
+ boolean isAggregated,
+ @NonNull WakeLockData totalWakeLockData,
+ @NonNull WakeLockData backgroundWakeLockData) {
+ this.uid = uid;
+ this.name = name;
+ this.isAggregated = isAggregated;
+ this.totalWakeLockData = totalWakeLockData;
+ this.backgroundWakeLockData = backgroundWakeLockData;
+ }
+
+ /** Whether the combination of total and background wakelock data is invalid. */
+ public static boolean isDataValid(
+ WakeLockData totalWakeLockData, WakeLockData backgroundWakeLockData) {
+ return totalWakeLockData.totalTimeHeldMs > 0
+ && totalWakeLockData.isDataValid()
+ && backgroundWakeLockData.isDataValid()
+ && totalWakeLockData.timesAcquired >= backgroundWakeLockData.timesAcquired
+ && totalWakeLockData.totalTimeHeldMs >= backgroundWakeLockData.totalTimeHeldMs
+ && totalWakeLockData.timeHeldMs >= backgroundWakeLockData.timeHeldMs;
+ }
+
+ private WakeLock(Parcel in) {
+ uid = in.readInt();
+ name = in.readString();
+ isAggregated = in.readBoolean();
+ totalWakeLockData = new WakeLockData(in);
+ backgroundWakeLockData = new WakeLockData(in);
+ }
+
+ private void writeToParcel(Parcel out) {
+ out.writeInt(uid);
+ out.writeString(name);
+ out.writeBoolean(isAggregated);
+ totalWakeLockData.writeToParcel(out);
+ backgroundWakeLockData.writeToParcel(out);
+ }
+
+ @Override
+ public String toString() {
return "WakeLock{"
- + "uid=" + uid
- + ", name='" + name + '\''
- + ", timesAcquired=" + timesAcquired
- + ", totalTimeHeldMs=" + totalTimeHeldMs
- + ", timeHeldMs=" + timeHeldMs
- + '}';
+ + "uid="
+ + uid
+ + ", name='"
+ + name
+ + '\''
+ + ", isAggregated="
+ + isAggregated
+ + ", totalWakeLockData="
+ + totalWakeLockData
+ + ", backgroundWakeLockData="
+ + backgroundWakeLockData
+ + '}';
}
}
private final List<WakeLock> mWakeLocks;
+ private final List<WakeLock> mAggregatedWakeLocks;
- /** @hide **/
- public WakeLockStats(@NonNull List<WakeLock> wakeLocks) {
+ /** @hide */
+ public WakeLockStats(
+ @NonNull List<WakeLock> wakeLocks, @NonNull List<WakeLock> aggregatedWakeLocks) {
mWakeLocks = wakeLocks;
+ mAggregatedWakeLocks = aggregatedWakeLocks;
}
@NonNull
@@ -90,22 +179,38 @@
return mWakeLocks;
}
+ @NonNull
+ public List<WakeLock> getAggregatedWakeLocks() {
+ return mAggregatedWakeLocks;
+ }
+
private WakeLockStats(Parcel in) {
- final int size = in.readInt();
- mWakeLocks = new ArrayList<>(size);
- for (int i = 0; i < size; i++) {
+ final int wakelockSize = in.readInt();
+ mWakeLocks = new ArrayList<>(wakelockSize);
+ for (int i = 0; i < wakelockSize; i++) {
mWakeLocks.add(new WakeLock(in));
}
+ final int aggregatedWakelockSize = in.readInt();
+ mAggregatedWakeLocks = new ArrayList<>(aggregatedWakelockSize);
+ for (int i = 0; i < aggregatedWakelockSize; i++) {
+ mAggregatedWakeLocks.add(new WakeLock(in));
+ }
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- final int size = mWakeLocks.size();
- out.writeInt(size);
- for (int i = 0; i < size; i++) {
+ final int wakelockSize = mWakeLocks.size();
+ out.writeInt(wakelockSize);
+ for (int i = 0; i < wakelockSize; i++) {
WakeLock stats = mWakeLocks.get(i);
stats.writeToParcel(out);
}
+ final int aggregatedWakelockSize = mAggregatedWakeLocks.size();
+ out.writeInt(aggregatedWakelockSize);
+ for (int i = 0; i < aggregatedWakelockSize; i++) {
+ WakeLock stats = mAggregatedWakeLocks.get(i);
+ stats.writeToParcel(out);
+ }
}
@NonNull
@@ -127,6 +232,13 @@
@Override
public String toString() {
- return "WakeLockStats " + mWakeLocks;
+ return "WakeLockStats{"
+ + "mWakeLocks: ["
+ + mWakeLocks
+ + "]"
+ + ", mAggregatedWakeLocks: ["
+ + mAggregatedWakeLocks
+ + "]"
+ + '}';
}
}
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index fd52c76..8495f37 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1944,25 +1944,27 @@
*
* @param permissionName The name of the permission you are checking for.
* @param packageName The name of the package you are checking against.
- * @param persistentDeviceId The persistent device id you are checking against.
- * @param userId The user Id associated with context.
+ * @param persistentDeviceId The id of the physical device that you are checking permission
+ * against.
*
* @return If the package has the permission on the device, PERMISSION_GRANTED is
* returned. If it does not have the permission on the device, PERMISSION_DENIED
* is returned.
*
+ * @see VirtualDevice#getPersistentDeviceId()
* @see PackageManager#PERMISSION_GRANTED
* @see PackageManager#PERMISSION_DENIED
*
* @hide
*/
@SystemApi
+ @PermissionResult
@FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
- public static int checkPermission(@NonNull String permissionName, @NonNull String packageName,
- @NonNull String persistentDeviceId, @UserIdInt int userId) {
+ public int checkPermission(@NonNull String permissionName, @NonNull String packageName,
+ @NonNull String persistentDeviceId) {
return sPackageNamePermissionCache.query(
new PackageNamePermissionQuery(permissionName, packageName, persistentDeviceId,
- userId));
+ mContext.getUserId()));
}
/**
diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java
index 4e08aee..d0c719b 100644
--- a/core/java/android/tracing/perfetto/DataSource.java
+++ b/core/java/android/tracing/perfetto/DataSource.java
@@ -18,6 +18,8 @@
import android.util.proto.ProtoInputStream;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* Templated base class meant to be derived by embedders to create a custom data
* source.
@@ -87,7 +89,8 @@
*
* NOTE: Should only be called from native side.
*/
- protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
+ @VisibleForTesting
+ public TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
return null;
}
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
index 3710b4d..904cf55 100644
--- a/core/java/android/tracing/perfetto/DataSourceInstance.java
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -16,6 +16,8 @@
package android.tracing.perfetto;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* @hide
*/
@@ -66,7 +68,8 @@
* Only required to be called when instance was retrieved with
* `DataSource#getDataSourceInstanceLocked`.
*/
- public final void release() {
+ @VisibleForTesting
+ public void release() {
mDataSource.releaseDataSourceInstance(mInstanceIndex);
}
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index 021bbf7..896b3f4 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -196,8 +196,6 @@
private Canvas mCanvas;
private int mSaveCount;
- @Surface.FrameRateCompatibility int mFrameRateCompatibility;
-
private final Object[] mNativeWindowLock = new Object[0];
// Set by native code, do not write!
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 06dc275..4df95bf 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -24,6 +24,7 @@
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW;
@@ -35,6 +36,7 @@
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
+import static android.view.flags.Flags.sensitiveContentAppProtection;
import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision;
import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
import static android.view.flags.Flags.viewVelocityApi;
@@ -134,6 +136,7 @@
import android.sysprop.DisplayProperties;
import android.text.InputType;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.FloatProperty;
@@ -2368,6 +2371,9 @@
private static boolean sToolkitSetFrameRateReadOnlyFlagValue;
private static boolean sToolkitMetricsForFrameRateDecisionFlagValue;
+ // Used to set frame rate compatibility.
+ @Surface.FrameRateCompatibility int mFrameRateCompatibility =
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
static {
EMPTY_STATE_SET = StateSet.get(0);
@@ -3697,6 +3703,7 @@
* 1 PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
* 1 PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
* 11 PFLAG4_CONTENT_SENSITIVITY_MASK
+ * 1 PFLAG4_IS_COUNTED_AS_SENSITIVE
* |-------|-------|-------|-------|
*/
@@ -3822,6 +3829,13 @@
private static final int PFLAG4_CONTENT_SENSITIVITY_MASK =
(CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE
| CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT;
+
+ /**
+ * Whether this view has been counted as a sensitive view or not.
+ *
+ * @see AttachInfo#mSensitiveViewsCount
+ */
+ private static final int PFLAG4_IS_COUNTED_AS_SENSITIVE = 0x4000000;
/* End of masks for mPrivateFlags4 */
/** @hide */
@@ -10383,6 +10397,9 @@
mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK;
mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT)
& PFLAG4_CONTENT_SENSITIVITY_MASK);
+ if (sensitiveContentAppProtection()) {
+ updateSensitiveViewsCountIfNeeded(isAggregatedVisible());
+ }
}
/**
@@ -10414,13 +10431,44 @@
*/
@FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
public final boolean isContentSensitive() {
- if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) {
+ final int contentSensitivity = getContentSensitivity();
+ if (contentSensitivity == CONTENT_SENSITIVITY_SENSITIVE) {
return true;
+ } else if (contentSensitivity == CONTENT_SENSITIVITY_NOT_SENSITIVE) {
+ return false;
+ } else if (sensitiveContentAppProtection()) {
+ return SensitiveAutofillHintsHelper
+ .containsSensitiveAutofillHint(getAutofillHints());
}
return false;
}
/**
+ * Helper used to track sensitive views when they are added or removed from the window
+ * based on whether it's laid out and visible.
+ *
+ * <p>This method is called from many places (visibility changed, view laid out, view attached
+ * or detached to/from window, etc...)
+ */
+ private void updateSensitiveViewsCountIfNeeded(boolean appeared) {
+ if (!sensitiveContentAppProtection() || mAttachInfo == null) {
+ return;
+ }
+
+ if (appeared && isContentSensitive()) {
+ if ((mPrivateFlags4 & PFLAG4_IS_COUNTED_AS_SENSITIVE) == 0) {
+ mPrivateFlags4 |= PFLAG4_IS_COUNTED_AS_SENSITIVE;
+ mAttachInfo.increaseSensitiveViewsCount();
+ }
+ } else {
+ if ((mPrivateFlags4 & PFLAG4_IS_COUNTED_AS_SENSITIVE) != 0) {
+ mPrivateFlags4 &= ~PFLAG4_IS_COUNTED_AS_SENSITIVE;
+ mAttachInfo.decreaseSensitiveViewsCount();
+ }
+ }
+ }
+
+ /**
* Gets the mode for determining whether this view is important for content capture.
*
* <p>See {@link #setImportantForContentCapture(int)} and
@@ -10889,8 +10937,11 @@
structure.setAutofillId(new AutofillId(getAutofillId(),
AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId())));
}
- structure.setCredentialManagerRequest(getCredentialManagerRequest(),
- getCredentialManagerCallback());
+ if (getViewCredentialHandler() != null) {
+ structure.setCredentialManagerRequest(
+ getViewCredentialHandler().getRequest(),
+ getViewCredentialHandler().getCallback());
+ }
CharSequence cname = info.getClassName();
structure.setClassName(cname != null ? cname.toString() : null);
structure.setContentDescription(info.getContentDescription());
@@ -13450,6 +13501,11 @@
} else {
mAutofillHints = autofillHints;
}
+ if (sensitiveContentAppProtection()) {
+ if (getContentSensitivity() == CONTENT_SENSITIVITY_AUTO) {
+ updateSensitiveViewsCountIfNeeded(isAggregatedVisible());
+ }
+ }
}
/**
@@ -16674,6 +16730,7 @@
}
notifyAppearedOrDisappearedForContentCaptureIfNeeded(isVisible);
+ updateSensitiveViewsCountIfNeeded(isVisible);
if (!getSystemGestureExclusionRects().isEmpty()) {
postUpdate(this::updateSystemGestureExclusionRects);
@@ -22663,6 +22720,7 @@
}
notifyAppearedOrDisappearedForContentCaptureIfNeeded(false);
+ updateSensitiveViewsCountIfNeeded(false);
mAttachInfo = null;
if (mOverlay != null) {
@@ -31810,6 +31868,11 @@
ScrollCaptureInternal mScrollCaptureInternal;
/**
+ * sensitive views attached to the window
+ */
+ int mSensitiveViewsCount;
+
+ /**
* Creates a new set of attachment information with the specified
* events handler and thread.
*
@@ -31828,6 +31891,24 @@
mTreeObserver = new ViewTreeObserver(context);
}
+ void increaseSensitiveViewsCount() {
+ if (mSensitiveViewsCount == 0) {
+ mViewRootImpl.notifySensitiveContentAppProtection(true);
+ }
+ mSensitiveViewsCount++;
+ }
+
+ void decreaseSensitiveViewsCount() {
+ mSensitiveViewsCount--;
+ if (mSensitiveViewsCount == 0) {
+ mViewRootImpl.notifySensitiveContentAppProtection(false);
+ }
+ if (mSensitiveViewsCount < 0) {
+ Log.wtf(VIEW_LOG_TAG, "mSensitiveViewsCount is negative" + mSensitiveViewsCount);
+ mSensitiveViewsCount = 0;
+ }
+ }
+
@Nullable
ContentCaptureManager getContentCaptureManager(@NonNull Context context) {
if (mContentCaptureManager != null) {
@@ -32441,6 +32522,36 @@
}
}
+ private static class SensitiveAutofillHintsHelper {
+ /**
+ * List of autofill hints deemed sensitive for screen protection during screen share.
+ */
+ private static final ArraySet<String> SENSITIVE_CONTENT_AUTOFILL_HINTS = new ArraySet<>();
+ static {
+ SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_USERNAME);
+ SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_PASSWORD_AUTO);
+ SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_PASSWORD);
+ }
+
+ /**
+ * Whether View's autofill hints contains a sensitive autofill hint.
+ *
+ * @see #SENSITIVE_CONTENT_AUTOFILL_HINTS
+ */
+ static boolean containsSensitiveAutofillHint(@Nullable String[] autofillHints) {
+ if (autofillHints == null) {
+ return false;
+ }
+
+ int size = autofillHints.length;
+ for (int i = 0; i < size; i++) {
+ if (SENSITIVE_CONTENT_AUTOFILL_HINTS.contains(autofillHints[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
/**
* Returns the current scroll capture hint for this view.
@@ -33538,7 +33649,8 @@
frameRateCateogry = FRAME_RATE_CATEGORY_HIGH;
}
} else {
- viewRootImpl.votePreferredFrameRate(mPreferredFrameRate);
+ viewRootImpl.votePreferredFrameRate(mPreferredFrameRate,
+ mFrameRateCompatibility);
return;
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1e79786..23a7017 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -30,6 +30,7 @@
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
@@ -57,6 +58,7 @@
import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
import static android.view.ViewRootImplProto.WIN_FRAME;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
+import static android.view.flags.Flags.sensitiveContentAppProtection;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
@@ -165,6 +167,7 @@
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
@@ -923,6 +926,8 @@
private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
+ private final ISensitiveContentProtectionManager mSensitiveContentProtectionService;
+
static final class SystemUiVisibilityInfo {
int globalVisibility;
int localValue;
@@ -1027,6 +1032,13 @@
// as needed and can be useful for power saving.
// Should not enable the dVRR feature if the value is false.
private boolean mIsFrameRatePowerSavingsBalanced = true;
+ // Used to check if there is a conflict between different frame rate voting.
+ // Take 24 and 30 as an example, 24 is not a divisor of 30.
+ // We consider there is a conflict.
+ private boolean mIsFrameRateConflicted = false;
+ // Used to set frame rate compatibility.
+ @Surface.FrameRateCompatibility int mFrameRateCompatibility =
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
// time for touch boost period.
private static final int FRAME_RATE_TOUCH_BOOST_TIME = 3000;
// time for checking idle status periodically.
@@ -1195,6 +1207,13 @@
mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS;
mOnBackInvokedDispatcher = new WindowOnBackInvokedDispatcher(context);
+ if (sensitiveContentAppProtection()) {
+ mSensitiveContentProtectionService =
+ ISensitiveContentProtectionManager.Stub.asInterface(
+ ServiceManager.getService(Context.SENSITIVE_CONTENT_PROTECTION_SERVICE));
+ } else {
+ mSensitiveContentProtectionService = null;
+ }
}
public static void addFirstDrawHandler(Runnable callback) {
@@ -4110,6 +4129,7 @@
? mFrameRateCategoryLowCount - 1 : mFrameRateCategoryLowCount;
mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
mPreferredFrameRate = -1;
+ mIsFrameRateConflicted = false;
}
private void createSyncIfNeeded() {
@@ -4145,6 +4165,29 @@
mWmsRequestSyncGroup.add(this, null /* runnable */);
}
+ /**
+ * Helper used to notify the service to block projection when a sensitive
+ * view (the view displays sensitive content) is attached to the window.
+ * The window manager service is also notified to unblock projection when
+ * no attached view (to the window) displays sensitive content.
+ *
+ * <ol>
+ * <li>It should only notify service to block projection when first sensitive view is
+ * attached to the window.
+ * <li>It should only notify service to unblock projection when all sensitive view are
+ * removed from the window.
+ * </ol>
+ */
+ void notifySensitiveContentAppProtection(boolean showSensitiveContent) {
+ try {
+ // The window would be blocked during screen share if it shows sensitive content.
+ mSensitiveContentProtectionService.setSensitiveContentProtection(
+ getWindowToken(), mContext.getPackageName(), showSensitiveContent);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to protect sensitive content during screen share", ex);
+ }
+ }
+
private void notifyContentCaptureEvents() {
if (!isContentCaptureEnabled()) {
if (DEBUG_CONTENT_CAPTURE) {
@@ -6508,6 +6551,7 @@
break;
case MSG_FRAME_RATE_SETTING:
mPreferredFrameRate = 0;
+ mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
setPreferredFrameRate(mPreferredFrameRate);
break;
}
@@ -12286,9 +12330,15 @@
if (!shouldSetFrameRateCategory()) {
return;
}
+ int categoryFromConflictedFrameRates = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+ if (mIsFrameRateConflicted) {
+ categoryFromConflictedFrameRates = mPreferredFrameRate > 60
+ ? FRAME_RATE_CATEGORY_HIGH : FRAME_RATE_CATEGORY_NORMAL;
+ }
int frameRateCategory = mIsTouchBoosting
- ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory;
+ ? FRAME_RATE_CATEGORY_HIGH_HINT
+ : Math.max(preferredFrameRateCategory, categoryFromConflictedFrameRates);
// FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT
// For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction.
@@ -12338,7 +12388,7 @@
+ preferredFrameRate);
}
mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
- Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).applyAsyncUnsafe();
+ mFrameRateCompatibility).applyAsyncUnsafe();
mLastPreferredFrameRate = preferredFrameRate;
}
} catch (Exception e) {
@@ -12361,7 +12411,8 @@
private boolean shouldSetFrameRate() {
// use toolkitSetFrameRate flag to gate the change
- return mSurface.isValid() && mPreferredFrameRate > 0 && shouldEnableDvrr();
+ return mSurface.isValid() && mPreferredFrameRate > 0
+ && shouldEnableDvrr() && !mIsFrameRateConflicted;
}
private boolean shouldTouchBoost(int motionEventAction, int windowType) {
@@ -12404,29 +12455,45 @@
}
/**
- * Allow Views to vote for the preferred frame rate
+ * Allow Views to vote for the preferred frame rate and compatibility.
* When determining the preferred frame rate value,
* we follow this logic: If no preferred frame rate has been set yet,
* we assign the value of frameRate as the preferred frame rate.
- * If either the current or the new preferred frame rate exceeds 60 Hz,
- * we select the higher value between them.
- * However, if both values are 60 Hz or lower, we set the preferred frame rate
- * to 60 Hz to maintain optimal performance.
+ * IF there are multiple frame rates are voted:
+ * 1. There is a frame rate is a multiple of all other frame rates.
+ * We choose this frmae rate to be the one to be set.
+ * 2. There is no frame rate can be a multiple of others
+ * We set category to HIGH if the maximum frame rate is greater than 60.
+ * Otherwise, we set category to NORMAL.
+ *
+ * Use FRAME_RATE_COMPATIBILITY_GTE for velocity and FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
+ * for TextureView video play and user requested frame rate.
*
* @param frameRate the preferred frame rate of a View
+ * @param frameRateCompatibility the preferred frame rate compatibility of a View
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
- public void votePreferredFrameRate(float frameRate) {
+ public void votePreferredFrameRate(float frameRate, int frameRateCompatibility) {
if (frameRate <= 0) {
return;
}
+ if (mPreferredFrameRate > 0 && mPreferredFrameRate % frameRate != 0
+ && frameRate % mPreferredFrameRate != 0) {
+ mIsFrameRateConflicted = true;
+ mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+ }
+ if (frameRate > mPreferredFrameRate) {
+ mFrameRateCompatibility = frameRateCompatibility;
+ }
mPreferredFrameRate = Math.max(mPreferredFrameRate, frameRate);
-
mHasInvalidation = true;
- mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
- mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
- FRAME_RATE_SETTING_REEVALUATE_TIME);
+
+ if (!mIsFrameRateConflicted) {
+ mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+ mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
+ FRAME_RATE_SETTING_REEVALUATE_TIME);
+ }
}
/**
@@ -12454,6 +12521,14 @@
}
/**
+ * Get the value of mFrameRateCompatibility
+ */
+ @VisibleForTesting
+ public int getFrameRateCompatibility() {
+ return mFrameRateCompatibility;
+ }
+
+ /**
* Get the value of mIsFrameRateBoosting
*/
@VisibleForTesting
@@ -12503,6 +12578,15 @@
}
/**
+ * Get the value of mIsFrameRateConflicted
+ * Can be used to checked if there is a conflict of frame rate votes.
+ */
+ @VisibleForTesting
+ public boolean isFrameRateConflicted() {
+ return mIsFrameRateConflicted;
+ }
+
+ /**
* Set the value of mIsFrameRatePowerSavingsBalanced
* Can be used to checked if toolkit dVRR feature is enabled.
*/
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
new file mode 100644
index 0000000..4e86616
--- /dev/null
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.window.flags"
+
+flag {
+ name: "enable_scaled_resizing"
+ namespace: "lse_desktop_experience"
+ description: "Enable the resizing of un-resizable apps through scaling their bounds up/down"
+ bug: "320350734"
+ is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 51a5ddf..3d3db47 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -111,8 +111,9 @@
* @param context The context of the application.
* @param shortcutType The shortcut type.
* @return The list of {@link AccessibilityTarget}.
+ * @hide
*/
- static List<AccessibilityTarget> getInstalledTargets(Context context,
+ public static List<AccessibilityTarget> getInstalledTargets(Context context,
@ShortcutType int shortcutType) {
final List<AccessibilityTarget> targets = new ArrayList<>();
targets.addAll(getAccessibilityFilteredTargets(context, shortcutType));
diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
index 73914a2..4ef0a1b 100644
--- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
@@ -67,6 +67,8 @@
mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT,
android.content.IntentSender.class);
+ String targetPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+ final UserManager userManager = UserManager.get(this);
if (mUserId == UserHandle.USER_NULL) {
Log.wtf(TAG, "Invalid user id: " + mUserId + ". Stopping.");
@@ -74,13 +76,20 @@
return;
}
+ if (android.os.Flags.allowPrivateProfile()
+ && !userManager.isManagedProfile(mUserId)) {
+ Log.e(TAG, "Unlaunchable activity for target package " + targetPackageName
+ + " called for a non-managed-profile " + mUserId);
+ finish();
+ return;
+ }
+
if (mReason != UNLAUNCHABLE_REASON_QUIET_MODE) {
Log.wtf(TAG, "Invalid unlaunchable type: " + mReason);
finish();
return;
}
- String targetPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
boolean showEmergencyCallButton =
(targetPackageName != null && targetPackageName.equals(
mTelecomManager.getDefaultDialerPackage(UserHandle.of(mUserId))));
diff --git a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java b/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
index fab8984..c23a501 100644
--- a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
+++ b/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
@@ -16,6 +16,8 @@
package com.android.internal.display;
+import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.util.Log;
@@ -54,4 +56,31 @@
}
return maxRefreshRate;
}
+
+ /**
+ * Find the highest refresh rate among all the modes of all the displays.
+ *
+ * This method will acquire DisplayManager.mLock, so calling it while holding other locks
+ * should be done with care.
+ * @param context The context
+ * @return The highest refresh rate
+ */
+ public static float findHighestRefreshRateAmongAllDisplays(Context context) {
+ final DisplayManager dm = context.getSystemService(DisplayManager.class);
+ final Display[] displays = dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+ if (displays.length == 0) {
+ Log.w(TAG, "No valid display devices");
+ return DEFAULT_REFRESH_RATE;
+ }
+
+ float maxRefreshRate = DEFAULT_REFRESH_RATE;
+ for (Display display : displays) {
+ for (Display.Mode mode : display.getSupportedModes()) {
+ if (mode.getRefreshRate() > maxRefreshRate) {
+ maxRefreshRate = mode.getRefreshRate();
+ }
+ }
+ }
+ return maxRefreshRate;
+ }
}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c5c17cf..2a522645 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -136,10 +136,8 @@
private final Parcel mHistoryBuffer;
private final File mSystemDir;
private final HistoryStepDetailsCalculator mStepDetailsCalculator;
- private final File mHistoryDir;
private final Clock mClock;
- private int mMaxHistoryFiles;
private int mMaxHistoryBufferSize;
/**
@@ -150,7 +148,7 @@
/**
* A list of history files with increasing timestamps.
*/
- private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>();
+ private final BatteryHistoryDirectory mHistoryDir;
/**
* A list of small history parcels, used when BatteryStatsImpl object is created from
@@ -161,7 +159,8 @@
/**
* When iterating history files, the current file index.
*/
- private int mCurrentFileIndex;
+ private BatteryHistoryFile mCurrentFile;
+
/**
* When iterating history files, the current file parcel.
*/
@@ -203,7 +202,6 @@
private byte mLastHistoryStepLevel = 0;
private boolean mMutable = true;
private final BatteryStatsHistory mWritableHistory;
- private boolean mCleanupEnabled = true;
private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> {
public final long monotonicTimeMs;
@@ -235,6 +233,271 @@
}
}
+ private static class BatteryHistoryDirectory {
+ private final File mDirectory;
+ private final MonotonicClock mMonotonicClock;
+ private int mMaxHistoryFiles;
+ private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>();
+ private final ReentrantLock mLock = new ReentrantLock();
+ private boolean mCleanupNeeded;
+
+ BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock,
+ int maxHistoryFiles) {
+ mDirectory = directory;
+ mMonotonicClock = monotonicClock;
+ mMaxHistoryFiles = maxHistoryFiles;
+ if (mMaxHistoryFiles == 0) {
+ Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
+ }
+ }
+
+ void setMaxHistoryFiles(int maxHistoryFiles) {
+ mMaxHistoryFiles = maxHistoryFiles;
+ cleanup();
+ }
+
+ void lock() {
+ mLock.lock();
+ }
+
+ void unlock() {
+ mLock.unlock();
+ if (mCleanupNeeded) {
+ cleanup();
+ }
+ }
+
+ boolean isLocked() {
+ return mLock.isLocked();
+ }
+
+ void load() {
+ mDirectory.mkdirs();
+ if (!mDirectory.exists()) {
+ Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
+ }
+
+ final List<File> toRemove = new ArrayList<>();
+ final Set<BatteryHistoryFile> dedup = new ArraySet<>();
+ mDirectory.listFiles((dir, name) -> {
+ final int b = name.lastIndexOf(FILE_SUFFIX);
+ if (b <= 0) {
+ toRemove.add(new File(dir, name));
+ return false;
+ }
+ try {
+ long monotonicTime = Long.parseLong(name.substring(0, b));
+ dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime));
+ } catch (NumberFormatException e) {
+ toRemove.add(new File(dir, name));
+ return false;
+ }
+ return true;
+ });
+ if (!dedup.isEmpty()) {
+ mHistoryFiles.addAll(dedup);
+ Collections.sort(mHistoryFiles);
+ }
+ if (!toRemove.isEmpty()) {
+ // Clear out legacy history files, which did not follow the X-Y.bin naming format.
+ BackgroundThread.getHandler().post(() -> {
+ lock();
+ try {
+ for (File file : toRemove) {
+ file.delete();
+ }
+ } finally {
+ unlock();
+ }
+ });
+ }
+ }
+
+ List<String> getFileNames() {
+ lock();
+ try {
+ List<String> names = new ArrayList<>();
+ for (BatteryHistoryFile historyFile : mHistoryFiles) {
+ names.add(historyFile.atomicFile.getBaseFile().getName());
+ }
+ return names;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Nullable
+ BatteryHistoryFile getFirstFile() {
+ lock();
+ try {
+ if (!mHistoryFiles.isEmpty()) {
+ return mHistoryFiles.get(0);
+ }
+ return null;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Nullable
+ BatteryHistoryFile getLastFile() {
+ lock();
+ try {
+ if (!mHistoryFiles.isEmpty()) {
+ return mHistoryFiles.get(mHistoryFiles.size() - 1);
+ }
+ return null;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Nullable
+ BatteryHistoryFile getNextFile(BatteryHistoryFile current, long startTimeMs,
+ long endTimeMs) {
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("Iterating battery history without a lock");
+ }
+
+ int nextFileIndex = 0;
+ int firstFileIndex = 0;
+ // skip the last file because its data is in history buffer.
+ int lastFileIndex = mHistoryFiles.size() - 2;
+ for (int i = lastFileIndex; i >= 0; i--) {
+ BatteryHistoryFile file = mHistoryFiles.get(i);
+ if (current != null && file.monotonicTimeMs == current.monotonicTimeMs) {
+ nextFileIndex = i + 1;
+ }
+ if (file.monotonicTimeMs > endTimeMs) {
+ lastFileIndex = i - 1;
+ }
+ if (file.monotonicTimeMs <= startTimeMs) {
+ firstFileIndex = i;
+ break;
+ }
+ }
+
+ if (nextFileIndex < firstFileIndex) {
+ nextFileIndex = firstFileIndex;
+ }
+
+ if (nextFileIndex <= lastFileIndex) {
+ return mHistoryFiles.get(nextFileIndex);
+ }
+
+ return null;
+ }
+
+ BatteryHistoryFile makeBatteryHistoryFile() {
+ BatteryHistoryFile file = new BatteryHistoryFile(mDirectory,
+ mMonotonicClock.monotonicTime());
+ lock();
+ try {
+ mHistoryFiles.add(file);
+ } finally {
+ unlock();
+ }
+ return file;
+ }
+
+ void writeToParcel(Parcel out, boolean useBlobs) {
+ lock();
+ try {
+ final long start = SystemClock.uptimeMillis();
+ out.writeInt(mHistoryFiles.size() - 1);
+ for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
+ AtomicFile file = mHistoryFiles.get(i).atomicFile;
+ byte[] raw = new byte[0];
+ try {
+ raw = file.readFully();
+ } catch (Exception e) {
+ Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
+ }
+ if (useBlobs) {
+ out.writeBlob(raw);
+ } else {
+ // Avoiding blobs in the check-in file for compatibility
+ out.writeByteArray(raw);
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
+ }
+ } finally {
+ unlock();
+ }
+ }
+
+ int getFileCount() {
+ lock();
+ try {
+ return mHistoryFiles.size();
+ } finally {
+ unlock();
+ }
+ }
+
+ int getSize() {
+ lock();
+ try {
+ int ret = 0;
+ for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
+ ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length();
+ }
+ return ret;
+ } finally {
+ unlock();
+ }
+ }
+
+ void reset() {
+ lock();
+ try {
+ if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
+ for (BatteryHistoryFile file : mHistoryFiles) {
+ file.atomicFile.delete();
+ }
+ mHistoryFiles.clear();
+ } finally {
+ unlock();
+ }
+ }
+
+ private void cleanup() {
+ if (mDirectory == null) {
+ return;
+ }
+
+ if (isLocked()) {
+ mCleanupNeeded = true;
+ return;
+ }
+
+ mCleanupNeeded = false;
+
+ lock();
+ try {
+ // if free disk space is less than 100MB, delete oldest history file.
+ if (!hasFreeDiskSpace(mDirectory)) {
+ BatteryHistoryFile oldest = mHistoryFiles.remove(0);
+ oldest.atomicFile.delete();
+ }
+
+ // if there are more history files than allowed, delete oldest history files.
+ // mMaxHistoryFiles comes from Constants.MAX_HISTORY_FILES and
+ // can be updated by DeviceConfig at run time.
+ while (mHistoryFiles.size() > mMaxHistoryFiles) {
+ BatteryHistoryFile oldest = mHistoryFiles.get(0);
+ oldest.atomicFile.delete();
+ mHistoryFiles.remove(0);
+ }
+ } finally {
+ unlock();
+ }
+ }
+ }
+
/**
* A delegate responsible for computing additional details for a step in battery history.
*/
@@ -351,7 +614,6 @@
BatteryStatsHistory writableHistory) {
mHistoryBuffer = historyBuffer;
mSystemDir = systemDir;
- mMaxHistoryFiles = maxHistoryFiles;
mMaxHistoryBufferSize = maxHistoryBufferSize;
mStepDetailsCalculator = stepDetailsCalculator;
mTracer = tracer;
@@ -363,66 +625,32 @@
mMutable = false;
}
- mHistoryDir = new File(systemDir, HISTORY_DIR);
- mHistoryDir.mkdirs();
- if (!mHistoryDir.exists()) {
- Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
- }
-
- final List<File> toRemove = new ArrayList<>();
- final Set<BatteryHistoryFile> dedup = new ArraySet<>();
- mHistoryDir.listFiles((dir, name) -> {
- final int b = name.lastIndexOf(FILE_SUFFIX);
- if (b <= 0) {
- toRemove.add(new File(dir, name));
- return false;
+ if (writableHistory != null) {
+ mHistoryDir = writableHistory.mHistoryDir;
+ } else {
+ mHistoryDir = new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR),
+ monotonicClock, maxHistoryFiles);
+ mHistoryDir.load();
+ BatteryHistoryFile activeFile = mHistoryDir.getLastFile();
+ if (activeFile == null) {
+ activeFile = mHistoryDir.makeBatteryHistoryFile();
}
- try {
- long monotonicTime = Long.parseLong(name.substring(0, b));
- dedup.add(new BatteryHistoryFile(mHistoryDir, monotonicTime));
- } catch (NumberFormatException e) {
- toRemove.add(new File(dir, name));
- return false;
- }
- return true;
- });
- if (!dedup.isEmpty()) {
- mHistoryFiles.addAll(dedup);
- Collections.sort(mHistoryFiles);
- setActiveFile(mHistoryFiles.get(mHistoryFiles.size() - 1));
- } else if (mMutable) {
- // No file found, default to have the initial file.
- BatteryHistoryFile name = makeBatteryHistoryFile();
- mHistoryFiles.add(name);
- setActiveFile(name);
- }
- if (!toRemove.isEmpty()) {
- // Clear out legacy history files, which did not follow the X-Y.bin naming format.
- BackgroundThread.getHandler().post(() -> {
- for (File file : toRemove) {
- file.delete();
- }
- });
+ setActiveFile(activeFile);
}
}
- private BatteryHistoryFile makeBatteryHistoryFile() {
- return new BatteryHistoryFile(mHistoryDir, mMonotonicClock.monotonicTime());
- }
-
- public BatteryStatsHistory(int maxHistoryFiles, int maxHistoryBufferSize,
+ public BatteryStatsHistory(int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
MonotonicClock monotonicClock) {
- this(maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator, clock, monotonicClock,
+ this(maxHistoryBufferSize, stepDetailsCalculator, clock, monotonicClock,
new TraceDelegate(), new EventLogger());
}
@VisibleForTesting
- public BatteryStatsHistory(int maxHistoryFiles, int maxHistoryBufferSize,
+ public BatteryStatsHistory(int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
MonotonicClock monotonicClock, TraceDelegate traceDelegate,
EventLogger eventLogger) {
- mMaxHistoryFiles = maxHistoryFiles;
mMaxHistoryBufferSize = maxHistoryBufferSize;
mStepDetailsCalculator = stepDetailsCalculator;
mTracer = traceDelegate;
@@ -484,7 +712,9 @@
* Changes the maximum number of history files to be kept.
*/
public void setMaxHistoryFiles(int maxHistoryFiles) {
- mMaxHistoryFiles = maxHistoryFiles;
+ if (mHistoryDir != null) {
+ mHistoryDir.setMaxHistoryFiles(maxHistoryFiles);
+ }
}
/**
@@ -513,7 +743,7 @@
* Returns true if this instance only supports reading history.
*/
public boolean isReadOnly() {
- return !mMutable || mActiveFile == null || mHistoryDir == null;
+ return !mMutable || mActiveFile == null/* || mHistoryDir == null*/;
}
/**
@@ -538,25 +768,13 @@
@GuardedBy("this")
private void startNextFileLocked(long elapsedRealtimeMs) {
- if (mMaxHistoryFiles == 0) {
- Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
- return;
- }
-
- if (mHistoryFiles.isEmpty()) {
- Slog.wtf(TAG, "mFileNumbers should never be empty");
- return;
- }
-
final long start = SystemClock.uptimeMillis();
writeHistory();
if (DEBUG) {
Slog.d(TAG, "writeHistory took ms:" + (SystemClock.uptimeMillis() - start));
}
- final BatteryHistoryFile next = makeBatteryHistoryFile();
- mHistoryFiles.add(next);
- setActiveFile(next);
+ setActiveFile(mHistoryDir.makeBatteryHistoryFile());
try {
mActiveFile.getBaseFile().createNewFile();
} catch (IOException e) {
@@ -578,37 +796,7 @@
}
mWrittenPowerStatsDescriptors.clear();
- cleanupLocked();
- }
-
- @GuardedBy("this")
- private void setCleanupEnabledLocked(boolean enabled) {
- mCleanupEnabled = enabled;
- if (mCleanupEnabled) {
- cleanupLocked();
- }
- }
-
- @GuardedBy("this")
- private void cleanupLocked() {
- if (!mCleanupEnabled || mHistoryDir == null) {
- return;
- }
-
- // if free disk space is less than 100MB, delete oldest history file.
- if (!hasFreeDiskSpace()) {
- BatteryHistoryFile oldest = mHistoryFiles.remove(0);
- oldest.atomicFile.delete();
- }
-
- // if there are more history files than allowed, delete oldest history files.
- // mMaxHistoryFiles comes from Constants.MAX_HISTORY_FILES and can be updated by GService
- // config at run time.
- while (mHistoryFiles.size() > mMaxHistoryFiles) {
- BatteryHistoryFile oldest = mHistoryFiles.get(0);
- oldest.atomicFile.delete();
- mHistoryFiles.remove(0);
- }
+ mHistoryDir.cleanup();
}
/**
@@ -616,9 +804,7 @@
* currently being read.
*/
public boolean isResetEnabled() {
- synchronized (this) {
- return mCleanupEnabled;
- }
+ return mHistoryDir == null || !mHistoryDir.isLocked();
}
/**
@@ -627,16 +813,10 @@
*/
public void reset() {
synchronized (this) {
- if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
- for (BatteryHistoryFile file : mHistoryFiles) {
- file.atomicFile.delete();
+ if (mHistoryDir != null) {
+ mHistoryDir.reset();
+ setActiveFile(mHistoryDir.makeBatteryHistoryFile());
}
- mHistoryFiles.clear();
-
- BatteryHistoryFile name = makeBatteryHistoryFile();
- mHistoryFiles.add(name);
- setActiveFile(name);
-
initHistoryBuffer();
}
}
@@ -646,8 +826,9 @@
*/
public long getStartTime() {
synchronized (this) {
- if (!mHistoryFiles.isEmpty()) {
- return mHistoryFiles.get(0).monotonicTimeMs;
+ BatteryHistoryFile file = mHistoryDir.getFirstFile();
+ if (file != null) {
+ return file.monotonicTimeMs;
} else {
return mHistoryBufferStartTime;
}
@@ -668,15 +849,13 @@
return copy().iterate(startTimeMs, endTimeMs);
}
- mCurrentFileIndex = 0;
+ if (mHistoryDir != null) {
+ mHistoryDir.lock();
+ }
+ mCurrentFile = null;
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
- if (mWritableHistory != null) {
- synchronized (mWritableHistory) {
- mWritableHistory.setCleanupEnabledLocked(false);
- }
- }
return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs);
}
@@ -685,10 +864,8 @@
*/
void iteratorFinished() {
mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
- if (mWritableHistory != null) {
- synchronized (mWritableHistory) {
- mWritableHistory.setCleanupEnabledLocked(true);
- }
+ if (mHistoryDir != null) {
+ mHistoryDir.unlock();
}
}
@@ -719,39 +896,27 @@
}
}
- int firstFileIndex = 0;
- // skip the last file because its data is in history buffer.
- int lastFileIndex = mHistoryFiles.size() - 1;
- for (int i = mHistoryFiles.size() - 1; i >= 0; i--) {
- BatteryHistoryFile file = mHistoryFiles.get(i);
- if (file.monotonicTimeMs >= endTimeMs) {
- lastFileIndex = i;
- }
- if (file.monotonicTimeMs <= startTimeMs) {
- firstFileIndex = i;
- break;
- }
- }
-
- if (mCurrentFileIndex < firstFileIndex) {
- mCurrentFileIndex = firstFileIndex;
- }
-
- while (mCurrentFileIndex < lastFileIndex) {
- mCurrentParcel = null;
- mCurrentParcelEnd = 0;
- final Parcel p = Parcel.obtain();
- AtomicFile file = mHistoryFiles.get(mCurrentFileIndex++).atomicFile;
- if (readFileToParcel(p, file)) {
- int bufSize = p.readInt();
- int curPos = p.dataPosition();
- mCurrentParcelEnd = curPos + bufSize;
- mCurrentParcel = p;
- if (curPos < mCurrentParcelEnd) {
- return mCurrentParcel;
+ if (mHistoryDir != null) {
+ BatteryHistoryFile nextFile = mHistoryDir.getNextFile(mCurrentFile, startTimeMs,
+ endTimeMs);
+ while (nextFile != null) {
+ mCurrentParcel = null;
+ mCurrentParcelEnd = 0;
+ final Parcel p = Parcel.obtain();
+ AtomicFile file = nextFile.atomicFile;
+ if (readFileToParcel(p, file)) {
+ int bufSize = p.readInt();
+ int curPos = p.dataPosition();
+ mCurrentParcelEnd = curPos + bufSize;
+ mCurrentParcel = p;
+ if (curPos < mCurrentParcelEnd) {
+ mCurrentFile = nextFile;
+ return mCurrentParcel;
+ }
+ } else {
+ p.recycle();
}
- } else {
- p.recycle();
+ nextFile = mHistoryDir.getNextFile(nextFile, startTimeMs, endTimeMs);
}
}
@@ -922,25 +1087,8 @@
}
private void writeToParcel(Parcel out, boolean useBlobs) {
- final long start = SystemClock.uptimeMillis();
- out.writeInt(mHistoryFiles.size() - 1);
- for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
- AtomicFile file = mHistoryFiles.get(i).atomicFile;
- byte[] raw = new byte[0];
- try {
- raw = file.readFully();
- } catch (Exception e) {
- Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
- }
- if (useBlobs) {
- out.writeBlob(raw);
- } else {
- // Avoiding blobs in the check-in file for compatibility
- out.writeByteArray(raw);
- }
- }
- if (DEBUG) {
- Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
+ if (mHistoryDir != null) {
+ mHistoryDir.writeToParcel(out, useBlobs);
}
}
@@ -1021,22 +1169,18 @@
* @return true if there is more than 100MB free disk space left.
*/
@android.ravenwood.annotation.RavenwoodReplace
- private boolean hasFreeDiskSpace() {
- final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
+ private static boolean hasFreeDiskSpace(File systemDir) {
+ final StatFs stats = new StatFs(systemDir.getAbsolutePath());
return stats.getAvailableBytes() > MIN_FREE_SPACE;
}
- private boolean hasFreeDiskSpace$ravenwood() {
+ private static boolean hasFreeDiskSpace$ravenwood(File systemDir) {
return true;
}
@VisibleForTesting
public List<String> getFilesNames() {
- List<String> names = new ArrayList<>();
- for (BatteryHistoryFile historyFile : mHistoryFiles) {
- names.add(historyFile.atomicFile.getBaseFile().getName());
- }
- return names;
+ return mHistoryDir.getFileNames();
}
@VisibleForTesting
@@ -1048,10 +1192,7 @@
* @return the total size of all history files and history buffer.
*/
public int getHistoryUsedSize() {
- int ret = 0;
- for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
- ret += mHistoryFiles.get(i).atomicFile.getBaseFile().length();
- }
+ int ret = mHistoryDir.getSize();
ret += mHistoryBuffer.dataSize();
if (mHistoryParcels != null) {
for (int i = 0; i < mHistoryParcels.size(); i++) {
@@ -1109,7 +1250,7 @@
*/
public void continueRecordingHistory() {
synchronized (this) {
- if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) {
+ if (mHistoryBuffer.dataPosition() <= 0 && mHistoryDir.getFileCount() <= 1) {
return;
}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index b2a6a93..83e9407 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -44,6 +44,7 @@
private BatteryStats.HistoryItem mHistoryItem = new BatteryStats.HistoryItem();
private boolean mNextItemReady;
private boolean mTimeInitialized;
+ private boolean mClosed;
public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history, long startTimeMs,
long endTimeMs) {
@@ -322,6 +323,9 @@
*/
@Override
public void close() {
- mBatteryStatsHistory.iteratorFinished();
+ if (!mClosed) {
+ mClosed = true;
+ mBatteryStatsHistory.iteratorFinished();
+ }
}
}
diff --git a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
similarity index 81%
rename from core/java/com/android/internal/protolog/BaseProtoLogImpl.java
rename to core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index abe6c7c..d9ac5a9 100644
--- a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -37,8 +37,11 @@
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
import com.android.internal.util.TraceBuffer;
import java.io.File;
@@ -48,52 +51,49 @@
import java.util.TreeMap;
import java.util.stream.Collectors;
-
/**
* A service for the ProtoLog logging system.
*/
-public class BaseProtoLogImpl {
- protected static final TreeMap<String, IProtoLogGroup> LOG_GROUPS = new TreeMap<>();
+public class LegacyProtoLogImpl implements IProtoLog {
+ private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
- /**
- * A runnable to update the cached output of {@link #isEnabled}.
- *
- * Must be invoked after every action that could change the result of {@link #isEnabled}, eg.
- * starting / stopping proto log, or enabling / disabling log groups.
- */
- public static Runnable sCacheUpdater = () -> { };
-
- protected static void addLogGroupEnum(IProtoLogGroup[] config) {
- for (IProtoLogGroup group : config) {
- LOG_GROUPS.put(group.name(), group);
- }
- }
-
+ private static final int BUFFER_CAPACITY = 1024 * 1024;
+ private static final int PER_CHUNK_SIZE = 1024;
private static final String TAG = "ProtoLog";
private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
static final String PROTOLOG_VERSION = "1.0.0";
private static final int DEFAULT_PER_CHUNK_SIZE = 0;
private final File mLogFile;
- private final String mViewerConfigFilename;
+ private final String mLegacyViewerConfigFilename;
private final TraceBuffer mBuffer;
- protected final ProtoLogViewerConfigReader mViewerConfig;
+ private final LegacyProtoLogViewerConfigReader mViewerConfig;
private final int mPerChunkSize;
private boolean mProtoLogEnabled;
private boolean mProtoLogEnabledLockFree;
private final Object mProtoLogEnabledLock = new Object();
- @VisibleForTesting
- public enum LogLevel {
- DEBUG, VERBOSE, INFO, WARN, ERROR, WTF
+ public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename) {
+ this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY,
+ new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE);
+ }
+
+ public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
+ LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
+ mLogFile = file;
+ mBuffer = new TraceBuffer(bufferCapacity);
+ mLegacyViewerConfigFilename = viewerConfigFilename;
+ mViewerConfig = viewerConfig;
+ mPerChunkSize = perChunkSize;
}
/**
* Main log method, do not call directly.
*/
@VisibleForTesting
- public void log(LogLevel level, IProtoLogGroup group, int messageHash, int paramsMask,
+ @Override
+ public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString, Object[] args) {
if (group.isLogToProto()) {
logToProto(messageHash, paramsMask, args);
@@ -103,7 +103,7 @@
}
}
- private void logToLogcat(String tag, LogLevel level, int messageHash,
+ private void logToLogcat(String tag, LogLevel level, long messageHash,
@Nullable String messageString, Object[] args) {
String message = null;
if (messageString == null) {
@@ -157,7 +157,7 @@
}
}
- private void logToProto(int messageHash, int paramsMask, Object[] args) {
+ private void logToProto(long messageHash, int paramsMask, Object[] args) {
if (!isProtoEnabled()) {
return;
}
@@ -219,20 +219,6 @@
}
}
- public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
- ProtoLogViewerConfigReader viewerConfig) {
- this(file, viewerConfigFilename, bufferCapacity, viewerConfig, DEFAULT_PER_CHUNK_SIZE);
- }
-
- public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
- ProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
- mLogFile = file;
- mBuffer = new TraceBuffer(bufferCapacity);
- mViewerConfigFilename = viewerConfigFilename;
- mViewerConfig = viewerConfig;
- mPerChunkSize = perChunkSize;
- }
-
/**
* Starts the logging a circular proto buffer.
*
@@ -248,7 +234,6 @@
mProtoLogEnabled = true;
mProtoLogEnabledLockFree = true;
}
- sCacheUpdater.run();
}
/**
@@ -274,7 +259,6 @@
throw new IllegalStateException("logging enabled while waiting for flush.");
}
}
- sCacheUpdater.run();
}
/**
@@ -284,11 +268,11 @@
return mProtoLogEnabledLockFree;
}
- protected int setLogging(boolean setTextLogging, boolean value, PrintWriter pw,
+ private int setLogging(boolean setTextLogging, boolean value, ILogger logger,
String... groups) {
for (int i = 0; i < groups.length; i++) {
String group = groups[i];
- IProtoLogGroup g = LOG_GROUPS.get(group);
+ IProtoLogGroup g = mLogGroups.get(group);
if (g != null) {
if (setTextLogging) {
g.setLogToLogcat(value);
@@ -296,11 +280,10 @@
g.setLogToProto(value);
}
} else {
- logAndPrintln(pw, "No IProtoLogGroup named " + group);
+ logger.log("No IProtoLogGroup named " + group);
return -1;
}
}
- sCacheUpdater.run();
return 0;
}
@@ -330,6 +313,7 @@
while ((arg = shell.getNextArg()) != null) {
args.add(arg);
}
+ final ILogger logger = (msg) -> logAndPrintln(pw, msg);
String[] groups = args.toArray(new String[args.size()]);
switch (cmd) {
case "start":
@@ -342,14 +326,14 @@
logAndPrintln(pw, getStatus());
return 0;
case "enable":
- return setLogging(false, true, pw, groups);
+ return setLogging(false, true, logger, groups);
case "enable-text":
- mViewerConfig.loadViewerConfig(pw, mViewerConfigFilename);
- return setLogging(true, true, pw, groups);
+ mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+ return setLogging(true, true, logger, groups);
case "disable":
- return setLogging(false, false, pw, groups);
+ return setLogging(false, false, logger, groups);
case "disable-text":
- return setLogging(true, false, pw, groups);
+ return setLogging(true, false, logger, groups);
default:
return unknownCommand(pw);
}
@@ -362,12 +346,12 @@
return "ProtoLog status: "
+ ((isProtoEnabled()) ? "Enabled" : "Disabled")
+ "\nEnabled log groups: \n Proto: "
- + LOG_GROUPS.values().stream().filter(
- it -> it.isEnabled() && it.isLogToProto())
+ + mLogGroups.values().stream().filter(
+ it -> it.isEnabled() && it.isLogToProto())
.map(IProtoLogGroup::name).collect(Collectors.joining(" "))
+ "\n Logcat: "
- + LOG_GROUPS.values().stream().filter(
- it -> it.isEnabled() && it.isLogToLogcat())
+ + mLogGroups.values().stream().filter(
+ it -> it.isEnabled() && it.isLogToLogcat())
.map(IProtoLogGroup::name).collect(Collectors.joining(" "))
+ "\nLogging definitions loaded: " + mViewerConfig.knownViewerStringsNumber();
}
@@ -393,5 +377,26 @@
pw.flush();
}
}
+
+ /**
+ * Start text logging
+ * @param groups Groups to start text logging for
+ * @param logger A logger to write status updates to
+ * @return status code
+ */
+ public int startLoggingToLogcat(String[] groups, ILogger logger) {
+ mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+ return setLogging(true /* setTextLogging */, true, logger, groups);
+ }
+
+ /**
+ * Stop text logging
+ * @param groups Groups to start text logging for
+ * @param logger A logger to write status updates to
+ * @return status code
+ */
+ public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+ return setLogging(true /* setTextLogging */, false, logger, groups);
+ }
}
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java
new file mode 100644
index 0000000..1833410
--- /dev/null
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 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.protolog;
+
+import com.android.internal.protolog.common.ILogger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Handles loading and parsing of ProtoLog viewer configuration.
+ */
+public class LegacyProtoLogViewerConfigReader {
+
+ private static final String TAG = "ProtoLogViewerConfigReader";
+ private Map<Long, String> mLogMessageMap = null;
+
+ /** Returns message format string for its hash or null if unavailable. */
+ public synchronized String getViewerString(long messageHash) {
+ if (mLogMessageMap != null) {
+ return mLogMessageMap.get(messageHash);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Reads the specified viewer configuration file. Does nothing if the config is already loaded.
+ */
+ public synchronized void loadViewerConfig(ILogger logger, String viewerConfigFilename) {
+ try {
+ loadViewerConfig(new GZIPInputStream(new FileInputStream(viewerConfigFilename)));
+ logger.log("Loaded " + mLogMessageMap.size()
+ + " log definitions from " + viewerConfigFilename);
+ } catch (FileNotFoundException e) {
+ logger.log("Unable to load log definitions: File "
+ + viewerConfigFilename + " not found." + e);
+ } catch (IOException e) {
+ logger.log("Unable to load log definitions: IOException while reading "
+ + viewerConfigFilename + ". " + e);
+ } catch (JSONException e) {
+ logger.log("Unable to load log definitions: JSON parsing exception while reading "
+ + viewerConfigFilename + ". " + e);
+ }
+ }
+
+ /**
+ * Reads the specified viewer configuration input stream.
+ * Does nothing if the config is already loaded.
+ */
+ public synchronized void loadViewerConfig(InputStream viewerConfigInputStream)
+ throws IOException, JSONException {
+ if (mLogMessageMap != null) {
+ return;
+ }
+ InputStreamReader config = new InputStreamReader(viewerConfigInputStream);
+ BufferedReader reader = new BufferedReader(config);
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line).append('\n');
+ }
+ reader.close();
+ JSONObject json = new JSONObject(builder.toString());
+ JSONObject messages = json.getJSONObject("messages");
+
+ mLogMessageMap = new TreeMap<>();
+ Iterator it = messages.keys();
+ while (it.hasNext()) {
+ String key = (String) it.next();
+ try {
+ long hash = Long.parseLong(key);
+ JSONObject val = messages.getJSONObject(key);
+ String msg = val.getString("message");
+ mLogMessageMap.put(hash, msg);
+ } catch (NumberFormatException expected) {
+ // Not a messageHash - skip it
+ }
+ }
+ }
+
+ /**
+ * Returns the number of loaded log definitions kept in memory.
+ */
+ public synchronized int knownViewerStringsNumber() {
+ if (mLogMessageMap != null) {
+ return mLogMessageMap.size();
+ }
+ return 0;
+ }
+
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
new file mode 100644
index 0000000..53062d8
--- /dev/null
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -0,0 +1,559 @@
+/*
+ * Copyright (C) 2024 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.protolog;
+
+import static perfetto.protos.PerfettoTrace.InternedData.PROTOLOG_STACKTRACE;
+import static perfetto.protos.PerfettoTrace.InternedData.PROTOLOG_STRING_ARGS;
+import static perfetto.protos.PerfettoTrace.InternedString.IID;
+import static perfetto.protos.PerfettoTrace.InternedString.STR;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.BOOLEAN_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.DOUBLE_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.MESSAGE_ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.SINT64_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STR_PARAM_IIDS;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.GROUPS;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.NAME;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.TAG;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MESSAGES;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.GROUP_ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.LEVEL;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static perfetto.protos.PerfettoTrace.TracePacket.INTERNED_DATA;
+import static perfetto.protos.PerfettoTrace.TracePacket.PROTOLOG_MESSAGE;
+import static perfetto.protos.PerfettoTrace.TracePacket.PROTOLOG_VIEWER_CONFIG;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQUENCE_FLAGS;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQ_INCREMENTAL_STATE_CLEARED;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQ_NEEDS_INCREMENTAL_STATE;
+import static perfetto.protos.PerfettoTrace.TracePacket.TIMESTAMP;
+
+import android.annotation.Nullable;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.perfetto.TracingContext;
+import android.util.LongArray;
+import android.util.Slog;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData;
+
+/**
+ * A service for the ProtoLog logging system.
+ */
+public class PerfettoProtoLogImpl implements IProtoLog {
+ private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+ private static final String LOG_TAG = "ProtoLog";
+ private final AtomicInteger mTracingInstances = new AtomicInteger();
+
+ private final ProtoLogDataSource mDataSource = new ProtoLogDataSource(
+ this.mTracingInstances::incrementAndGet,
+ this::dumpTransitionTraceConfig,
+ this.mTracingInstances::decrementAndGet
+ );
+ private final ProtoLogViewerConfigReader mViewerConfigReader;
+ private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+
+ public PerfettoProtoLogImpl(String viewerConfigFilePath) {
+ this(() -> {
+ try {
+ return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+ } catch (FileNotFoundException e) {
+ Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
+ return null;
+ }
+ });
+ }
+
+ public PerfettoProtoLogImpl(ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+ this(viewerConfigInputStreamProvider,
+ new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+ }
+
+ @VisibleForTesting
+ public PerfettoProtoLogImpl(
+ ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+ ProtoLogViewerConfigReader viewerConfigReader
+ ) {
+ Producer.init(InitArguments.DEFAULTS);
+ mDataSource.register(DataSourceParams.DEFAULTS);
+ this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+ this.mViewerConfigReader = viewerConfigReader;
+ }
+
+ /**
+ * Main log method, do not call directly.
+ */
+ @VisibleForTesting
+ @Override
+ public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
+ @Nullable String messageString, Object[] args) {
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "log");
+
+ long tsNanos = SystemClock.elapsedRealtimeNanos();
+ try {
+ logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos);
+ if (group.isLogToLogcat()) {
+ logToLogcat(group.getTag(), level, messageHash, messageString, args);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void dumpTransitionTraceConfig() {
+ ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
+ if (pis == null) {
+ Slog.w(LOG_TAG, "Failed to get viewer input stream.");
+ return;
+ }
+
+ mDataSource.trace(ctx -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+
+ os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos());
+
+ final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG);
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (pis.getFieldNumber() == (int) MESSAGES) {
+ final long inMessageToken = pis.start(MESSAGES);
+ final long outMessagesToken = os.start(MESSAGES);
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (pis.getFieldNumber()) {
+ case (int) MessageData.MESSAGE_ID:
+ os.write(MessageData.MESSAGE_ID,
+ pis.readLong(MessageData.MESSAGE_ID));
+ break;
+ case (int) MESSAGE:
+ os.write(MESSAGE, pis.readString(MESSAGE));
+ break;
+ case (int) LEVEL:
+ os.write(LEVEL, pis.readInt(LEVEL));
+ break;
+ case (int) GROUP_ID:
+ os.write(GROUP_ID, pis.readInt(GROUP_ID));
+ break;
+ default:
+ throw new RuntimeException(
+ "Unexpected field id " + pis.getFieldNumber());
+ }
+ }
+
+ pis.end(inMessageToken);
+ os.end(outMessagesToken);
+ }
+
+ if (pis.getFieldNumber() == (int) GROUPS) {
+ final long inGroupToken = pis.start(GROUPS);
+ final long outGroupToken = os.start(GROUPS);
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (pis.getFieldNumber()) {
+ case (int) ID:
+ int id = pis.readInt(ID);
+ os.write(ID, id);
+ break;
+ case (int) NAME:
+ String name = pis.readString(NAME);
+ os.write(NAME, name);
+ break;
+ case (int) TAG:
+ String tag = pis.readString(TAG);
+ os.write(TAG, tag);
+ break;
+ default:
+ throw new RuntimeException(
+ "Unexpected field id " + pis.getFieldNumber());
+ }
+ }
+
+ pis.end(inGroupToken);
+ os.end(outGroupToken);
+ }
+ }
+
+ os.end(outProtologViewerConfigToken);
+
+ ctx.flush();
+ });
+
+ mDataSource.flush();
+ }
+
+ private void logToLogcat(String tag, LogLevel level, long messageHash,
+ @Nullable String messageString, Object[] args) {
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToLogcat");
+ try {
+ doLogToLogcat(tag, level, messageHash, messageString, args);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogToLogcat(String tag, LogLevel level, long messageHash,
+ @androidx.annotation.Nullable String messageString, Object[] args) {
+ String message = null;
+ if (messageString == null) {
+ messageString = mViewerConfigReader.getViewerString(messageHash);
+ }
+ if (messageString != null) {
+ if (args != null) {
+ try {
+ message = TextUtils.formatSimple(messageString, args);
+ } catch (Exception ex) {
+ Slog.w(LOG_TAG, "Invalid ProtoLog format string.", ex);
+ }
+ } else {
+ message = messageString;
+ }
+ }
+ if (message == null) {
+ StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE (" + messageHash + ")");
+ for (Object o : args) {
+ builder.append(" ").append(o);
+ }
+ message = builder.toString();
+ }
+ passToLogcat(tag, level, message);
+ }
+
+ /**
+ * SLog wrapper.
+ */
+ @VisibleForTesting
+ public void passToLogcat(String tag, LogLevel level, String message) {
+ switch (level) {
+ case DEBUG:
+ Slog.d(tag, message);
+ break;
+ case VERBOSE:
+ Slog.v(tag, message);
+ break;
+ case INFO:
+ Slog.i(tag, message);
+ break;
+ case WARN:
+ Slog.w(tag, message);
+ break;
+ case ERROR:
+ Slog.e(tag, message);
+ break;
+ case WTF:
+ Slog.wtf(tag, message);
+ break;
+ }
+ }
+
+ private void logToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+ Object[] args, long tsNanos) {
+ if (!isProtoEnabled()) {
+ return;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToProto");
+ try {
+ doLogToProto(level, groupName, messageHash, paramsMask, args, tsNanos);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ private void doLogToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+ Object[] args, long tsNanos) {
+ mDataSource.trace(ctx -> {
+ final ProtoLogDataSource.TlsState tlsState = ctx.getCustomTlsState();
+ final LogLevel logFrom = tlsState.getLogFromLevel(groupName);
+
+ if (level.ordinal() < logFrom.ordinal()) {
+ return;
+ }
+
+ if (args != null) {
+ // Intern all string params before creating the trace packet for the proto
+ // message so that the interned strings appear before in the trace to make the
+ // trace processing easier.
+ int argIndex = 0;
+ for (Object o : args) {
+ int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
+ if (type == LogDataType.STRING) {
+ internStringArg(ctx, o.toString());
+ }
+ argIndex++;
+ }
+ }
+
+ int internedStacktrace = 0;
+ if (tlsState.getShouldCollectStacktrace(groupName)) {
+ // Intern stackstraces before creating the trace packet for the proto message so
+ // that the interned stacktrace strings appear before in the trace to make the
+ // trace processing easier.
+ String stacktrace = collectStackTrace();
+ internedStacktrace = internStacktraceString(ctx, stacktrace);
+ }
+
+ final ProtoOutputStream os = ctx.newTracePacket();
+ os.write(TIMESTAMP, tsNanos);
+ long token = os.start(PROTOLOG_MESSAGE);
+ os.write(MESSAGE_ID, messageHash);
+
+ boolean needsIncrementalState = false;
+
+ if (args != null) {
+
+ int argIndex = 0;
+ LongArray longParams = new LongArray();
+ ArrayList<Double> doubleParams = new ArrayList<>();
+ ArrayList<Boolean> booleanParams = new ArrayList<>();
+ for (Object o : args) {
+ int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
+ try {
+ switch (type) {
+ case LogDataType.STRING:
+ final int internedStringId = internStringArg(ctx, o.toString());
+ os.write(STR_PARAM_IIDS, internedStringId);
+ needsIncrementalState = true;
+ break;
+ case LogDataType.LONG:
+ longParams.add(((Number) o).longValue());
+ break;
+ case LogDataType.DOUBLE:
+ doubleParams.add(((Number) o).doubleValue());
+ break;
+ case LogDataType.BOOLEAN:
+ booleanParams.add((boolean) o);
+ break;
+ }
+ } catch (ClassCastException ex) {
+ Slog.e(LOG_TAG, "Invalid ProtoLog paramsMask", ex);
+ }
+ argIndex++;
+ }
+
+ for (int i = 0; i < longParams.size(); ++i) {
+ os.write(SINT64_PARAMS, longParams.get(i));
+ }
+ doubleParams.forEach(it -> os.write(DOUBLE_PARAMS, it));
+ // Converting booleans to int because Perfetto doesn't yet support repeated
+ // booleans, so we use a repeated integers instead (b/313651412).
+ booleanParams.forEach(it -> os.write(BOOLEAN_PARAMS, it ? 1 : 0));
+ }
+
+ if (tlsState.getShouldCollectStacktrace(groupName)) {
+ os.write(STACKTRACE_IID, internedStacktrace);
+ }
+
+ os.end(token);
+
+ if (needsIncrementalState) {
+ os.write(SEQUENCE_FLAGS, SEQ_NEEDS_INCREMENTAL_STATE);
+ }
+
+ });
+ }
+
+ private static final int STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL = 12;
+
+ private String collectStackTrace() {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ StringWriter sw = new StringWriter();
+ try (PrintWriter pw = new PrintWriter(sw)) {
+ for (int i = STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL; i < stackTrace.length; ++i) {
+ pw.println("\tat " + stackTrace[i]);
+ }
+ }
+
+ return sw.toString();
+ }
+
+ private int internStacktraceString(TracingContext<ProtoLogDataSource.Instance,
+ ProtoLogDataSource.TlsState,
+ ProtoLogDataSource.IncrementalState> ctx,
+ String stacktrace) {
+ final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+ return internString(ctx, incrementalState.stacktraceInterningMap,
+ PROTOLOG_STACKTRACE, stacktrace);
+ }
+
+ private int internStringArg(
+ TracingContext<ProtoLogDataSource.Instance,
+ ProtoLogDataSource.TlsState,
+ ProtoLogDataSource.IncrementalState> ctx,
+ String string
+ ) {
+ final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+ return internString(ctx, incrementalState.argumentInterningMap,
+ PROTOLOG_STRING_ARGS, string);
+ }
+
+ private int internString(
+ TracingContext<ProtoLogDataSource.Instance,
+ ProtoLogDataSource.TlsState,
+ ProtoLogDataSource.IncrementalState> ctx,
+ Map<String, Integer> internMap,
+ long fieldId,
+ String string
+ ) {
+ final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+
+ if (!incrementalState.clearReported) {
+ final ProtoOutputStream os = ctx.newTracePacket();
+ os.write(SEQUENCE_FLAGS, SEQ_INCREMENTAL_STATE_CLEARED);
+ incrementalState.clearReported = true;
+ }
+
+ if (!internMap.containsKey(string)) {
+ final int internedIndex = internMap.size() + 1;
+ internMap.put(string, internedIndex);
+
+ final ProtoOutputStream os = ctx.newTracePacket();
+ final long token = os.start(INTERNED_DATA);
+ final long innerToken = os.start(fieldId);
+ os.write(IID, internedIndex);
+ os.write(STR, string.getBytes());
+ os.end(innerToken);
+ os.end(token);
+ }
+
+ return internMap.get(string);
+ }
+
+ /**
+ * Responds to a shell command.
+ */
+ public int onShellCommand(ShellCommand shell) {
+ PrintWriter pw = shell.getOutPrintWriter();
+ String cmd = shell.getNextArg();
+ if (cmd == null) {
+ return unknownCommand(pw);
+ }
+ ArrayList<String> args = new ArrayList<>();
+ String arg;
+ while ((arg = shell.getNextArg()) != null) {
+ args.add(arg);
+ }
+ final ILogger logger = (msg) -> logAndPrintln(pw, msg);
+ String[] groups = args.toArray(new String[args.size()]);
+ switch (cmd) {
+ case "enable-text":
+ return this.startLoggingToLogcat(groups, logger);
+ case "disable-text":
+ return this.stopLoggingToLogcat(groups, logger);
+ default:
+ return unknownCommand(pw);
+ }
+ }
+
+ private int unknownCommand(PrintWriter pw) {
+ pw.println("Unknown command");
+ pw.println("Window manager logging options:");
+ pw.println(" enable-text [group...]: Enable logcat logging for given groups");
+ pw.println(" disable-text [group...]: Disable logcat logging for given groups");
+ return -1;
+ }
+
+ /**
+ * Returns {@code true} iff logging to proto is enabled.
+ */
+ public boolean isProtoEnabled() {
+ return mTracingInstances.get() > 0;
+ }
+
+ /**
+ * Start text logging
+ * @param groups Groups to start text logging for
+ * @param logger A logger to write status updates to
+ * @return status code
+ */
+ public int startLoggingToLogcat(String[] groups, ILogger logger) {
+ mViewerConfigReader.loadViewerConfig(logger);
+ return setTextLogging(true, logger, groups);
+ }
+
+ /**
+ * Stop text logging
+ * @param groups Groups to start text logging for
+ * @param logger A logger to write status updates to
+ * @return status code
+ */
+ public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+ mViewerConfigReader.unloadViewerConfig();
+ return setTextLogging(false, logger, groups);
+ }
+
+ /**
+ * Start logging the stack trace of the when the log message happened for target groups
+ * @return status code
+ */
+ public int startLoggingStackTrace(String[] groups, ILogger logger) {
+ return -1;
+ }
+
+ /**
+ * Stop logging the stack trace of the when the log message happened for target groups
+ * @return status code
+ */
+ public int stopLoggingStackTrace() {
+ return -1;
+ }
+
+ private int setTextLogging(boolean value, ILogger logger, String... groups) {
+ for (int i = 0; i < groups.length; i++) {
+ String group = groups[i];
+ IProtoLogGroup g = mLogGroups.get(group);
+ if (g != null) {
+ g.setLogToLogcat(value);
+ } else {
+ logger.log("No IProtoLogGroup named " + group);
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
+ Slog.i(LOG_TAG, msg);
+ if (pw != null) {
+ pw.println(msg);
+ pw.flush();
+ }
+ }
+}
+
diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
new file mode 100644
index 0000000..a8ff75d
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2023 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.protolog;
+
+import static perfetto.protos.PerfettoTrace.DataSourceConfig.PROTOLOG_CONFIG;
+import static perfetto.protos.PerfettoTrace.ProtoLogConfig.GROUP_OVERRIDES;
+import static perfetto.protos.PerfettoTrace.ProtoLogConfig.TRACING_MODE;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.COLLECT_STACKTRACE;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.LOG_FROM;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.GROUP_NAME;
+
+import android.tracing.perfetto.CreateIncrementalStateArgs;
+import android.tracing.perfetto.CreateTlsStateArgs;
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.FlushCallbackArguments;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.WireTypeMismatchException;
+
+import com.android.internal.protolog.common.LogLevel;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import perfetto.protos.PerfettoTrace;
+
+public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance,
+ ProtoLogDataSource.TlsState,
+ ProtoLogDataSource.IncrementalState> {
+
+ private final Runnable mOnStart;
+ private final Runnable mOnFlush;
+ private final Runnable mOnStop;
+
+ public ProtoLogDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) {
+ super("android.protolog");
+ this.mOnStart = onStart;
+ this.mOnFlush = onFlush;
+ this.mOnStop = onStop;
+ }
+
+ @Override
+ public Instance createInstance(ProtoInputStream configStream, int instanceIndex) {
+ ProtoLogConfig config = null;
+
+ try {
+ while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ try {
+ if (configStream.getFieldNumber() == (int) PROTOLOG_CONFIG) {
+ if (config != null) {
+ throw new RuntimeException("ProtoLog config already set in loop");
+ }
+ config = readProtoLogConfig(configStream);
+ }
+ } catch (WireTypeMismatchException e) {
+ throw new RuntimeException("Failed to parse ProtoLog DataSource config", e);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read ProtoLog DataSource config", e);
+ }
+
+ if (config == null) {
+ // No config found
+ config = ProtoLogConfig.DEFAULT;
+ }
+
+ return new Instance(
+ this, instanceIndex, config, mOnStart, mOnFlush, mOnStop);
+ }
+
+ @Override
+ public TlsState createTlsState(CreateTlsStateArgs<Instance> args) {
+ try (Instance dsInstance = args.getDataSourceInstanceLocked()) {
+ if (dsInstance == null) {
+ // Datasource instance has been removed
+ return new TlsState(ProtoLogConfig.DEFAULT);
+ }
+ return new TlsState(dsInstance.mConfig);
+ }
+ }
+
+ @Override
+ public IncrementalState createIncrementalState(CreateIncrementalStateArgs<Instance> args) {
+ return new IncrementalState();
+ }
+
+ public static class TlsState {
+ private final ProtoLogConfig mConfig;
+
+ private TlsState(ProtoLogConfig config) {
+ this.mConfig = config;
+ }
+
+ /**
+ * Get the log from level for a group.
+ * @param groupTag The tag of the group to get the log from level.
+ * @return The lowest LogLevel (inclusive) to log message from.
+ */
+ public LogLevel getLogFromLevel(String groupTag) {
+ return getConfigFor(groupTag).logFrom;
+ }
+
+ /**
+ * Get if the stacktrace for the log message should be collected for this group.
+ * @param groupTag The tag of the group to get whether or not a stacktrace was requested.
+ * @return True iff a stacktrace was requested to be collected from this group in the
+ * tracing config.
+ */
+ public boolean getShouldCollectStacktrace(String groupTag) {
+ return getConfigFor(groupTag).collectStackTrace;
+ }
+
+ private GroupConfig getConfigFor(String groupTag) {
+ return mConfig.getConfigFor(groupTag);
+ }
+ }
+
+ public static class IncrementalState {
+ public final Map<String, Integer> argumentInterningMap = new HashMap<>();
+ public final Map<String, Integer> stacktraceInterningMap = new HashMap<>();
+ public boolean clearReported = false;
+ }
+
+ private static class ProtoLogConfig {
+ private final LogLevel mDefaultLogFromLevel;
+ private final Map<String, GroupConfig> mGroupConfigs;
+
+ private static final ProtoLogConfig DEFAULT =
+ new ProtoLogConfig(LogLevel.WTF, new HashMap<>());
+
+ private ProtoLogConfig(
+ LogLevel defaultLogFromLevel, Map<String, GroupConfig> groupConfigs) {
+ this.mDefaultLogFromLevel = defaultLogFromLevel;
+ this.mGroupConfigs = groupConfigs;
+ }
+
+ private GroupConfig getConfigFor(String groupTag) {
+ return mGroupConfigs.getOrDefault(groupTag, getDefaultGroupConfig());
+ }
+
+ private GroupConfig getDefaultGroupConfig() {
+ return new GroupConfig(mDefaultLogFromLevel, false);
+ }
+ }
+
+ public static class GroupConfig {
+ public final LogLevel logFrom;
+ public final boolean collectStackTrace;
+
+ public GroupConfig(LogLevel logFromLevel, boolean collectStackTrace) {
+ this.logFrom = logFromLevel;
+ this.collectStackTrace = collectStackTrace;
+ }
+ }
+
+ private ProtoLogConfig readProtoLogConfig(ProtoInputStream configStream)
+ throws IOException {
+ final long config_token = configStream.start(PROTOLOG_CONFIG);
+
+ LogLevel defaultLogFromLevel = LogLevel.WTF;
+ final Map<String, GroupConfig> groupConfigs = new HashMap<>();
+
+ while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (configStream.getFieldNumber() == (int) TRACING_MODE) {
+ int tracingMode = configStream.readInt(TRACING_MODE);
+ switch (tracingMode) {
+ case PerfettoTrace.ProtoLogConfig.DEFAULT:
+ break;
+ case PerfettoTrace.ProtoLogConfig.ENABLE_ALL:
+ defaultLogFromLevel = LogLevel.DEBUG;
+ break;
+ default:
+ throw new RuntimeException("Unhandled ProtoLog tracing mode type");
+ }
+ }
+ if (configStream.getFieldNumber() == (int) GROUP_OVERRIDES) {
+ final long group_overrides_token = configStream.start(GROUP_OVERRIDES);
+
+ String tag = null;
+ LogLevel logFromLevel = defaultLogFromLevel;
+ boolean collectStackTrace = false;
+ while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (configStream.getFieldNumber() == (int) GROUP_NAME) {
+ tag = configStream.readString(GROUP_NAME);
+ }
+ if (configStream.getFieldNumber() == (int) LOG_FROM) {
+ final int logFromInt = configStream.readInt(LOG_FROM);
+ switch (logFromInt) {
+ case (PerfettoTrace.PROTOLOG_LEVEL_DEBUG): {
+ logFromLevel = LogLevel.DEBUG;
+ break;
+ }
+ case (PerfettoTrace.PROTOLOG_LEVEL_VERBOSE): {
+ logFromLevel = LogLevel.VERBOSE;
+ break;
+ }
+ case (PerfettoTrace.PROTOLOG_LEVEL_INFO): {
+ logFromLevel = LogLevel.INFO;
+ break;
+ }
+ case (PerfettoTrace.PROTOLOG_LEVEL_WARN): {
+ logFromLevel = LogLevel.WARN;
+ break;
+ }
+ case (PerfettoTrace.PROTOLOG_LEVEL_ERROR): {
+ logFromLevel = LogLevel.ERROR;
+ break;
+ }
+ case (PerfettoTrace.PROTOLOG_LEVEL_WTF): {
+ logFromLevel = LogLevel.WTF;
+ break;
+ }
+ default: {
+ throw new RuntimeException("Unhandled log level");
+ }
+ }
+ }
+ if (configStream.getFieldNumber() == (int) COLLECT_STACKTRACE) {
+ collectStackTrace = configStream.readBoolean(COLLECT_STACKTRACE);
+ }
+ }
+
+ if (tag == null) {
+ throw new RuntimeException("Failed to decode proto config. "
+ + "Got a group override without a group tag.");
+ }
+
+ groupConfigs.put(tag, new GroupConfig(logFromLevel, collectStackTrace));
+
+ configStream.end(group_overrides_token);
+ }
+ }
+
+ configStream.end(config_token);
+
+ return new ProtoLogConfig(defaultLogFromLevel, groupConfigs);
+ }
+
+ public static class Instance extends DataSourceInstance {
+
+ private final Runnable mOnStart;
+ private final Runnable mOnFlush;
+ private final Runnable mOnStop;
+ private final ProtoLogConfig mConfig;
+
+ public Instance(
+ DataSource<Instance, TlsState, IncrementalState> dataSource,
+ int instanceIdx,
+ ProtoLogConfig config,
+ Runnable onStart,
+ Runnable onFlush,
+ Runnable onStop
+ ) {
+ super(dataSource, instanceIdx);
+ this.mOnStart = onStart;
+ this.mOnFlush = onFlush;
+ this.mOnStop = onStop;
+ this.mConfig = config;
+ }
+
+ @Override
+ public void onStart(StartCallbackArguments args) {
+ this.mOnStart.run();
+ }
+
+ @Override
+ public void onFlush(FlushCallbackArguments args) {
+ this.mOnFlush.run();
+ }
+
+ @Override
+ public void onStop(StopCallbackArguments args) {
+ this.mOnStop.run();
+ }
+ }
+}
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 527cfdd..78bed94 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -16,30 +16,35 @@
package com.android.internal.protolog;
+import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH;
+import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH;
+import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH;
+
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.IProtoLog;
import com.android.internal.protolog.common.IProtoLogGroup;
-
-import java.io.File;
+import com.android.internal.protolog.common.LogLevel;
+import com.android.internal.protolog.common.ProtoLogToolInjected;
/**
* A service for the ProtoLog logging system.
*/
-public class ProtoLogImpl extends BaseProtoLogImpl {
- private static final int BUFFER_CAPACITY = 1024 * 1024;
- private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.winscope";
- private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz";
- private static final int PER_CHUNK_SIZE = 1024;
+public class ProtoLogImpl {
+ private static IProtoLog sServiceInstance = null;
- private static ProtoLogImpl sServiceInstance = null;
+ @ProtoLogToolInjected(VIEWER_CONFIG_PATH)
+ private static String sViewerConfigPath;
- static {
- addLogGroupEnum(ProtoLogGroup.values());
- }
+ @ProtoLogToolInjected(LEGACY_VIEWER_CONFIG_PATH)
+ private static String sLegacyViewerConfigPath;
+
+ @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
+ private static String sLegacyOutputFilePath;
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void d(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance()
@@ -47,7 +52,7 @@
}
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void v(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
@@ -55,21 +60,21 @@
}
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void i(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
}
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void w(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
}
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void e(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance()
@@ -77,40 +82,36 @@
}
/** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
+ public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask,
@Nullable String messageString,
Object... args) {
getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
}
- /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
public static boolean isEnabled(IProtoLogGroup group) {
- return group.isLogToLogcat()
- || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
+ // TODO: Implement for performance reasons, with optional level parameter?
+ return true;
}
/**
* Returns the single instance of the ProtoLogImpl singleton class.
*/
- public static synchronized ProtoLogImpl getSingleInstance() {
+ public static synchronized IProtoLog getSingleInstance() {
if (sServiceInstance == null) {
- sServiceInstance = new ProtoLogImpl(
- new File(LOG_FILENAME)
- , BUFFER_CAPACITY
- , new ProtoLogViewerConfigReader()
- , PER_CHUNK_SIZE);
+ if (android.tracing.Flags.perfettoProtolog()) {
+ sServiceInstance =
+ new PerfettoProtoLogImpl(sViewerConfigPath);
+ } else {
+ sServiceInstance =
+ new LegacyProtoLogImpl(sLegacyOutputFilePath, sLegacyViewerConfigPath);
+ }
}
return sServiceInstance;
}
@VisibleForTesting
- public static synchronized void setSingleInstance(@Nullable ProtoLogImpl instance) {
+ public static synchronized void setSingleInstance(@Nullable IProtoLog instance) {
sServiceInstance = instance;
}
-
- public ProtoLogImpl(File logFile, int bufferCapacity,
- ProtoLogViewerConfigReader viewConfigReader, int perChunkSize) {
- super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader, perChunkSize);
- }
}
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index aa30a77..3c206ac 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -1,48 +1,30 @@
-/*
- * Copyright (C) 2020 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.protolog;
-import android.annotation.Nullable;
-import android.util.Slog;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MESSAGES;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
-import org.json.JSONException;
-import org.json.JSONObject;
+import android.util.proto.ProtoInputStream;
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import com.android.internal.protolog.common.ILogger;
+
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.util.Iterator;
import java.util.Map;
-import java.util.TreeMap;
-import java.util.zip.GZIPInputStream;
-/**
- * Handles loading and parsing of ProtoLog viewer configuration.
- */
public class ProtoLogViewerConfigReader {
- private static final String TAG = "ProtoLogViewerConfigReader";
- private Map<Integer, String> mLogMessageMap = null;
+ private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+ private Map<Long, String> mLogMessageMap = null;
- /** Returns message format string for its hash or null if unavailable. */
- public synchronized String getViewerString(int messageHash) {
+ public ProtoLogViewerConfigReader(
+ ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+ this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+ }
+
+ /**
+ * Returns message format string for its hash or null if unavailable
+ * or the viewer config is not loaded into memory.
+ */
+ public synchronized String getViewerString(long messageHash) {
if (mLogMessageMap != null) {
return mLogMessageMap.get(messageHash);
} else {
@@ -51,75 +33,61 @@
}
/**
- * Reads the specified viewer configuration file. Does nothing if the config is already loaded.
+ * Loads the viewer config into memory. No-op if already loaded in memory.
*/
- public synchronized void loadViewerConfig(PrintWriter pw, String viewerConfigFilename) {
- try {
- loadViewerConfig(new GZIPInputStream(new FileInputStream(viewerConfigFilename)));
- logAndPrintln(pw, "Loaded " + mLogMessageMap.size()
- + " log definitions from " + viewerConfigFilename);
- } catch (FileNotFoundException e) {
- logAndPrintln(pw, "Unable to load log definitions: File "
- + viewerConfigFilename + " not found." + e);
- } catch (IOException e) {
- logAndPrintln(pw, "Unable to load log definitions: IOException while reading "
- + viewerConfigFilename + ". " + e);
- } catch (JSONException e) {
- logAndPrintln(pw, "Unable to load log definitions: JSON parsing exception while reading "
- + viewerConfigFilename + ". " + e);
- }
- }
-
- /**
- * Reads the specified viewer configuration input stream.
- * Does nothing if the config is already loaded.
- */
- public synchronized void loadViewerConfig(InputStream viewerConfigInputStream)
- throws IOException, JSONException {
+ public synchronized void loadViewerConfig(ILogger logger) {
if (mLogMessageMap != null) {
return;
}
- InputStreamReader config = new InputStreamReader(viewerConfigInputStream);
- BufferedReader reader = new BufferedReader(config);
- StringBuilder builder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- builder.append(line).append('\n');
- }
- reader.close();
- JSONObject json = new JSONObject(builder.toString());
- JSONObject messages = json.getJSONObject("messages");
- mLogMessageMap = new TreeMap<>();
- Iterator it = messages.keys();
- while (it.hasNext()) {
- String key = (String) it.next();
- try {
- int hash = Integer.parseInt(key);
- JSONObject val = messages.getJSONObject(key);
- String msg = val.getString("message");
- mLogMessageMap.put(hash, msg);
- } catch (NumberFormatException expected) {
- // Not a messageHash - skip it
- }
+ try {
+ doLoadViewerConfig();
+ logger.log("Loaded " + mLogMessageMap.size() + " log definitions");
+ } catch (IOException e) {
+ logger.log("Unable to load log definitions: "
+ + "IOException while processing viewer config" + e);
}
}
/**
- * Returns the number of loaded log definitions kept in memory.
+ * Unload the viewer config from memory.
*/
- public synchronized int knownViewerStringsNumber() {
- if (mLogMessageMap != null) {
- return mLogMessageMap.size();
- }
- return 0;
+ public synchronized void unloadViewerConfig() {
+ mLogMessageMap = null;
}
- static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
- Slog.i(TAG, msg);
- if (pw != null) {
- pw.println(msg);
- pw.flush();
+ private void doLoadViewerConfig() throws IOException {
+ final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (pis.getFieldNumber() == (int) MESSAGES) {
+ final long inMessageToken = pis.start(MESSAGES);
+
+ long messageId = 0;
+ String message = null;
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (pis.getFieldNumber()) {
+ case (int) MESSAGE_ID:
+ messageId = pis.readLong(MESSAGE_ID);
+ break;
+ case (int) MESSAGE:
+ message = pis.readString(MESSAGE);
+ break;
+ }
+ }
+
+ if (messageId == 0) {
+ throw new IOException("Failed to get message id");
+ }
+
+ if (message == null) {
+ throw new IOException("Failed to get message string");
+ }
+
+ mLogMessageMap.put(messageId, message);
+
+ pis.end(inMessageToken);
+ }
}
}
}
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
new file mode 100644
index 0000000..334f548
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.protolog;
+
+import android.util.proto.ProtoInputStream;
+
+public interface ViewerConfigInputStreamProvider {
+ /**
+ * @return a ProtoInputStream.
+ */
+ ProtoInputStream getInputStream();
+}
diff --git a/core/java/com/android/internal/protolog/common/ILogger.java b/core/java/com/android/internal/protolog/common/ILogger.java
new file mode 100644
index 0000000..cc6fa5e
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/ILogger.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 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.protolog.common;
+
+public interface ILogger {
+ /**
+ * Log a message.
+ * @param message The log message.
+ */
+ void log(String message);
+}
diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java
new file mode 100644
index 0000000..c06d14b
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/IProtoLog.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.protolog.common;
+
+/**
+ * Interface for ProtoLog implementations.
+ */
+public interface IProtoLog {
+
+ /**
+ * Log a ProtoLog message
+ * @param logLevel Log level of the proto message.
+ * @param group The group this message belongs to.
+ * @param messageHash The hash of the message.
+ * @param paramsMask The parameters mask of the message.
+ * @param messageString The message string.
+ * @param args The arguments of the message.
+ */
+ void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
+ String messageString, Object[] args);
+
+ /**
+ * Check if ProtoLog is tracing.
+ * @return true iff a ProtoLog tracing session is active.
+ */
+ boolean isProtoEnabled();
+
+ /**
+ * Start logging log groups to logcat
+ * @param groups Groups to start text logging for
+ * @return status code
+ */
+ int startLoggingToLogcat(String[] groups, ILogger logger);
+
+ /**
+ * Stop logging log groups to logcat
+ * @param groups Groups to start text logging for
+ * @return status code
+ */
+ int stopLoggingToLogcat(String[] groups, ILogger logger);
+}
diff --git a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
index e3db468..4e9686f99 100644
--- a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
+++ b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
@@ -26,6 +26,7 @@
boolean isEnabled();
/**
+ * @deprecated TODO(b/324128613) remove once we migrate fully to Perfetto
* is binary logging enabled for the group.
*/
boolean isLogToProto();
diff --git a/core/java/com/android/internal/protolog/common/ProtoLog.java b/core/java/com/android/internal/protolog/common/ProtoLog.java
index 8870096..18e3f66 100644
--- a/core/java/com/android/internal/protolog/common/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/common/ProtoLog.java
@@ -16,8 +16,6 @@
package com.android.internal.protolog.common;
-import android.util.Log;
-
/**
* ProtoLog API - exposes static logging methods. Usage of this API is similar
* to {@code android.utils.Log} class. Instead of plain text log messages each call consists of
@@ -55,9 +53,6 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.d(group.getTag(), String.format(messageString, args));
- }
}
/**
@@ -73,9 +68,6 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.v(group.getTag(), String.format(messageString, args));
- }
}
/**
@@ -91,9 +83,6 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.i(group.getTag(), String.format(messageString, args));
- }
}
/**
@@ -109,9 +98,6 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.w(group.getTag(), String.format(messageString, args));
- }
}
/**
@@ -127,9 +113,6 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.e(group.getTag(), String.format(messageString, args));
- }
}
/**
@@ -145,8 +128,30 @@
throw new UnsupportedOperationException(
"ProtoLog calls MUST be processed with ProtoLogTool");
}
- if (group.isLogToLogcat()) {
- Log.wtf(group.getTag(), String.format(messageString, args));
+ }
+
+ /**
+ * Check if ProtoLog isEnabled for a target group.
+ * @param group Group to check enable status of.
+ * @return true iff this is being logged.
+ */
+ public static boolean isEnabled(IProtoLogGroup group) {
+ if (REQUIRE_PROTOLOGTOOL) {
+ throw new UnsupportedOperationException(
+ "ProtoLog calls MUST be processed with ProtoLogTool");
}
+ return false;
+ }
+
+ /**
+ * Get the single ProtoLog instance.
+ * @return A singleton instance of ProtoLog.
+ */
+ public static IProtoLog getSingleInstance() {
+ if (REQUIRE_PROTOLOGTOOL) {
+ throw new UnsupportedOperationException(
+ "ProtoLog calls MUST be processed with ProtoLogTool");
+ }
+ return null;
}
}
diff --git a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
new file mode 100644
index 0000000..ffd0d76
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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.protolog.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface ProtoLogToolInjected {
+ enum Value { VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH }
+
+ Value value();
+}
diff --git a/core/proto/android/internal/protolog.proto b/core/proto/android/internal/protolog.proto
index fee7a87..9e205e2 100644
--- a/core/proto/android/internal/protolog.proto
+++ b/core/proto/android/internal/protolog.proto
@@ -27,7 +27,7 @@
option (.android.msg_privacy).dest = DEST_LOCAL;
/* log statement identifier, created from message string and log level. */
- optional sfixed32 message_hash = 1;
+ optional sfixed32 message_hash_legacy = 1 [deprecated = true];
/* log time, relative to the elapsed system time clock. */
optional fixed64 elapsed_realtime_nanos = 2;
/* string parameters passed to the log call. */
@@ -38,6 +38,9 @@
repeated double double_params = 5 [packed=true];
/* boolean parameters passed to the log call. */
repeated bool boolean_params = 6 [packed=true];
+
+ /* log statement identifier, created from message string and log level. */
+ optional sfixed64 message_hash = 7;
}
/* represents a log file containing ProtoLog log entries.
diff --git a/core/res/res/values/config_battery_saver.xml b/core/res/res/values/config_battery_saver.xml
index e1b0ef4..551cd0a 100644
--- a/core/res/res/values/config_battery_saver.xml
+++ b/core/res/res/values/config_battery_saver.xml
@@ -33,6 +33,9 @@
<!-- Whether or not battery saver should be "sticky" when manually enabled. -->
<bool name="config_batterySaverStickyBehaviourDisabled">false</bool>
+ <!-- Whether to enable "Battery Saver turned off" notification. -->
+ <bool name="config_batterySaverTurnedOffNotificationEnabled">true</bool>
+
<!-- Config flag to track default disable threshold for Dynamic power savings enabled battery saver. -->
<integer name="config_dynamicPowerSavingsDefaultDisableThreshold">80</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3284791..b950a60 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4123,6 +4123,7 @@
<java-symbol type="bool" name="config_batterySaverSupported" />
<java-symbol type="string" name="config_batterySaverDeviceSpecificConfig" />
<java-symbol type="bool" name="config_batterySaverStickyBehaviourDisabled" />
+ <java-symbol type="bool" name="config_batterySaverTurnedOffNotificationEnabled" />
<java-symbol type="integer" name="config_dynamicPowerSavingsDefaultDisableThreshold" />
<java-symbol type="string" name="config_batterySaverScheduleProvider" />
<java-symbol type="string" name="config_powerSaveModeChangedListenerPackage" />
diff --git a/core/tests/coretests/src/android/os/WakeLockStatsTest.java b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
index 2675ba0..f3b18c8 100644
--- a/core/tests/coretests/src/android/os/WakeLockStatsTest.java
+++ b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
@@ -29,10 +29,114 @@
public class WakeLockStatsTest {
@Test
+ public void isDataValidOfWakeLockData_invalid_returnFalse() {
+ WakeLockStats.WakeLockData wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 0, /* totalTimeHeldMs= */ 10, /* timeHeldMs= */ 0);
+ assertThat(wakeLockData.isDataValid()).isFalse();
+
+ wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 1, /* totalTimeHeldMs= */ 0, /* timeHeldMs= */ 0);
+ assertThat(wakeLockData.isDataValid()).isFalse();
+
+ wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 1, /* totalTimeHeldMs= */ 10, /* timeHeldMs= */ -10);
+ assertThat(wakeLockData.isDataValid()).isFalse();
+
+ wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 1, /* totalTimeHeldMs= */ 10, /* timeHeldMs= */ 20);
+ assertThat(wakeLockData.isDataValid()).isFalse();
+ }
+
+ @Test
+ public void isDataValidOfWakeLockData_empty_returnTrue() {
+ final WakeLockStats.WakeLockData wakeLockData = WakeLockStats.WakeLockData.EMPTY;
+ assertThat(wakeLockData.isDataValid()).isTrue();
+ }
+
+ @Test
+ public void isDataValidOfWakeLockData_valid_returnTrue() {
+ WakeLockStats.WakeLockData wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 1, /* totalTimeHeldMs= */ 10, /* timeHeldMs= */ 5);
+ assertThat(wakeLockData.isDataValid()).isTrue();
+ }
+
+ @Test
+ public void isDataValidOfWakeLock_zeroTotalHeldMs_returnFalse() {
+ final WakeLockStats.WakeLockData wakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 0, /* totalTimeHeldMs= */ 0, /* timeHeldMs= */ 0);
+
+ assertThat(WakeLockStats.WakeLock.isDataValid(wakeLockData, wakeLockData)).isFalse();
+ }
+
+ @Test
+ public void isDataValidOfWakeLock_invalidData_returnFalse() {
+ final WakeLockStats.WakeLockData totalWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 6, /* totalTimeHeldMs= */ 60, /* timeHeldMs= */ 20);
+ final WakeLockStats.WakeLockData backgroundWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 0, /* totalTimeHeldMs= */ 10, /* timeHeldMs= */ 0);
+
+ assertThat(WakeLockStats.WakeLock.isDataValid(totalWakeLockData, backgroundWakeLockData))
+ .isFalse();
+ }
+
+ @Test
+ public void isDataValidOfWakeLock_totalSmallerThanBackground_returnFalse() {
+ final WakeLockStats.WakeLockData totalWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 10, /* totalTimeHeldMs= */ 60, /* timeHeldMs= */ 50);
+ final WakeLockStats.WakeLockData backgroundWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 6, /* totalTimeHeldMs= */ 100, /* timeHeldMs= */ 30);
+
+ assertThat(WakeLockStats.WakeLock.isDataValid(totalWakeLockData, backgroundWakeLockData))
+ .isFalse();
+ }
+
+ @Test
+ public void isDataValidOfWakeLock_returnTrue() {
+ final WakeLockStats.WakeLockData totalWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 10, /* totalTimeHeldMs= */ 100, /* timeHeldMs= */ 50);
+ final WakeLockStats.WakeLockData backgroundWakeLockData =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 6, /* totalTimeHeldMs= */ 60, /* timeHeldMs= */ 20);
+
+ assertThat(WakeLockStats.WakeLock.isDataValid(totalWakeLockData, backgroundWakeLockData))
+ .isTrue();
+ }
+
+ @Test
public void parcelablity() {
+ final WakeLockStats.WakeLockData totalWakeLockData1 =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 10, /* totalTimeHeldMs= */ 60, /* timeHeldMs= */ 50);
+ final WakeLockStats.WakeLockData backgroundWakeLockData1 =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 6, /* totalTimeHeldMs= */ 100, /* timeHeldMs= */ 30);
+ final WakeLockStats.WakeLock wakeLock1 =
+ new WakeLockStats.WakeLock(
+ 1, "foo", /* isAggregated= */ false, totalWakeLockData1,
+ backgroundWakeLockData1);
+ final WakeLockStats.WakeLockData totalWakeLockData2 =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 20, /* totalTimeHeldMs= */ 80, /* timeHeldMs= */ 30);
+ final WakeLockStats.WakeLockData backgroundWakeLockData2 =
+ new WakeLockStats.WakeLockData(
+ /* timesAcquired= */ 1, /* totalTimeHeldMs= */ 100, /* timeHeldMs= */ 30);
+ final WakeLockStats.WakeLock wakeLock2 =
+ new WakeLockStats.WakeLock(
+ 2, "bar", /* isAggregated= */ true, totalWakeLockData2,
+ backgroundWakeLockData2);
WakeLockStats wakeLockStats = new WakeLockStats(
- List.of(new WakeLockStats.WakeLock(1, "foo", 200, 3000, 40000),
- new WakeLockStats.WakeLock(2, "bar", 500, 6000, 70000)));
+ List.of(wakeLock1), List.of(wakeLock2));
Parcel parcel = Parcel.obtain();
wakeLockStats.writeToParcel(parcel, 0);
@@ -44,15 +148,28 @@
parcel.setDataPosition(0);
WakeLockStats actual = WakeLockStats.CREATOR.createFromParcel(parcel);
- assertThat(actual.getWakeLocks()).hasSize(2);
- WakeLockStats.WakeLock wl1 = actual.getWakeLocks().get(0);
- assertThat(wl1.uid).isEqualTo(1);
- assertThat(wl1.name).isEqualTo("foo");
- assertThat(wl1.timesAcquired).isEqualTo(200);
- assertThat(wl1.totalTimeHeldMs).isEqualTo(3000);
- assertThat(wl1.timeHeldMs).isEqualTo(40000);
+ assertThat(actual.getWakeLocks()).hasSize(1);
+ WakeLockStats.WakeLock actualWakelock = actual.getWakeLocks().get(0);
+ assertThat(actualWakelock.uid).isEqualTo(1);
+ assertThat(actualWakelock.name).isEqualTo("foo");
+ assertThat(actualWakelock.isAggregated).isFalse();
+ assertThat(actualWakelock.totalWakeLockData.timesAcquired).isEqualTo(10);
+ assertThat(actualWakelock.totalWakeLockData.totalTimeHeldMs).isEqualTo(60);
+ assertThat(actualWakelock.totalWakeLockData.timeHeldMs).isEqualTo(50);
+ assertThat(actualWakelock.backgroundWakeLockData.timesAcquired).isEqualTo(6);
+ assertThat(actualWakelock.backgroundWakeLockData.totalTimeHeldMs).isEqualTo(100);
+ assertThat(actualWakelock.backgroundWakeLockData.timeHeldMs).isEqualTo(30);
- WakeLockStats.WakeLock wl2 = actual.getWakeLocks().get(1);
- assertThat(wl2.uid).isEqualTo(2);
+ assertThat(actual.getAggregatedWakeLocks()).hasSize(1);
+ WakeLockStats.WakeLock actualAggregatedWakelock = actual.getAggregatedWakeLocks().get(0);
+ assertThat(actualAggregatedWakelock.uid).isEqualTo(2);
+ assertThat(actualAggregatedWakelock.name).isEqualTo("bar");
+ assertThat(actualAggregatedWakelock.isAggregated).isTrue();
+ assertThat(actualAggregatedWakelock.totalWakeLockData.timesAcquired).isEqualTo(20);
+ assertThat(actualAggregatedWakelock.totalWakeLockData.totalTimeHeldMs).isEqualTo(80);
+ assertThat(actualAggregatedWakelock.totalWakeLockData.timeHeldMs).isEqualTo(30);
+ assertThat(actualAggregatedWakelock.backgroundWakeLockData.timesAcquired).isEqualTo(1);
+ assertThat(actualAggregatedWakelock.backgroundWakeLockData.totalTimeHeldMs).isEqualTo(100);
+ assertThat(actualAggregatedWakelock.backgroundWakeLockData.timeHeldMs).isEqualTo(30);
}
-}
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 038c00e..64cbe7f 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -24,6 +24,8 @@
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
@@ -705,17 +707,51 @@
public void votePreferredFrameRate_voteFrameRate_aggregate() {
View view = new View(sContext);
attachViewToWindow(view);
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
sInstrumentation.runOnMainSync(() -> {
- ViewRootImpl viewRootImpl = view.getViewRootImpl();
assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1);
- viewRootImpl.votePreferredFrameRate(24);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_GTE);
assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1);
- viewRootImpl.votePreferredFrameRate(30);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_GTE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(30, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
assertEquals(viewRootImpl.getPreferredFrameRate(), 30, 0.1);
- viewRootImpl.votePreferredFrameRate(60);
+ // If there is a conflict, then set compatibility to
+ // FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ // Should be true since there is a conflict between 24 and 30.
+ assertEquals(viewRootImpl.isFrameRateConflicted(), true);
+ view.invalidate();
+ });
+ sInstrumentation.waitForIdleSync();
+
+ sInstrumentation.runOnMainSync(() -> {
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE);
assertEquals(viewRootImpl.getPreferredFrameRate(), 60, 0.1);
- viewRootImpl.votePreferredFrameRate(120);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_GTE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(120, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
assertEquals(viewRootImpl.getPreferredFrameRate(), 120, 0.1);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ // Should be false since 60 is a divisor of 120.
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE);
+ assertEquals(viewRootImpl.getPreferredFrameRate(), 120, 0.1);
+ // compatibility should be remained the same (FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ // since the frame rate 60 is smaller than 120.
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ // Should be false since 60 is a divisor of 120.
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+
});
}
@@ -842,14 +878,26 @@
sInstrumentation.runOnMainSync(() -> {
assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1);
- viewRootImpl.votePreferredFrameRate(24);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
+ viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
view.invalidate();
assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
});
Thread.sleep(delay);
assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1);
+ assertEquals(viewRootImpl.getFrameRateCompatibility(),
+ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+ assertEquals(viewRootImpl.isFrameRateConflicted(), false);
}
/**
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index 1fd1003..238a3e1 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -199,3 +199,8 @@
name: "services.core.protolog.json",
srcs: ["services.core.protolog.json"],
}
+
+filegroup {
+ name: "file-core.protolog.pb",
+ srcs: ["core.protolog.pb"],
+}
diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb
new file mode 100644
index 0000000..0415e44
--- /dev/null
+++ b/data/etc/core.protolog.pb
Binary files differ
diff --git a/graphics/java/android/graphics/pdf/PdfEditor.java b/graphics/java/android/graphics/pdf/PdfEditor.java
index 69e1982..3cd709e 100644
--- a/graphics/java/android/graphics/pdf/PdfEditor.java
+++ b/graphics/java/android/graphics/pdf/PdfEditor.java
@@ -25,9 +25,7 @@
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
-
import dalvik.system.CloseGuard;
-
import libcore.io.IoUtils;
import java.io.IOException;
@@ -39,12 +37,6 @@
*/
public final class PdfEditor {
- /**
- * Any call the native pdfium code has to be single threaded as the library does not support
- * parallel use.
- */
- private static final Object sPdfiumLock = new Object();
-
private final CloseGuard mCloseGuard = CloseGuard.get();
private long mNativeDocument;
@@ -87,7 +79,7 @@
}
mInput = input;
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
mNativeDocument = nativeOpen(mInput.getFd(), size);
try {
mPageCount = nativeGetPageCount(mNativeDocument);
@@ -120,7 +112,7 @@
throwIfClosed();
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
mPageCount = nativeRemovePage(mNativeDocument, pageIndex);
}
}
@@ -146,12 +138,12 @@
Point size = new Point();
getPageSize(pageIndex, size);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.ni(),
0, 0, size.x, size.y);
}
} else {
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.ni(),
clip.left, clip.top, clip.right, clip.bottom);
}
@@ -169,7 +161,7 @@
throwIfOutSizeNull(outSize);
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeGetPageSize(mNativeDocument, pageIndex, outSize);
}
}
@@ -185,7 +177,7 @@
throwIfOutMediaBoxNull(outMediaBox);
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
return nativeGetPageMediaBox(mNativeDocument, pageIndex, outMediaBox);
}
}
@@ -201,7 +193,7 @@
throwIfMediaBoxNull(mediaBox);
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeSetPageMediaBox(mNativeDocument, pageIndex, mediaBox);
}
}
@@ -217,7 +209,7 @@
throwIfOutCropBoxNull(outCropBox);
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
return nativeGetPageCropBox(mNativeDocument, pageIndex, outCropBox);
}
}
@@ -233,7 +225,7 @@
throwIfCropBoxNull(cropBox);
throwIfPageNotInDocument(pageIndex);
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeSetPageCropBox(mNativeDocument, pageIndex, cropBox);
}
}
@@ -246,7 +238,7 @@
public boolean shouldScaleForPrinting() {
throwIfClosed();
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
return nativeScaleForPrinting(mNativeDocument);
}
}
@@ -263,7 +255,7 @@
try {
throwIfClosed();
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeWrite(mNativeDocument, output.getFd());
}
} finally {
@@ -295,7 +287,7 @@
private void doClose() {
if (mNativeDocument != 0) {
- synchronized (sPdfiumLock) {
+ synchronized (PdfRenderer.sPdfiumLock) {
nativeClose(mNativeDocument);
}
mNativeDocument = 0;
diff --git a/graphics/java/android/graphics/pdf/PdfRenderer.java b/graphics/java/android/graphics/pdf/PdfRenderer.java
new file mode 100644
index 0000000..4666963
--- /dev/null
+++ b/graphics/java/android/graphics/pdf/PdfRenderer.java
@@ -0,0 +1,502 @@
+/*
+ * 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 android.graphics.pdf;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * <p>
+ * This class enables rendering a PDF document. This class is not thread safe.
+ * </p>
+ * <p>
+ * If you want to render a PDF, you create a renderer and for every page you want
+ * to render, you open the page, render it, and close the page. After you are done
+ * with rendering, you close the renderer. After the renderer is closed it should not
+ * be used anymore. Note that the pages are rendered one by one, i.e. you can have
+ * only a single page opened at any given time.
+ * </p>
+ * <p>
+ * A typical use of the APIs to render a PDF looks like this:
+ * </p>
+ * <pre>
+ * // create a new renderer
+ * PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());
+ *
+ * // let us just render all pages
+ * final int pageCount = renderer.getPageCount();
+ * for (int i = 0; i < pageCount; i++) {
+ * Page page = renderer.openPage(i);
+ *
+ * // say we render for showing on the screen
+ * page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);
+ *
+ * // do stuff with the bitmap
+ *
+ * // close the page
+ * page.close();
+ * }
+ *
+ * // close the renderer
+ * renderer.close();
+ * </pre>
+ *
+ * <h3>Print preview and print output</h3>
+ * <p>
+ * If you are using this class to rasterize a PDF for printing or show a print
+ * preview, it is recommended that you respect the following contract in order
+ * to provide a consistent user experience when seeing a preview and printing,
+ * i.e. the user sees a preview that is the same as the printout.
+ * </p>
+ * <ul>
+ * <li>
+ * Respect the property whether the document would like to be scaled for printing
+ * as per {@link #shouldScaleForPrinting()}.
+ * </li>
+ * <li>
+ * When scaling a document for printing the aspect ratio should be preserved.
+ * </li>
+ * <li>
+ * Do not inset the content with any margins from the {@link android.print.PrintAttributes}
+ * as the application is responsible to render it such that the margins are respected.
+ * </li>
+ * <li>
+ * If document page size is greater than the printed media size the content should
+ * be anchored to the upper left corner of the page for left-to-right locales and
+ * top right corner for right-to-left locales.
+ * </li>
+ * </ul>
+ *
+ * @see #close()
+ */
+public final class PdfRenderer implements AutoCloseable {
+ /**
+ * Any call the native pdfium code has to be single threaded as the library does not support
+ * parallel use.
+ */
+ final static Object sPdfiumLock = new Object();
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final Point mTempPoint = new Point();
+
+ private long mNativeDocument;
+
+ private final int mPageCount;
+
+ private ParcelFileDescriptor mInput;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Page mCurrentPage;
+
+ /** @hide */
+ @IntDef({
+ Page.RENDER_MODE_FOR_DISPLAY,
+ Page.RENDER_MODE_FOR_PRINT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RenderMode {}
+
+ /**
+ * Creates a new instance.
+ * <p>
+ * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
+ * i.e. its data being randomly accessed, e.g. pointing to a file.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
+ * and is responsible for closing it when the renderer is closed.
+ * </p>
+ * <p>
+ * If the file is from an untrusted source it is recommended to run the renderer in a separate,
+ * isolated process with minimal permissions to limit the impact of security exploits.
+ * </p>
+ *
+ * @param input Seekable file descriptor to read from.
+ *
+ * @throws java.io.IOException If an error occurs while reading the file.
+ * @throws java.lang.SecurityException If the file requires a password or
+ * the security scheme is not supported.
+ */
+ public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException {
+ if (input == null) {
+ throw new NullPointerException("input cannot be null");
+ }
+
+ final long size;
+ try {
+ Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
+ size = Os.fstat(input.getFileDescriptor()).st_size;
+ } catch (ErrnoException ee) {
+ throw new IllegalArgumentException("file descriptor not seekable");
+ }
+ mInput = input;
+
+ synchronized (sPdfiumLock) {
+ mNativeDocument = nativeCreate(mInput.getFd(), size);
+ try {
+ mPageCount = nativeGetPageCount(mNativeDocument);
+ } catch (Throwable t) {
+ nativeClose(mNativeDocument);
+ mNativeDocument = 0;
+ throw t;
+ }
+ }
+
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Closes this renderer. You should not use this instance
+ * after this method is called.
+ */
+ public void close() {
+ throwIfClosed();
+ throwIfPageOpened();
+ doClose();
+ }
+
+ /**
+ * Gets the number of pages in the document.
+ *
+ * @return The page count.
+ */
+ public int getPageCount() {
+ throwIfClosed();
+ return mPageCount;
+ }
+
+ /**
+ * Gets whether the document prefers to be scaled for printing.
+ * You should take this info account if the document is rendered
+ * for printing and the target media size differs from the page
+ * size.
+ *
+ * @return If to scale the document.
+ */
+ public boolean shouldScaleForPrinting() {
+ throwIfClosed();
+
+ synchronized (sPdfiumLock) {
+ return nativeScaleForPrinting(mNativeDocument);
+ }
+ }
+
+ /**
+ * Opens a page for rendering.
+ *
+ * @param index The page index.
+ * @return A page that can be rendered.
+ *
+ * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close()
+ */
+ public Page openPage(int index) {
+ throwIfClosed();
+ throwIfPageOpened();
+ throwIfPageNotInDocument(index);
+ mCurrentPage = new Page(index);
+ return mCurrentPage;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ doClose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void doClose() {
+ if (mCurrentPage != null) {
+ mCurrentPage.close();
+ mCurrentPage = null;
+ }
+
+ if (mNativeDocument != 0) {
+ synchronized (sPdfiumLock) {
+ nativeClose(mNativeDocument);
+ }
+ mNativeDocument = 0;
+ }
+
+ if (mInput != null) {
+ IoUtils.closeQuietly(mInput);
+ mInput = null;
+ }
+ mCloseGuard.close();
+ }
+
+ private void throwIfClosed() {
+ if (mInput == null) {
+ throw new IllegalStateException("Already closed");
+ }
+ }
+
+ private void throwIfPageOpened() {
+ if (mCurrentPage != null) {
+ throw new IllegalStateException("Current page not closed");
+ }
+ }
+
+ private void throwIfPageNotInDocument(int pageIndex) {
+ if (pageIndex < 0 || pageIndex >= mPageCount) {
+ throw new IllegalArgumentException("Invalid page index");
+ }
+ }
+
+ /**
+ * This class represents a PDF document page for rendering.
+ */
+ public final class Page implements AutoCloseable {
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Mode to render the content for display on a screen.
+ */
+ public static final int RENDER_MODE_FOR_DISPLAY = 1;
+
+ /**
+ * Mode to render the content for printing.
+ */
+ public static final int RENDER_MODE_FOR_PRINT = 2;
+
+ private final int mIndex;
+ private final int mWidth;
+ private final int mHeight;
+
+ private long mNativePage;
+
+ private Page(int index) {
+ Point size = mTempPoint;
+ synchronized (sPdfiumLock) {
+ mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size);
+ }
+ mIndex = index;
+ mWidth = size.x;
+ mHeight = size.y;
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Gets the page index.
+ *
+ * @return The index.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Gets the page width in points (1/72").
+ *
+ * @return The width in points.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Gets the page height in points (1/72").
+ *
+ * @return The height in points.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Renders a page to a bitmap.
+ * <p>
+ * You may optionally specify a rectangular clip in the bitmap bounds. No rendering
+ * outside the clip will be performed, hence it is your responsibility to initialize
+ * the bitmap outside the clip.
+ * </p>
+ * <p>
+ * You may optionally specify a matrix to transform the content from page coordinates
+ * which are in points (1/72") to bitmap coordinates which are in pixels. If this
+ * matrix is not provided this method will apply a transformation that will fit the
+ * whole page to the destination clip if provided or the destination bitmap if no
+ * clip is provided.
+ * </p>
+ * <p>
+ * The clip and transformation are useful for implementing tile rendering where the
+ * destination bitmap contains a portion of the image, for example when zooming.
+ * Another useful application is for printing where the size of the bitmap holding
+ * the page is too large and a client can render the page in stripes.
+ * </p>
+ * <p>
+ * <strong>Note: </strong> The destination bitmap format must be
+ * {@link Config#ARGB_8888 ARGB}.
+ * </p>
+ * <p>
+ * <strong>Note: </strong> The optional transformation matrix must be affine as per
+ * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify
+ * rotation, scaling, translation but not a perspective transformation.
+ * </p>
+ *
+ * @param destination Destination bitmap to which to render.
+ * @param destClip Optional clip in the bitmap bounds.
+ * @param transform Optional transformation to apply when rendering.
+ * @param renderMode The render mode.
+ *
+ * @see #RENDER_MODE_FOR_DISPLAY
+ * @see #RENDER_MODE_FOR_PRINT
+ */
+ public void render(@NonNull Bitmap destination, @Nullable Rect destClip,
+ @Nullable Matrix transform, @RenderMode int renderMode) {
+ if (mNativePage == 0) {
+ throw new NullPointerException();
+ }
+
+ destination = Preconditions.checkNotNull(destination, "bitmap null");
+
+ if (destination.getConfig() != Config.ARGB_8888) {
+ throw new IllegalArgumentException("Unsupported pixel format");
+ }
+
+ if (destClip != null) {
+ if (destClip.left < 0 || destClip.top < 0
+ || destClip.right > destination.getWidth()
+ || destClip.bottom > destination.getHeight()) {
+ throw new IllegalArgumentException("destBounds not in destination");
+ }
+ }
+
+ if (transform != null && !transform.isAffine()) {
+ throw new IllegalArgumentException("transform not affine");
+ }
+
+ if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) {
+ throw new IllegalArgumentException("Unsupported render mode");
+ }
+
+ if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) {
+ throw new IllegalArgumentException("Only single render mode supported");
+ }
+
+ final int contentLeft = (destClip != null) ? destClip.left : 0;
+ final int contentTop = (destClip != null) ? destClip.top : 0;
+ final int contentRight = (destClip != null) ? destClip.right
+ : destination.getWidth();
+ final int contentBottom = (destClip != null) ? destClip.bottom
+ : destination.getHeight();
+
+ // If transform is not set, stretch page to whole clipped area
+ if (transform == null) {
+ int clipWidth = contentRight - contentLeft;
+ int clipHeight = contentBottom - contentTop;
+
+ transform = new Matrix();
+ transform.postScale((float)clipWidth / getWidth(),
+ (float)clipHeight / getHeight());
+ transform.postTranslate(contentLeft, contentTop);
+ }
+
+ // FIXME: This code is planned to be outside the UI rendering module, so it should not
+ // be able to access native instances from Bitmap, Matrix, etc.
+ final long transformPtr = transform.ni();
+
+ synchronized (sPdfiumLock) {
+ nativeRenderPage(mNativeDocument, mNativePage, destination.getNativeInstance(),
+ contentLeft, contentTop, contentRight, contentBottom, transformPtr,
+ renderMode);
+ }
+ }
+
+ /**
+ * Closes this page.
+ *
+ * @see android.graphics.pdf.PdfRenderer#openPage(int)
+ */
+ @Override
+ public void close() {
+ throwIfClosed();
+ doClose();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ doClose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void doClose() {
+ if (mNativePage != 0) {
+ synchronized (sPdfiumLock) {
+ nativeClosePage(mNativePage);
+ }
+ mNativePage = 0;
+ }
+
+ mCloseGuard.close();
+ mCurrentPage = null;
+ }
+
+ private void throwIfClosed() {
+ if (mNativePage == 0) {
+ throw new IllegalStateException("Already closed");
+ }
+ }
+ }
+
+ private static native long nativeCreate(int fd, long size);
+ private static native void nativeClose(long documentPtr);
+ private static native int nativeGetPageCount(long documentPtr);
+ private static native boolean nativeScaleForPrinting(long documentPtr);
+ private static native void nativeRenderPage(long documentPtr, long pagePtr, long bitmapHandle,
+ int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr,
+ int renderMode);
+ private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex,
+ Point outSize);
+ private static native void nativeClosePage(long pagePtr);
+}
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index d66c925..0ecf1f8 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -82,16 +82,18 @@
genrule {
name: "wm_shell_protolog_src",
srcs: [
+ ":protolog-impl",
":wm_shell_protolog-groups",
":wm_shell-sources",
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
- "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " +
- "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " +
"--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
"--loggroups-jar $(location :wm_shell_protolog-groups) " +
+ "--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " +
+ "--legacy-viewer-config-file-path /system_ext/etc/wmshell.protolog.json.gz " +
+ "--legacy-output-file-path /data/misc/wmtrace/shell_log.winscope " +
"--output-srcjar $(out) " +
"$(locations :wm_shell-sources)",
out: ["wm_shell_protolog.srcjar"],
@@ -108,12 +110,30 @@
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
"--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
"--loggroups-jar $(location :wm_shell_protolog-groups) " +
- "--viewer-conf $(out) " +
+ "--viewer-config-type json " +
+ "--viewer-config $(out) " +
"$(locations :wm_shell-sources)",
out: ["wm_shell_protolog.json"],
}
genrule {
+ name: "gen-wmshell.protolog.pb",
+ srcs: [
+ ":wm_shell_protolog-groups",
+ ":wm_shell-sources",
+ ],
+ tools: ["protologtool"],
+ cmd: "$(location protologtool) generate-viewer-config " +
+ "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+ "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
+ "--loggroups-jar $(location :wm_shell_protolog-groups) " +
+ "--viewer-config-type proto " +
+ "--viewer-config $(out) " +
+ "$(locations :wm_shell-sources)",
+ out: ["wmshell.protolog.pb"],
+}
+
+genrule {
name: "protolog.json.gz",
srcs: [":generate-wm_shell_protolog.json"],
out: ["wmshell.protolog.json.gz"],
@@ -127,6 +147,13 @@
filename_from_src: true,
}
+prebuilt_etc {
+ name: "wmshell.protolog.pb",
+ system_ext_specific: true,
+ src: ":gen-wmshell.protolog.pb",
+ filename_from_src: true,
+}
+
// End ProtoLog
java_library {
diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml
new file mode 100644
index 0000000..52a5967
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_hovered="true"
+ android:color="@color/desktop_mode_caption_button_on_hover_dark"/>
+ <item android:color="@color/desktop_mode_caption_button"/>
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml
new file mode 100644
index 0000000..6d8a51c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_hovered="true"
+ android:color="@color/desktop_mode_caption_button_on_hover_light"/>
+ <item android:color="@color/desktop_mode_caption_button"/>
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
new file mode 100644
index 0000000..9482645
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/progress">
+ <rotate
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fromDegrees="275"
+ android:toDegrees="275">
+ <shape
+ android:shape="ring"
+ android:thickness="3dp"
+ android:innerRadius="17dp"
+ android:useLevel="true">
+ </shape>
+ </rotate>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/rounded_button.xml b/libs/WindowManager/Shell/res/drawable/rounded_button.xml
new file mode 100644
index 0000000..17a0bab
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/rounded_button.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="20dp" />
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index e4f793c..d1b1af3 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -74,17 +74,11 @@
android:layout_height="40dp"
android:layout_weight="1"/>
- <ImageButton
- android:id="@+id/maximize_window"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:padding="9dp"
- android:layout_marginEnd="8dp"
- android:contentDescription="@string/maximize_button_text"
- android:src="@drawable/decor_desktop_mode_maximize_button_dark"
- android:scaleType="fitCenter"
- android:gravity="end"
- android:background="@null"/>
+ <com.android.wm.shell.windowdecor.MaximizeButtonView
+ android:id="@+id/maximize_button_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"/>
<ImageButton
android:id="@+id/close_window"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 0db72f7..dbfd6e5 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -15,6 +15,7 @@
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/maximize_menu"
style="?android:attr/buttonBarStyle"
android:layout_width="@dimen/desktop_mode_maximize_menu_width"
android:layout_height="@dimen/desktop_mode_maximize_menu_height"
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
new file mode 100644
index 0000000..bb6efce
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:progressDrawable="@drawable/circular_progress"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:indeterminate="false"
+ android:visibility="invisible"/>
+
+ <ImageButton
+ android:id="@+id/maximize_window"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:padding="9dp"
+ android:contentDescription="@string/maximize_button_text"
+ android:src="@drawable/decor_desktop_mode_maximize_button_dark"
+ android:scaleType="fitCenter"
+ android:background="@drawable/rounded_button"/>
+</merge>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index fae71ef..758dbfd 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -66,4 +66,9 @@
<color name="desktop_mode_maximize_menu_button_outline">#797869</color>
<color name="desktop_mode_maximize_menu_button_outline_on_hover">#606219</color>
<color name="desktop_mode_maximize_menu_button_on_hover">#E7E790</color>
+ <color name="desktop_mode_maximize_menu_progress_light">#33000000</color>
+ <color name="desktop_mode_maximize_menu_progress_dark">#33FFFFFF</color>
+ <color name="desktop_mode_caption_button_on_hover_light">#11000000</color>
+ <color name="desktop_mode_caption_button_on_hover_dark">#11FFFFFF</color>
+ <color name="desktop_mode_caption_button">#00000000</color>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
index 88525aa..93893e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
@@ -16,7 +16,10 @@
package com.android.wm.shell;
-import com.android.wm.shell.protolog.ShellProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellInit;
@@ -24,19 +27,19 @@
import java.util.Arrays;
/**
- * Controls the {@link ShellProtoLogImpl} in WMShell via adb shell commands.
+ * Controls the {@link ProtoLog} in WMShell via adb shell commands.
*
* Use with {@code adb shell dumpsys activity service SystemUIService WMShell protolog ...}.
*/
public class ProtoLogController implements ShellCommandHandler.ShellCommandActionHandler {
private final ShellCommandHandler mShellCommandHandler;
- private final ShellProtoLogImpl mShellProtoLog;
+ private final IProtoLog mShellProtoLog;
public ProtoLogController(ShellInit shellInit,
ShellCommandHandler shellCommandHandler) {
shellInit.addInitCallback(this::onInit, this);
mShellCommandHandler = shellCommandHandler;
- mShellProtoLog = ShellProtoLogImpl.getSingleInstance();
+ mShellProtoLog = ProtoLog.getSingleInstance();
}
void onInit() {
@@ -45,22 +48,35 @@
@Override
public boolean onShellCommand(String[] args, PrintWriter pw) {
+ final ILogger logger = pw::println;
switch (args[0]) {
case "status": {
- pw.println(mShellProtoLog.getStatus());
+ if (android.tracing.Flags.perfettoProtolog()) {
+ pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+ return false;
+ }
+ ((LegacyProtoLogImpl) mShellProtoLog).getStatus();
return true;
}
case "start": {
- mShellProtoLog.startProtoLog(pw);
+ if (android.tracing.Flags.perfettoProtolog()) {
+ pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+ return false;
+ }
+ ((LegacyProtoLogImpl) mShellProtoLog).startProtoLog(pw);
return true;
}
case "stop": {
- mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
+ if (android.tracing.Flags.perfettoProtolog()) {
+ pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+ return false;
+ }
+ ((LegacyProtoLogImpl) mShellProtoLog).stopProtoLog(pw, true);
return true;
}
case "enable-text": {
String[] groups = Arrays.copyOfRange(args, 1, args.length);
- int result = mShellProtoLog.startTextLogging(groups, pw);
+ int result = mShellProtoLog.startLoggingToLogcat(groups, logger);
if (result == 0) {
pw.println("Starting logging on groups: " + Arrays.toString(groups));
return true;
@@ -69,7 +85,7 @@
}
case "disable-text": {
String[] groups = Arrays.copyOfRange(args, 1, args.length);
- int result = mShellProtoLog.stopTextLogging(groups, pw);
+ int result = mShellProtoLog.stopLoggingToLogcat(groups, logger);
if (result == 0) {
pw.println("Stopping logging on groups: " + Arrays.toString(groups));
return true;
@@ -78,19 +94,23 @@
}
case "enable": {
String[] groups = Arrays.copyOfRange(args, 1, args.length);
- return mShellProtoLog.startTextLogging(groups, pw) == 0;
+ return mShellProtoLog.startLoggingToLogcat(groups, logger) == 0;
}
case "disable": {
String[] groups = Arrays.copyOfRange(args, 1, args.length);
- return mShellProtoLog.stopTextLogging(groups, pw) == 0;
+ return mShellProtoLog.stopLoggingToLogcat(groups, logger) == 0;
}
case "save-for-bugreport": {
+ if (android.tracing.Flags.perfettoProtolog()) {
+ pw.println("(Deprecated) legacy command");
+ return false;
+ }
if (!mShellProtoLog.isProtoEnabled()) {
pw.println("Logging to proto is not enabled for WMShell.");
return false;
}
- mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
- mShellProtoLog.startProtoLog(pw);
+ ((LegacyProtoLogImpl) mShellProtoLog).stopProtoLog(pw, true /* writeToFile */);
+ ((LegacyProtoLogImpl) mShellProtoLog).startProtoLog(pw);
return true;
}
default: {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index e0f0556..15350fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -122,6 +122,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -247,6 +248,9 @@
/** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */
private float mFontScale = 0;
+ /** Saved locale, used to detect local changes in {@link #onConfigurationChanged}. */
+ private Locale mLocale = null;
+
/** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
@@ -683,6 +687,17 @@
mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
}
+ /** Called when sensitive notification state has changed */
+ public void onSensitiveNotificationProtectionStateChanged(
+ boolean sensitiveNotificationProtectionActive) {
+ if (mStackView != null) {
+ mStackView.onSensitiveNotificationProtectionStateChanged(
+ sensitiveNotificationProtectionActive);
+ ProtoLog.d(WM_SHELL_BUBBLES, "onSensitiveNotificationProtectionStateChanged=%b",
+ sensitiveNotificationProtectionActive);
+ }
+ }
+
/** Whether bubbles are showing in the bubble bar. */
public boolean isShowingAsBubbleBar() {
return canShowAsBubbleBar() && mBubbleStateListener != null;
@@ -1057,6 +1072,11 @@
mLayoutDirection = newConfig.getLayoutDirection();
mStackView.onLayoutDirectionChanged(mLayoutDirection);
}
+ Locale newLocale = newConfig.locale;
+ if (newLocale != null && !newLocale.equals(mLocale)) {
+ mLocale = newLocale;
+ mStackView.updateLocale();
+ }
}
}
@@ -2583,6 +2603,14 @@
mMainExecutor.execute(
() -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
}
+
+ @Override
+ public void onSensitiveNotificationProtectionStateChanged(
+ boolean sensitiveNotificationProtectionActive) {
+ mMainExecutor.execute(
+ () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged(
+ sensitiveNotificationProtectionActive));
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 123693d..74f087b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -517,6 +517,15 @@
}
}
+ void updateLocale() {
+ if (mManageButton != null) {
+ mManageButton.setText(mContext.getString(R.string.manage_bubbles_text));
+ }
+ if (mOverflowView != null) {
+ mOverflowView.updateLocale();
+ }
+ }
+
void applyThemeAttrs() {
final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
android.R.attr.dialogCornerRadius,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
index b06de4f..633b01b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
@@ -242,6 +242,11 @@
mEmptyStateSubtitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
}
+ public void updateLocale() {
+ mEmptyStateTitle.setText(mContext.getString(R.string.bubble_overflow_empty_title));
+ mEmptyStateSubtitle.setText(mContext.getString(R.string.bubble_overflow_empty_subtitle));
+ }
+
private final BubbleData.Listener mDataListener = new BubbleData.Listener() {
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index b23fd52..8fd6ffe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -291,6 +291,11 @@
*/
private boolean mRemovingLastBubbleWhileExpanded = false;
+ /**
+ * Whether sensitive notification protection should disable flyout
+ */
+ private boolean mSensitiveNotificationProtectionActive = false;
+
/** Animator for animating the expanded view's alpha (including the TaskView inside it). */
private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
@@ -1447,6 +1452,12 @@
}
}
+ void updateLocale() {
+ if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) {
+ mBubbleOverflow.getExpandedView().updateLocale();
+ }
+ }
+
private void updateOverflow() {
mBubbleOverflow.update();
mBubbleContainer.reorderView(mBubbleOverflow.getIconView(),
@@ -2199,6 +2210,11 @@
}
}
+ void onSensitiveNotificationProtectionStateChanged(
+ boolean sensitiveNotificationProtectionActive) {
+ mSensitiveNotificationProtectionActive = sensitiveNotificationProtectionActive;
+ }
+
/**
* Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or
* not.
@@ -2842,6 +2858,7 @@
|| isExpanded()
|| mIsExpansionAnimating
|| mIsGestureInProgress
+ || mSensitiveNotificationProtectionActive
|| mBubbleToExpandAfterFlyoutCollapse != null
|| bubbleView == null) {
if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 28af0ca..26077cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -286,6 +286,16 @@
void onUserRemoved(int removedUserId);
/**
+ * Called when the Sensitive notification protection state has changed, such as when media
+ * projection starts and stops.
+ *
+ * @param sensitiveNotificationProtectionActive {@code true} if notifications should be
+ * protected
+ */
+ void onSensitiveNotificationProtectionStateChanged(
+ boolean sensitiveNotificationProtectionActive);
+
+ /**
* A listener to be notified of bubble state changes, used by launcher to render bubbles in
* its process.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS
index 8271014..08c7031 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS
@@ -1,2 +1,6 @@
# WM shell sub-module bubble owner
madym@google.com
+atsjenk@google.com
+liranb@google.com
+sukeshram@google.com
+mpodolian@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
deleted file mode 100644
index 93ffb3d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2020 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.wm.shell.protolog;
-
-import android.annotation.Nullable;
-
-import com.android.internal.protolog.BaseProtoLogImpl;
-import com.android.internal.protolog.ProtoLogViewerConfigReader;
-import com.android.internal.protolog.common.IProtoLogGroup;
-
-import java.io.File;
-import java.io.PrintWriter;
-
-
-/**
- * A service for the ProtoLog logging system.
- */
-public class ShellProtoLogImpl extends BaseProtoLogImpl {
- private static final String TAG = "ProtoLogImpl";
- private static final int BUFFER_CAPACITY = 1024 * 1024;
- // TODO: find a proper location to save the protolog message file
- private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope";
- private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz";
-
- private static ShellProtoLogImpl sServiceInstance = null;
-
- static {
- addLogGroupEnum(ShellProtoLogGroup.values());
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance()
- .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args);
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
- args);
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance()
- .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args);
- }
-
- /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
- public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
- @Nullable String messageString,
- Object... args) {
- getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
- }
-
- /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
- public static boolean isEnabled(IProtoLogGroup group) {
- return group.isLogToLogcat()
- || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
- }
-
- /**
- * Returns the single instance of the ProtoLogImpl singleton class.
- */
- public static synchronized ShellProtoLogImpl getSingleInstance() {
- if (sServiceInstance == null) {
- sServiceInstance = new ShellProtoLogImpl();
- }
- return sServiceInstance;
- }
-
- public int startTextLogging(String[] groups, PrintWriter pw) {
- mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME);
- return setLogging(true /* setTextLogging */, true, pw, groups);
- }
-
- public int stopTextLogging(String[] groups, PrintWriter pw) {
- return setLogging(true /* setTextLogging */, false, pw, groups);
- }
-
- private ShellProtoLogImpl() {
- super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY,
- new ProtoLogViewerConfigReader());
- }
-}
-
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
index 9b48a54..7a50814 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
@@ -18,7 +18,7 @@
import android.util.Log
import com.android.internal.protolog.common.IProtoLogGroup
-import com.android.wm.shell.protolog.ShellProtoLogImpl
+import com.android.internal.protolog.common.ProtoLog
/**
* Log messages using an API similar to [com.android.internal.protolog.common.ProtoLog]. Useful for
@@ -31,42 +31,42 @@
companion object {
/** @see [com.android.internal.protolog.common.ProtoLog.d] */
fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.d(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.v] */
fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.v(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.i] */
fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.i(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.w] */
fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.w(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.e] */
fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.e(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.wtf] */
fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ShellProtoLogImpl.isEnabled(group)) {
+ if (ProtoLog.isEnabled(group)) {
Log.wtf(group.tag, String.format(messageString, *args))
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 57e964f..5b8ffb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -22,6 +22,8 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_HOVER_EXIT;
import static android.view.WindowInsets.Type.statusBars;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -311,8 +313,8 @@
private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
- DragDetector.MotionEventHandler {
-
+ View.OnGenericMotionListener , DragDetector.MotionEventHandler {
+ private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150;
private final int mTaskId;
private final WindowContainerToken mTaskToken;
private final DragPositioningCallback mDragPositioningCallback;
@@ -323,6 +325,7 @@
private boolean mHasLongClicked;
private boolean mShouldClick;
private int mDragPointerId = -1;
+ private final Runnable mCloseMaximizeWindowRunnable;
private DesktopModeTouchEventListener(
RunningTaskInfo taskInfo,
@@ -332,6 +335,11 @@
mDragPositioningCallback = dragPositioningCallback;
mDragDetector = new DragDetector(this);
mGestureDetector = new GestureDetector(mContext, this);
+ mCloseMaximizeWindowRunnable = () -> {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ if (decoration == null) return;
+ decoration.closeMaximizeMenu();
+ };
}
@Override
@@ -387,13 +395,10 @@
mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId));
}
} else if (id == R.id.maximize_window) {
- if (decoration.isMaximizeMenuActive()) {
- decoration.closeMaximizeMenu();
- return;
- }
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
decoration.closeHandleMenu();
+ decoration.closeMaximizeMenu();
+ mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
} else if (id == R.id.maximize_menu_maximize_button) {
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
@@ -460,6 +465,36 @@
return false;
}
+ @Override
+ public boolean onGenericMotion(View v, MotionEvent ev) {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ final int id = v.getId();
+ if (ev.getAction() == ACTION_HOVER_ENTER) {
+ if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
+ decoration.onMaximizeWindowHoverEnter();
+ } else if (id == R.id.maximize_window
+ || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
+ // Re-hovering over any of the maximize menu views should keep the menu open by
+ // cancelling any attempts to close the menu.
+ mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
+ }
+ return true;
+ } else if (ev.getAction() == ACTION_HOVER_EXIT) {
+ if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
+ decoration.onMaximizeWindowHoverExit();
+ } else if (id == R.id.maximize_window
+ || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
+ // Close menu if not hovering over maximize menu or maximize button after a
+ // delay to give user a chance to re-enter view or to move from one maximize
+ // menu view to another.
+ mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
+ CLOSE_MAXIMIZE_MENU_DELAY_MS);
+ }
+ return true;
+ }
+ return false;
+ }
+
private void moveTaskToFront(RunningTaskInfo taskInfo) {
if (!taskInfo.isFocused) {
mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo));
@@ -990,7 +1025,7 @@
new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
windowDecoration.setCaptionListeners(
- touchEventListener, touchEventListener, touchEventListener);
+ touchEventListener, touchEventListener, touchEventListener, touchEventListener);
windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
windowDecoration.setDragPositioningCallback(dragPositioningCallback);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
@@ -1036,6 +1071,7 @@
return;
}
decoration.showResizeVeil(t, bounds);
+ decoration.setAnimatingTaskResize(true);
}
@Override
@@ -1050,6 +1086,7 @@
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
if (decoration == null) return;
decoration.hideResizeVeil();
+ decoration.setAnimatingTaskResize(false);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 185365b..74f460b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -60,6 +60,8 @@
import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
+import kotlin.Unit;
+
import java.util.function.Supplier;
/**
@@ -79,6 +81,7 @@
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
private View.OnLongClickListener mOnCaptionLongClickListener;
+ private View.OnGenericMotionListener mOnCaptionGenericMotionListener;
private DragPositioningCallback mDragPositioningCallback;
private DragResizeInputListener mDragResizeListener;
private DragDetector mDragDetector;
@@ -152,10 +155,12 @@
void setCaptionListeners(
View.OnClickListener onCaptionButtonClickListener,
View.OnTouchListener onCaptionTouchListener,
- View.OnLongClickListener onLongClickListener) {
+ View.OnLongClickListener onLongClickListener,
+ View.OnGenericMotionListener onGenericMotionListener) {
mOnCaptionButtonClickListener = onCaptionButtonClickListener;
mOnCaptionTouchListener = onCaptionTouchListener;
mOnCaptionLongClickListener = onLongClickListener;
+ mOnCaptionGenericMotionListener = onGenericMotionListener;
}
void setExclusionRegionListener(ExclusionRegionListener exclusionRegionListener) {
@@ -225,9 +230,15 @@
mOnCaptionTouchListener,
mOnCaptionButtonClickListener,
mOnCaptionLongClickListener,
+ mOnCaptionGenericMotionListener,
mAppName,
- mAppIconBitmap
- );
+ mAppIconBitmap,
+ () -> {
+ if (!isMaximizeMenuActive()) {
+ createMaximizeMenu();
+ }
+ return Unit.INSTANCE;
+ });
} else {
throw new IllegalArgumentException("Unexpected layout resource id");
}
@@ -548,7 +559,8 @@
*/
void createMaximizeMenu() {
mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer,
- mDisplayController, mTaskInfo, mOnCaptionButtonClickListener, mContext,
+ mDisplayController, mTaskInfo, mOnCaptionButtonClickListener,
+ mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext,
calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier);
mMaximizeMenu.show();
}
@@ -776,6 +788,22 @@
return R.id.desktop_mode_caption;
}
+ void setAnimatingTaskResize(boolean animatingTaskResize) {
+ if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) return;
+ ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ .setAnimatingTaskResize(animatingTaskResize);
+ }
+
+ void onMaximizeWindowHoverExit() {
+ ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ .onMaximizeWindowHoverExit();
+ }
+
+ void onMaximizeWindowHoverEnter() {
+ ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ .onMaximizeWindowHoverEnter();
+ }
+
@Override
public String toString() {
return "{"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
new file mode 100644
index 0000000..b2f8cfd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.windowdecor
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ProgressBar
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import androidx.core.content.ContextCompat
+import com.android.wm.shell.R
+
+private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350
+private const val MAX_DRAWABLE_ALPHA = 255
+
+class MaximizeButtonView(
+ context: Context,
+ attrs: AttributeSet
+) : FrameLayout(context, attrs) {
+ lateinit var onHoverAnimationFinishedListener: () -> Unit
+ private val hoverProgressAnimatorSet = AnimatorSet()
+ var hoverDisabled = false
+
+ private val progressBar: ProgressBar
+ private val maximizeWindow: ImageButton
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.maximize_menu_button, this, true)
+
+ progressBar = requireViewById(R.id.progress_bar)
+ maximizeWindow = requireViewById(R.id.maximize_window)
+ }
+
+ fun startHoverAnimation() {
+ if (hoverDisabled) return
+ if (hoverProgressAnimatorSet.isRunning) {
+ cancelHoverAnimation()
+ }
+
+ maximizeWindow.background.alpha = 0
+
+ hoverProgressAnimatorSet.playSequentially(
+ ValueAnimator.ofInt(0, MAX_DRAWABLE_ALPHA)
+ .setDuration(50)
+ .apply {
+ addUpdateListener {
+ maximizeWindow.background.alpha = animatedValue as Int
+ }
+ },
+ ObjectAnimator.ofInt(progressBar, "progress", 100)
+ .setDuration(OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS.toLong())
+ .apply {
+ doOnStart {
+ progressBar.setProgress(0, false)
+ progressBar.visibility = View.VISIBLE
+ }
+ doOnEnd {
+ progressBar.visibility = View.INVISIBLE
+ onHoverAnimationFinishedListener()
+ }
+ }
+ )
+ hoverProgressAnimatorSet.start()
+ }
+
+ fun cancelHoverAnimation() {
+ hoverProgressAnimatorSet.removeAllListeners()
+ hoverProgressAnimatorSet.cancel()
+ progressBar.visibility = View.INVISIBLE
+ }
+
+ fun setAnimationTints(darkMode: Boolean) {
+ if (darkMode) {
+ progressBar.progressTintList = ColorStateList.valueOf(
+ resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark))
+ maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+ R.color.desktop_mode_caption_button_color_selector_dark))
+ } else {
+ progressBar.progressTintList = ColorStateList.valueOf(
+ resources.getColor(R.color.desktop_mode_maximize_menu_progress_light))
+ maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+ R.color.desktop_mode_caption_button_color_selector_light))
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 794b357..b82f7ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor
+import android.annotation.IdRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.content.res.Resources
@@ -27,6 +28,8 @@
import android.view.SurfaceControl.Transaction
import android.view.SurfaceControlViewHost
import android.view.View.OnClickListener
+import android.view.View.OnGenericMotionListener
+import android.view.View.OnTouchListener
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.widget.Button
@@ -49,6 +52,8 @@
private val displayController: DisplayController,
private val taskInfo: RunningTaskInfo,
private val onClickListener: OnClickListener,
+ private val onGenericMotionListener: OnGenericMotionListener,
+ private val onTouchListener: OnTouchListener,
private val decorWindowContext: Context,
private val menuPosition: PointF,
private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
@@ -142,15 +147,26 @@
private fun setupMaximizeMenu() {
val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return
- maximizeMenuView.requireViewById<Button>(
+ maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
+ maximizeMenuView.setOnTouchListener(onTouchListener)
+
+ val maximizeButton = maximizeMenuView.requireViewById<Button>(
R.id.maximize_menu_maximize_button
- ).setOnClickListener(onClickListener)
- maximizeMenuView.requireViewById<Button>(
+ )
+ maximizeButton.setOnClickListener(onClickListener)
+ maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
+
+ val snapRightButton = maximizeMenuView.requireViewById<Button>(
R.id.maximize_menu_snap_right_button
- ).setOnClickListener(onClickListener)
- maximizeMenuView.requireViewById<Button>(
+ )
+ snapRightButton.setOnClickListener(onClickListener)
+ snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
+
+ val snapLeftButton = maximizeMenuView.requireViewById<Button>(
R.id.maximize_menu_snap_left_button
- ).setOnClickListener(onClickListener)
+ )
+ snapLeftButton.setOnClickListener(onClickListener)
+ snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
}
/**
@@ -173,4 +189,12 @@
private fun viewsLaidOut(): Boolean {
return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false
}
+
+ companion object {
+ fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
+ return viewId == R.id.maximize_menu || viewId == R.id.maximize_menu_maximize_button ||
+ viewId == R.id.maximize_menu_snap_left_button ||
+ viewId == R.id.maximize_menu_snap_right_button
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
index 2309c54..7e5b9bd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -21,6 +21,7 @@
import com.android.internal.R.attr.materialColorSurfaceContainerLow
import com.android.internal.R.attr.materialColorSurfaceDim
import com.android.wm.shell.R
+import com.android.wm.shell.windowdecor.MaximizeButtonView
/**
* A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
@@ -32,8 +33,10 @@
onCaptionTouchListener: View.OnTouchListener,
onCaptionButtonClickListener: View.OnClickListener,
onLongClickListener: OnLongClickListener,
+ onCaptionGenericMotionListener: View.OnGenericMotionListener,
appName: CharSequence,
- appIconBitmap: Bitmap
+ appIconBitmap: Bitmap,
+ onMaximizeHoverAnimationFinishedListener: () -> Unit
) : DesktopModeWindowDecorationViewHolder(rootView) {
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
@@ -41,6 +44,8 @@
private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window)
private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button)
+ private val maximizeButtonView: MaximizeButtonView =
+ rootView.requireViewById(R.id.maximize_button_view)
private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
@@ -55,10 +60,13 @@
closeWindowButton.setOnClickListener(onCaptionButtonClickListener)
maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener)
maximizeWindowButton.setOnTouchListener(onCaptionTouchListener)
+ maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener)
maximizeWindowButton.onLongClickListener = onLongClickListener
closeWindowButton.setOnTouchListener(onCaptionTouchListener)
appNameTextView.text = appName
appIconImageView.setImageBitmap(appIconBitmap)
+ maximizeButtonView.onHoverAnimationFinishedListener =
+ onMaximizeHoverAnimationFinishedListener
}
override fun bindData(taskInfo: RunningTaskInfo) {
@@ -73,12 +81,30 @@
maximizeWindowButton.imageAlpha = alpha
closeWindowButton.imageAlpha = alpha
expandMenuButton.imageAlpha = alpha
+
+ maximizeButtonView.setAnimationTints(isDarkMode())
}
override fun onHandleMenuOpened() {}
override fun onHandleMenuClosed() {}
+ fun setAnimatingTaskResize(animatingTaskResize: Boolean) {
+ // If animating a task resize, cancel any running hover animations
+ if (animatingTaskResize) {
+ maximizeButtonView.cancelHoverAnimation()
+ }
+ maximizeButtonView.hoverDisabled = animatingTaskResize
+ }
+
+ fun onMaximizeWindowHoverExit() {
+ maximizeButtonView.cancelHoverAnimation()
+ }
+
+ fun onMaximizeWindowHoverEnter() {
+ maximizeButtonView.startHoverAnimation()
+ }
+
@ColorInt
private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
if (isTransparentBackgroundRequested(taskInfo)) {
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index abd84de..40239b8 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -428,6 +428,7 @@
"jni/MovieImpl.cpp",
"jni/pdf/PdfDocument.cpp",
"jni/pdf/PdfEditor.cpp",
+ "jni/pdf/PdfRenderer.cpp",
"jni/pdf/PdfUtils.cpp",
],
shared_libs: [
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index fb0cdb0..883f273 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -70,6 +70,7 @@
extern int register_android_graphics_fonts_FontFamily(JNIEnv* env);
extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env);
extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env);
+extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env);
extern int register_android_graphics_text_MeasuredText(JNIEnv* env);
extern int register_android_graphics_text_LineBreaker(JNIEnv *env);
extern int register_android_graphics_text_TextShaper(JNIEnv *env);
@@ -141,6 +142,7 @@
REG_JNI(register_android_graphics_fonts_FontFamily),
REG_JNI(register_android_graphics_pdf_PdfDocument),
REG_JNI(register_android_graphics_pdf_PdfEditor),
+ REG_JNI(register_android_graphics_pdf_PdfRenderer),
REG_JNI(register_android_graphics_text_MeasuredText),
REG_JNI(register_android_graphics_text_LineBreaker),
REG_JNI(register_android_graphics_text_TextShaper),
diff --git a/libs/hwui/jni/pdf/PdfRenderer.cpp b/libs/hwui/jni/pdf/PdfRenderer.cpp
new file mode 100644
index 0000000..cc1f961
--- /dev/null
+++ b/libs/hwui/jni/pdf/PdfRenderer.cpp
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+#include "PdfUtils.h"
+
+#include "GraphicsJNI.h"
+#include "SkBitmap.h"
+#include "SkMatrix.h"
+#include "fpdfview.h"
+
+#include <vector>
+#include <utils/Log.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+namespace android {
+
+static const int RENDER_MODE_FOR_DISPLAY = 1;
+static const int RENDER_MODE_FOR_PRINT = 2;
+
+static struct {
+ jfieldID x;
+ jfieldID y;
+} gPointClassInfo;
+
+static jlong nativeOpenPageAndGetSize(JNIEnv* env, jclass thiz, jlong documentPtr,
+ jint pageIndex, jobject outSize) {
+ FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
+
+ FPDF_PAGE page = FPDF_LoadPage(document, pageIndex);
+ if (!page) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot load page");
+ return -1;
+ }
+
+ double width = 0;
+ double height = 0;
+
+ int result = FPDF_GetPageSizeByIndex(document, pageIndex, &width, &height);
+ if (!result) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot get page size");
+ return -1;
+ }
+
+ env->SetIntField(outSize, gPointClassInfo.x, width);
+ env->SetIntField(outSize, gPointClassInfo.y, height);
+
+ return reinterpret_cast<jlong>(page);
+}
+
+static void nativeClosePage(JNIEnv* env, jclass thiz, jlong pagePtr) {
+ FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr);
+ FPDF_ClosePage(page);
+}
+
+static void nativeRenderPage(JNIEnv* env, jclass thiz, jlong documentPtr, jlong pagePtr,
+ jlong bitmapPtr, jint clipLeft, jint clipTop, jint clipRight, jint clipBottom,
+ jlong transformPtr, jint renderMode) {
+ FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr);
+
+ SkBitmap skBitmap;
+ bitmap::toBitmap(bitmapPtr).getSkBitmap(&skBitmap);
+
+ const int stride = skBitmap.width() * 4;
+
+ FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(skBitmap.width(), skBitmap.height(),
+ FPDFBitmap_BGRA, skBitmap.getPixels(), stride);
+
+ int renderFlags = FPDF_REVERSE_BYTE_ORDER;
+ if (renderMode == RENDER_MODE_FOR_DISPLAY) {
+ renderFlags |= FPDF_LCD_TEXT;
+ } else if (renderMode == RENDER_MODE_FOR_PRINT) {
+ renderFlags |= FPDF_PRINTING;
+ }
+
+ SkMatrix matrix = *reinterpret_cast<SkMatrix*>(transformPtr);
+ SkScalar transformValues[6];
+ if (!matrix.asAffine(transformValues)) {
+ jniThrowException(env, "java/lang/IllegalArgumentException",
+ "transform matrix has perspective. Only affine matrices are allowed.");
+ return;
+ }
+
+ FS_MATRIX transform = {transformValues[SkMatrix::kAScaleX], transformValues[SkMatrix::kASkewY],
+ transformValues[SkMatrix::kASkewX], transformValues[SkMatrix::kAScaleY],
+ transformValues[SkMatrix::kATransX],
+ transformValues[SkMatrix::kATransY]};
+
+ FS_RECTF clip = {(float) clipLeft, (float) clipTop, (float) clipRight, (float) clipBottom};
+
+ FPDF_RenderPageBitmapWithMatrix(bitmap, page, &transform, &clip, renderFlags);
+
+ skBitmap.notifyPixelsChanged();
+}
+
+static const JNINativeMethod gPdfRenderer_Methods[] = {
+ {"nativeCreate", "(IJ)J", (void*) nativeOpen},
+ {"nativeClose", "(J)V", (void*) nativeClose},
+ {"nativeGetPageCount", "(J)I", (void*) nativeGetPageCount},
+ {"nativeScaleForPrinting", "(J)Z", (void*) nativeScaleForPrinting},
+ {"nativeRenderPage", "(JJJIIIIJI)V", (void*) nativeRenderPage},
+ {"nativeOpenPageAndGetSize", "(JILandroid/graphics/Point;)J", (void*) nativeOpenPageAndGetSize},
+ {"nativeClosePage", "(J)V", (void*) nativeClosePage}
+};
+
+int register_android_graphics_pdf_PdfRenderer(JNIEnv* env) {
+ int result = RegisterMethodsOrDie(
+ env, "android/graphics/pdf/PdfRenderer", gPdfRenderer_Methods,
+ NELEM(gPdfRenderer_Methods));
+
+ jclass clazz = FindClassOrDie(env, "android/graphics/Point");
+ gPointClassInfo.x = GetFieldIDOrDie(env, clazz, "x", "I");
+ gPointClassInfo.y = GetFieldIDOrDie(env, clazz, "y", "I");
+
+ return result;
+};
+
+};
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 7b53ca6..0fb7c95 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -204,7 +204,7 @@
method public boolean isDefaultServiceForAid(android.content.ComponentName, String);
method public boolean isDefaultServiceForCategory(android.content.ComponentName, String);
method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>);
- method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String);
+ method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean);
method public boolean removeAidsForService(android.content.ComponentName, String);
method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setDefaultToObserveModeForService(@NonNull android.content.ComponentName, boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String);
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 65d0625..64f7fa4 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -32,7 +32,7 @@
boolean setDefaultForNextTap(int userHandle, in ComponentName service);
boolean setDefaultToObserveModeForService(int userId, in android.content.ComponentName service, boolean enable);
boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup);
- boolean registerPollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter);
+ boolean registerPollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter, boolean autoTransact);
boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement);
boolean unsetOffHostForService(int userHandle, in ComponentName service);
AidGroup getAidGroupForService(int userHandle, in ComponentName service, String category);
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index c81b95b..e62e37b 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -681,34 +681,18 @@
/**
* Add a Polling Loop Filter. Custom NFC polling frames that match this filter will be
- * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this or
- * {@link ApduServiceInfo#addPollingLoopFilterToAutoTransact(String)} multiple times will
- * cause the value to be overwritten each time.
+ * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this
+ * multiple times will cause the value to be overwritten each time.
* @param pollingLoopFilter the polling loop filter to add, must be a valide hexadecimal string
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
- public void addPollingLoopFilter(@NonNull String pollingLoopFilter) {
- mAutoTransact.put(pollingLoopFilter.toUpperCase(Locale.ROOT), false);
+ public void addPollingLoopFilter(@NonNull String pollingLoopFilter,
+ boolean autoTransact) {
+ mAutoTransact.put(pollingLoopFilter, autoTransact);
}
/**
- * Add a Polling Loop Filter. Custom NFC polling frames that match this filter will cause the
- * device to exit observe mode, just as if
- * {@link android.nfc.NfcAdapter#setObserveModeEnabled(boolean)} had been called with true,
- * allowing transactions to proceed. The matching frame will also be delivered to
- * {@link HostApduService#processPollingFrames(List)}. Adding a key with this or
- * {@link ApduServiceInfo#addPollingLoopFilter(String)} multiple times will
- * cause the value to be overwritten each time.
- *
- * @param pollingLoopFilter the polling loop filter to add, must be a valide hexadecimal string
- */
- @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
- public void addPollingLoopFilterToAutoTransact(@NonNull String pollingLoopFilter) {
- mAutoTransact.put(pollingLoopFilter.toUpperCase(Locale.ROOT), true);
- }
-
- /**
* Remove a Polling Loop Filter. Custom NFC polling frames that match this filter will no
* longer be delivered to {@link HostApduService#processPollingFrames(List)}.
* @param pollingLoopFilter this polling loop filter to add.
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index e681a85..47ddd9d 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -42,6 +42,7 @@
import android.util.Log;
import java.util.HashMap;
+import java.util.HexFormat;
import java.util.List;
import java.util.regex.Pattern;
@@ -59,7 +60,6 @@
*/
public final class CardEmulation {
private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?");
- private static final Pattern PLF_PATTERN = Pattern.compile("[0-9A-Fa-f]{1,32}");
static final String TAG = "CardEmulation";
@@ -360,21 +360,28 @@
}
/**
- * Register a polling loop filter (PLF) for a HostApduService. The PLF can be sequence of an
- * even number of hexadecimal numbers (0-9, A-F or a-f). When non-standard polling loop frame
- * matches this sequence exactly, it may be delivered to
- * {@link HostApduService#processPollingFrames(List)} if this service is currently
- * preferred or there are no other services registered for this filter.
+ * Register a polling loop filter (PLF) for a HostApduService and indicate whether it should
+ * auto-transact or not. The PLF can be sequence of an
+ * even number of at least 2 hexadecimal numbers (0-9, A-F or a-f), representing a series of
+ * bytes. When non-standard polling loop frame matches this sequence exactly, it may be
+ * delivered to {@link HostApduService#processPollingFrames(List)}. If auto-transact is set to
+ * true, then observe mode will also be disabled. if this service is currently preferred or
+ * there are no other services registered for this filter.
* @param service The HostApduService to register the filter for
* @param pollingLoopFilter The filter to register
+ * @param autoTransact true to have the NFC stack automatically disable observe mode and allow
+ * transactions to proceed when this filter matches, false otherwise
* @return true if the filter was registered, false otherwise
+ * @throws IllegalArgumentException if the passed in string doesn't parse to at least one byte
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public boolean registerPollingLoopFilterForService(@NonNull ComponentName service,
- @NonNull String pollingLoopFilter) {
+ @NonNull String pollingLoopFilter, boolean autoTransact) {
+ pollingLoopFilter = validatePollingLoopFilter(pollingLoopFilter);
+
try {
return sService.registerPollingLoopFilterForService(mContext.getUser().getIdentifier(),
- service, pollingLoopFilter);
+ service, pollingLoopFilter, autoTransact);
} catch (RemoteException e) {
// Try one more time
recoverService();
@@ -384,7 +391,8 @@
}
try {
return sService.registerPollingLoopFilterForService(
- mContext.getUser().getIdentifier(), service, pollingLoopFilter);
+ mContext.getUser().getIdentifier(), service,
+ pollingLoopFilter, autoTransact);
} catch (RemoteException ee) {
Log.e(TAG, "Failed to reach CardEmulationService.");
return false;
@@ -979,15 +987,14 @@
* @hide
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
- public static boolean isValidPollingLoopFilter(@NonNull String pollingLoopFilter) {
+ public static @NonNull String validatePollingLoopFilter(@NonNull String pollingLoopFilter) {
// Verify hex characters
- if (!PLF_PATTERN.matcher(pollingLoopFilter).matches()) {
- Log.e(TAG, "Polling Loop Filter " + pollingLoopFilter
- + " is not a valid Polling Loop Filter.");
- return false;
+ byte[] plfBytes = HexFormat.of().parseHex(pollingLoopFilter);
+ if (plfBytes.length == 0) {
+ throw new IllegalArgumentException(
+ "Polling loop filter must contain at least one byte.");
}
-
- return true;
+ return HexFormat.of().withUpperCase().formatHex(plfBytes);
}
/**
diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml
index 7cb1d01..b4d2eeb 100644
--- a/packages/CredentialManager/res/values/colors.xml
+++ b/packages/CredentialManager/res/values/colors.xml
@@ -22,4 +22,7 @@
<color name="dropdown_container">#F3F3FA</color>
<color name="sign_in_options_container">#DADADA</color>
<color name="sign_in_options_icon_color">#1B1B1B</color>
+
+ <!-- These colors are used for Inline Suggestions. -->
+ <color name="inline_background">#FFFFFF</color>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index b47a4dc..350920b2 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -28,4 +28,6 @@
<dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
<integer name="autofill_max_visible_datasets">5</integer>
<dimen name="dropdown_touch_target_min_height">48dp</dimen>
+ <dimen name="horizontal_chip_padding">8dp</dimen>
+ <dimen name="vertical_chip_padding">6dp</dimen>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index eef75c7..0f17217 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -57,6 +57,7 @@
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.android.credentialmanager.GetFlowUtils
+import com.android.credentialmanager.common.ui.InlinePresentationsFactory
import com.android.credentialmanager.common.ui.RemoteViewsFactory
import com.android.credentialmanager.getflow.ProviderDisplayInfo
import com.android.credentialmanager.getflow.toProviderDisplayInfo
@@ -294,8 +295,12 @@
} else {
inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
}
- inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon,
- spec!!, duplicateDisplayNamesForPasskeys)
+ if (spec != null) {
+ inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon,
+ InlinePresentationsFactory.modifyInlinePresentationSpec
+ (this@CredentialAutofillService, spec),
+ duplicateDisplayNamesForPasskeys)
+ }
}
var dropdownPresentation: RemoteViews? = null
if (i < lastDropdownDatasetIndex) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
new file mode 100644
index 0000000..3ebdd20
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
@@ -0,0 +1,84 @@
+/*
+* Copyright (C) 2024 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.credentialmanager.common.ui
+
+
+import android.content.Context
+import android.util.Size
+import android.widget.inline.InlinePresentationSpec
+import androidx.autofill.inline.common.TextViewStyle
+import androidx.autofill.inline.common.ViewStyle
+import androidx.autofill.inline.UiVersions
+import androidx.autofill.inline.UiVersions.Style
+import androidx.autofill.inline.v1.InlineSuggestionUi
+import androidx.core.content.ContextCompat
+import android.util.TypedValue
+import android.graphics.Typeface
+
+
+class InlinePresentationsFactory {
+ companion object {
+ private const val googleSansMediumFontFamily = "google-sans-medium"
+ private const val googleSansTextFontFamily = "google-sans-text"
+ // There is no min width required for now but this is needed for the spec builder
+ private const val minInlineWidth = 5000
+
+
+ fun modifyInlinePresentationSpec(context: Context,
+ originalSpec: InlinePresentationSpec): InlinePresentationSpec {
+ return InlinePresentationSpec.Builder(Size(originalSpec.minSize.width, originalSpec
+ .minSize.height),
+ Size(minInlineWidth, originalSpec
+ .maxSize.height))
+ .setStyle(UiVersions.newStylesBuilder().addStyle(getStyle(context)).build())
+ .build()
+ }
+
+
+ fun getStyle(context: Context): Style {
+ val textColorPrimary = ContextCompat.getColor(context,
+ com.android.credentialmanager.R.color.text_primary)
+ val textColorSecondary = ContextCompat.getColor(context,
+ com.android.credentialmanager.R.color.text_secondary)
+ val textColorBackground = ContextCompat.getColor(context,
+ com.android.credentialmanager.R.color.inline_background)
+ val chipHorizontalPadding = context.resources.getDimensionPixelSize(com.android
+ .credentialmanager.R.dimen.horizontal_chip_padding)
+ val chipVerticalPadding = context.resources.getDimensionPixelSize(com.android
+ .credentialmanager.R.dimen.vertical_chip_padding)
+ return InlineSuggestionUi.newStyleBuilder()
+ .setChipStyle(
+ ViewStyle.Builder().setPadding(chipHorizontalPadding,
+ chipVerticalPadding,
+ chipHorizontalPadding, chipVerticalPadding).build()
+ )
+ .setTitleStyle(
+ TextViewStyle.Builder().setTextColor(textColorPrimary).setTextSize
+ (TypedValue.COMPLEX_UNIT_DIP, 14F)
+ .setTypeface(googleSansMediumFontFamily,
+ Typeface.NORMAL).setBackgroundColor(textColorBackground)
+ .build()
+ )
+ .setSubtitleStyle(TextViewStyle.Builder().setTextColor(textColorSecondary)
+ .setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12F).setTypeface
+ (googleSansTextFontFamily, Typeface.NORMAL).setBackgroundColor
+ (textColorBackground).build())
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/FusedLocation/AndroidManifest.xml b/packages/FusedLocation/AndroidManifest.xml
index 05561d7..158c33a 100644
--- a/packages/FusedLocation/AndroidManifest.xml
+++ b/packages/FusedLocation/AndroidManifest.xml
@@ -28,6 +28,7 @@
<uses-permission android:name="android.permission.INSTALL_LOCATION_PROVIDER" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<application
android:label="@string/app_label"
@@ -49,5 +50,17 @@
<meta-data android:name="serviceVersion" android:value="0" />
<meta-data android:name="serviceIsMultiuser" android:value="true" />
</service>
+
+ <!-- GNSS overlay Service that LocationManagerService binds to.
+ LocationManagerService will bind to the service with the highest
+ version. -->
+ <service android:name="com.android.location.gnss.GnssOverlayLocationService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.location.provider.action.GNSS_PROVIDER" />
+ </intent-filter>
+ <meta-data android:name="serviceVersion" android:value="0" />
+ <meta-data android:name="serviceIsMultiuser" android:value="true" />
+ </service>
</application>
</manifest>
diff --git a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java
new file mode 100644
index 0000000..c6576e3
--- /dev/null
+++ b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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.location.gnss;
+
+import static android.location.provider.ProviderProperties.ACCURACY_FINE;
+import static android.location.provider.ProviderProperties.POWER_USAGE_HIGH;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.location.provider.LocationProviderBase;
+import android.location.provider.ProviderProperties;
+import android.location.provider.ProviderRequest;
+import android.os.Bundle;
+import android.util.SparseArray;
+
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ConcurrentUtils;
+
+import java.util.List;
+
+/** Basic pass-through GNSS location provider implementation. */
+public class GnssOverlayLocationProvider extends LocationProviderBase {
+
+ private static final String TAG = "GnssOverlay";
+
+ private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder()
+ .setHasAltitudeSupport(true)
+ .setHasSpeedSupport(true)
+ .setHasBearingSupport(true)
+ .setPowerUsage(POWER_USAGE_HIGH)
+ .setAccuracy(ACCURACY_FINE)
+ .build();
+
+ @GuardedBy("mPendingFlushes")
+ private final SparseArray<OnFlushCompleteCallback> mPendingFlushes = new SparseArray<>();
+
+ private final LocationManager mLocationManager;
+
+ private final GnssLocationListener mGnssLocationListener = new GnssLocationListener();
+
+ @GuardedBy("mPendingFlushes")
+ private int mFlushCode = 0;
+
+ /** Location listener for receiving locations from LocationManager. */
+ private class GnssLocationListener implements LocationListener {
+ @Override
+ public void onLocationChanged(Location location) {
+ reportLocation(location);
+ }
+
+ @Override
+ public void onLocationChanged(List<Location> locations) {
+ reportLocations(locations);
+ }
+
+ @Override
+ public void onFlushComplete(int requestCode) {
+ OnFlushCompleteCallback flushCompleteCallback;
+ synchronized (mPendingFlushes) {
+ flushCompleteCallback = mPendingFlushes.get(requestCode);
+ mPendingFlushes.remove(requestCode);
+ }
+ if (flushCompleteCallback != null) {
+ flushCompleteCallback.onFlushComplete();
+ }
+ }
+ }
+
+ public GnssOverlayLocationProvider(Context context) {
+ super(context, TAG, PROPERTIES);
+ mLocationManager = context.getSystemService(LocationManager.class);
+ }
+
+ void start() {
+ }
+
+ void stop() {
+ mLocationManager.removeUpdates(mGnssLocationListener);
+ }
+
+ @Override
+ public void onSendExtraCommand(String command, @Nullable Bundle extras) {
+ mLocationManager.sendExtraCommand(LocationManager.GPS_HARDWARE_PROVIDER, command, extras);
+ }
+
+ @Override
+ public void onFlush(OnFlushCompleteCallback callback) {
+ int flushCodeCopy;
+ synchronized (mPendingFlushes) {
+ flushCodeCopy = mFlushCode++;
+ mPendingFlushes.put(flushCodeCopy, callback);
+ }
+ mLocationManager.requestFlush(
+ LocationManager.GPS_HARDWARE_PROVIDER, mGnssLocationListener, flushCodeCopy);
+ }
+
+ @Override
+ public void onSetRequest(ProviderRequest request) {
+ if (request.isActive()) {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_HARDWARE_PROVIDER,
+ new LocationRequest.Builder(request.getIntervalMillis())
+ .setMaxUpdateDelayMillis(request.getMaxUpdateDelayMillis())
+ .setLowPower(request.isLowPower())
+ .setLocationSettingsIgnored(request.isLocationSettingsIgnored())
+ .setWorkSource(request.getWorkSource())
+ .setQuality(request.getQuality())
+ .build(),
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ mGnssLocationListener);
+ } else {
+ mLocationManager.removeUpdates(mGnssLocationListener);
+ }
+ }
+}
diff --git a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java
new file mode 100644
index 0000000..dd034fe
--- /dev/null
+++ b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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.location.gnss;
+
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class GnssOverlayLocationService extends Service {
+
+ @Nullable private GnssOverlayLocationProvider mProvider;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mProvider == null) {
+ mProvider = new GnssOverlayLocationProvider(this);
+ mProvider.start();
+ }
+
+ return mProvider.getBinder();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProvider != null) {
+ mProvider.stop();
+ mProvider = null;
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+}
diff --git a/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java
new file mode 100644
index 0000000..5b33deb
--- /dev/null
+++ b/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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.location.gnss.tests;
+
+import static android.location.LocationManager.GPS_HARDWARE_PROVIDER;
+
+import static androidx.test.ext.truth.location.LocationSubject.assertThat;
+
+import android.content.Context;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.location.provider.ILocationProvider;
+import android.location.provider.ILocationProviderManager;
+import android.location.provider.ProviderProperties;
+import android.location.provider.ProviderRequest;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.location.gnss.GnssOverlayLocationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssOverlayLocationServiceTest {
+
+ private static final String TAG = "GnssOverlayLocationServiceTest";
+
+ private static final long TIMEOUT_MS = 5000;
+
+ private Random mRandom;
+ private LocationManager mLocationManager;
+
+ private ILocationProvider mProvider;
+ private LocationProviderManagerCapture mManager;
+
+ @Before
+ public void setUp() throws Exception {
+ long seed = System.currentTimeMillis();
+ Log.i(TAG, "location seed: " + seed);
+
+ Context context = ApplicationProvider.getApplicationContext();
+ mRandom = new Random(seed);
+ mLocationManager = context.getSystemService(LocationManager.class);
+
+ setMockLocation(true);
+
+ mManager = new LocationProviderManagerCapture();
+ mProvider = ILocationProvider.Stub.asInterface(
+ new GnssOverlayLocationProvider(context).getBinder());
+ mProvider.setLocationProviderManager(mManager);
+
+ mLocationManager.addTestProvider(GPS_HARDWARE_PROVIDER,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ Criteria.POWER_MEDIUM,
+ Criteria.ACCURACY_FINE);
+ mLocationManager.setTestProviderEnabled(GPS_HARDWARE_PROVIDER, true);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (String provider : mLocationManager.getAllProviders()) {
+ mLocationManager.removeTestProvider(provider);
+ }
+
+ setMockLocation(false);
+ }
+
+ @Test
+ public void testGpsRequest() throws Exception {
+ mProvider.setRequest(
+ new ProviderRequest.Builder()
+ .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+ .setIntervalMillis(1000)
+ .build());
+
+ Location location = createLocation(GPS_HARDWARE_PROVIDER, mRandom);
+ mLocationManager.setTestProviderLocation(GPS_HARDWARE_PROVIDER, location);
+
+ assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location);
+ }
+
+ private static class LocationProviderManagerCapture extends ILocationProviderManager.Stub {
+
+ private final LinkedBlockingQueue<Location> mLocations;
+
+ private LocationProviderManagerCapture() {
+ mLocations = new LinkedBlockingQueue<>();
+ }
+
+ @Override
+ public void onInitialize(boolean allowed, ProviderProperties properties,
+ String attributionTag) {}
+
+ @Override
+ public void onSetAllowed(boolean allowed) {}
+
+ @Override
+ public void onSetProperties(ProviderProperties properties) {}
+
+ @Override
+ public void onReportLocation(Location location) {
+ mLocations.add(location);
+ }
+
+ @Override
+ public void onReportLocations(List<Location> locations) {
+ mLocations.addAll(locations);
+ }
+
+ @Override
+ public void onFlushComplete() {}
+
+ public Location getNextLocation(long timeoutMs) throws InterruptedException {
+ return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private static final double MIN_LATITUDE = -90D;
+ private static final double MAX_LATITUDE = 90D;
+ private static final double MIN_LONGITUDE = -180D;
+ private static final double MAX_LONGITUDE = 180D;
+
+ private static final float MIN_ACCURACY = 1;
+ private static final float MAX_ACCURACY = 100;
+
+ private static Location createLocation(String provider, Random random) {
+ return createLocation(provider,
+ MIN_LATITUDE + random.nextDouble() * (MAX_LATITUDE - MIN_LATITUDE),
+ MIN_LONGITUDE + random.nextDouble() * (MAX_LONGITUDE - MIN_LONGITUDE),
+ MIN_ACCURACY + random.nextFloat() * (MAX_ACCURACY - MIN_ACCURACY));
+ }
+
+ private static Location createLocation(String provider, double latitude, double longitude,
+ float accuracy) {
+ Location location = new Location(provider);
+ location.setLatitude(latitude);
+ location.setLongitude(longitude);
+ location.setAccuracy(accuracy);
+ location.setTime(System.currentTimeMillis());
+ location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
+ return location;
+ }
+
+ private static void setMockLocation(boolean allowed) throws IOException {
+ ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .executeShellCommand("appops set "
+ + InstrumentationRegistry.getTargetContext().getPackageName()
+ + " android:mock_location " + (allowed ? "allow" : "deny"));
+ try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] buffer = new byte[32768];
+ int count;
+ try {
+ while ((count = fis.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ fis.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ Log.e(TAG, new String(os.toByteArray()));
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 904e184..cf6aab6 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -403,7 +403,7 @@
resolvedPath = info.getResolvedBaseApkPath();
}
if (info == null || !info.isSealed() || resolvedPath == null) {
- Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
+ Log.w(TAG, "Session " + sessionId + " in funky state; ignoring");
finish();
return;
}
@@ -418,7 +418,7 @@
-1 /* defaultValue */);
final SessionInfo info = mInstaller.getSessionInfo(sessionId);
if (info == null || !info.isPreApprovalRequested()) {
- Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
+ Log.w(TAG, "Session " + sessionId + " in funky state; ignoring");
finish();
return;
}
@@ -839,7 +839,9 @@
// work for the multiple user case, i.e. the caller task user and started
// Activity user are not the same. To avoid having multiple PIAs in the task,
// finish the current PackageInstallerActivity
- finish();
+ // Because finish() is overridden to set the installation result, we must use
+ // the original finish() method, or the confirmation dialog fails to appear.
+ PackageInstallerActivity.super.finish();
}
}, 500);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d1a3571..bed95a5 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -550,5 +550,6 @@
required: [
"privapp_whitelist_com.android.systemui",
"wmshell.protolog.json.gz",
+ "wmshell.protolog.pb",
],
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 71f9ba27..a69a2a6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -377,6 +377,13 @@
}
flag {
+ name: "screenshot_action_dismiss_system_windows"
+ namespace: "systemui"
+ description: "Dismiss existing system windows when starting action from screenshot UI"
+ bug: "309933761"
+}
+
+flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
@@ -529,3 +536,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "bind_keyguard_media_visibility"
+ namespace: "systemui"
+ description: "Binds Keyguard Media Controller Visibility to MediaContainerView"
+ bug: "298213983"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
index 535c2d3..e862f0c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
@@ -17,7 +17,6 @@
import android.view.View
import androidx.annotation.VisibleForTesting
-import java.util.Random
/** Plays [TurbulenceNoiseView] in ease-in, main (no easing), and ease-out order. */
class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) {
@@ -37,8 +36,6 @@
}
}
- private val random = Random()
-
/** Current state of the animation. */
@VisibleForTesting
var state: AnimationState = AnimationState.NOT_PLAYING
@@ -95,12 +92,7 @@
}
state = AnimationState.EASE_IN
- // Add offset to avoid repetitive noise.
- turbulenceNoiseView.playEaseIn(
- offsetX = random.nextFloat(),
- offsetY = random.nextFloat(),
- this::playMainAnimation
- )
+ turbulenceNoiseView.playEaseIn(this::playMainAnimation)
}
private fun playMainAnimation() {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
index c59bc10..5e72e3b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
@@ -109,7 +109,7 @@
/** Plays the turbulence noise with linear ease-in. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- fun playEaseIn(offsetX: Float = 0f, offsetY: Float = 0f, onAnimationEnd: Runnable? = null) {
+ fun playEaseIn(onAnimationEnd: Runnable? = null) {
if (noiseConfig == null) {
return
}
@@ -129,8 +129,8 @@
val progress = updateListener.animatedValue as Float
shader.setNoiseMove(
- offsetX + initialX + timeInSec * config.noiseMoveSpeedX,
- offsetY + initialY + timeInSec * config.noiseMoveSpeedY,
+ initialX + timeInSec * config.noiseMoveSpeedX,
+ initialY + timeInSec * config.noiseMoveSpeedY,
initialZ + timeInSec * config.noiseMoveSpeedZ
)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
index b8c4fae..62dd4ac 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
@@ -57,9 +57,6 @@
import com.android.compose.modifiers.padding
import com.android.compose.theme.LocalAndroidColorScheme
-/** Indicator corner radius used when the user drags the [PlatformSlider]. */
-private val DefaultPlatformSliderDraggingCornerRadius = 8.dp
-
/**
* Platform slider implementation that displays a slider with an [icon] and a [label] at the start.
*
@@ -83,10 +80,8 @@
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- colors: PlatformSliderColors =
- if (isSystemInDarkTheme()) darkThemePlatformSliderColors()
- else lightThemePlatformSliderColors(),
- draggingCornersRadius: Dp = DefaultPlatformSliderDraggingCornerRadius,
+ colors: PlatformSliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
+ draggingCornersRadius: Dp = PlatformSliderDefaults.DefaultPlatformSliderDraggingCornerRadius,
icon: (@Composable (isDragging: Boolean) -> Unit)? = null,
label: (@Composable (isDragging: Boolean) -> Unit)? = null,
) {
@@ -109,7 +104,7 @@
val paddingStart by
animateDpAsState(
targetValue =
- if ((!isDragging && value == 0f) || icon == null) {
+ if ((!isDragging && value == valueRange.start) || icon == null) {
16.dp
} else {
0.dp
@@ -125,6 +120,7 @@
valueRange = valueRange,
onValueChangeFinished = onValueChangeFinished,
interactionSource = interactionSource,
+ enabled = enabled,
track = {
Track(
sliderState = it,
@@ -286,6 +282,17 @@
val disabledLabelColor: Color,
)
+object PlatformSliderDefaults {
+
+ /** Indicator corner radius used when the user drags the [PlatformSlider]. */
+ val DefaultPlatformSliderDraggingCornerRadius = 8.dp
+
+ @Composable
+ fun defaultPlatformSliderColors(): PlatformSliderColors =
+ if (isSystemInDarkTheme()) darkThemePlatformSliderColors()
+ else lightThemePlatformSliderColors()
+}
+
/** [PlatformSliderColors] for the light theme */
@Composable
private fun lightThemePlatformSliderColors() =
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt
new file mode 100644
index 0000000..b4cb098
--- /dev/null
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume
+
+import dagger.Module
+
+@Module interface VolumeSlidersModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 3677cab..53f400f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -20,6 +20,8 @@
import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeWeatherClockBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.WeatherClockBlueprintModule
import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
import dagger.Module
@@ -31,6 +33,8 @@
OptionalSectionModule::class,
ShortcutsBesideUdfpsBlueprintModule::class,
SplitShadeBlueprintModule::class,
+ SplitShadeWeatherClockBlueprintModule::class,
+ WeatherClockBlueprintModule::class,
],
)
interface LockscreenSceneBlueprintModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index a07ab4a..452dc03 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -33,7 +33,7 @@
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
-import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.keyguard.ui.composable.section.LockSection
import com.android.systemui.keyguard.ui.composable.section.NotificationSection
import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
@@ -56,7 +56,7 @@
constructor(
private val viewModel: LockscreenContentViewModel,
private val statusBarSection: StatusBarSection,
- private val clockSection: ClockSection,
+ private val clockSection: DefaultClockSection,
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index b035e42..71c60c7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -33,7 +33,7 @@
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
-import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.keyguard.ui.composable.section.LockSection
import com.android.systemui.keyguard.ui.composable.section.NotificationSection
import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
@@ -56,7 +56,7 @@
constructor(
private val viewModel: LockscreenContentViewModel,
private val statusBarSection: StatusBarSection,
- private val clockSection: ClockSection,
+ private val clockSection: DefaultClockSection,
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
index 44fe883..af836b6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -39,7 +39,7 @@
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
-import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.keyguard.ui.composable.section.LockSection
import com.android.systemui.keyguard.ui.composable.section.NotificationSection
import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
@@ -63,7 +63,7 @@
constructor(
private val viewModel: LockscreenContentViewModel,
private val statusBarSection: StatusBarSection,
- private val clockSection: ClockSection,
+ private val clockSection: DefaultClockSection,
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
new file mode 100644
index 0000000..e2e7a95
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2024 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.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.systemui.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
+import com.android.systemui.keyguard.ui.composable.section.WeatherClockSection
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import java.util.Optional
+import javax.inject.Inject
+
+class WeatherClockBlueprint
+@Inject
+constructor(
+ private val viewModel: LockscreenContentViewModel,
+ private val statusBarSection: StatusBarSection,
+ private val weatherClockSection: WeatherClockSection,
+ private val smartSpaceSection: SmartSpaceSection,
+ private val notificationSection: NotificationSection,
+ private val lockSection: LockSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
+ private val bottomAreaSection: BottomAreaSection,
+ private val settingsMenuSection: SettingsMenuSection,
+ private val clockInteractor: KeyguardClockInteractor,
+) : ComposableLockscreenSceneBlueprint {
+
+ override val id: String = WEATHER_CLOCK_BLUEPRINT_ID
+ @Composable
+ override fun SceneScope.Content(modifier: Modifier) {
+ val isUdfpsVisible = viewModel.isUdfpsVisible
+ val burnIn = rememberBurnIn(clockInteractor)
+ val resources = LocalContext.current.resources
+
+ LockscreenLongPress(
+ viewModel = viewModel.longPress,
+ modifier = modifier,
+ ) { onSettingsMenuPlaced ->
+ Layout(
+ content = {
+ // Constrained to above the lock icon.
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+ // TODO: Add weather clock for small and large clock
+ with(smartSpaceSection) {
+ SmartSpace(
+ burnInParams = burnIn.parameters,
+ onTopChanged = burnIn.onSmartspaceTopChanged,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ top = { viewModel.getSmartSpacePaddingTop(resources) },
+ )
+ .padding(
+ bottom =
+ dimensionResource(
+ R.dimen.keyguard_status_view_bottom_margin
+ ),
+ ),
+ )
+ }
+
+ if (viewModel.areNotificationsVisible) {
+ with(notificationSection) {
+ Notifications(
+ modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+ )
+ }
+ }
+
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
+
+ with(lockSection) { LockIcon() }
+
+ // Aligned to bottom and constrained to below the lock icon.
+ Column(modifier = Modifier.fillMaxWidth()) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ with(bottomAreaSection) {
+ IndicationArea(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ // Aligned to bottom and NOT constrained by the lock icon.
+ with(bottomAreaSection) {
+ Shortcut(isStart = true, applyPadding = true)
+ Shortcut(isStart = false, applyPadding = true)
+ }
+ with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
+ },
+ modifier = Modifier.fillMaxSize(),
+ ) { measurables, constraints ->
+ check(measurables.size == 6)
+ val aboveLockIconMeasurable = measurables[0]
+ val lockIconMeasurable = measurables[1]
+ val belowLockIconMeasurable = measurables[2]
+ val startShortcutMeasurable = measurables[3]
+ val endShortcutMeasurable = measurables[4]
+ val settingsMenuMeasurable = measurables[5]
+
+ val noMinConstraints =
+ constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ )
+ val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+ val lockIconBounds =
+ IntRect(
+ left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+ top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+ right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+ bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+ )
+
+ val aboveLockIconPlaceable =
+ aboveLockIconMeasurable.measure(
+ noMinConstraints.copy(maxHeight = lockIconBounds.top)
+ )
+ val belowLockIconPlaceable =
+ belowLockIconMeasurable.measure(
+ noMinConstraints.copy(
+ maxHeight =
+ (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
+ )
+ )
+ val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
+ val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
+ val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ aboveLockIconPlaceable.place(
+ x = 0,
+ y = 0,
+ )
+ lockIconPlaceable.place(
+ x = lockIconBounds.left,
+ y = lockIconBounds.top,
+ )
+ belowLockIconPlaceable.place(
+ x = 0,
+ y = constraints.maxHeight - belowLockIconPlaceable.height,
+ )
+ startShortcutPleaceable.place(
+ x = 0,
+ y = constraints.maxHeight - startShortcutPleaceable.height,
+ )
+ endShortcutPleaceable.place(
+ x = constraints.maxWidth - endShortcutPleaceable.width,
+ y = constraints.maxHeight - endShortcutPleaceable.height,
+ )
+ settingsMenuPlaceable.place(
+ x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
+ y = constraints.maxHeight - settingsMenuPlaceable.height,
+ )
+ }
+ }
+ }
+ }
+}
+
+class SplitShadeWeatherClockBlueprint
+@Inject
+constructor(
+ private val viewModel: LockscreenContentViewModel,
+ private val statusBarSection: StatusBarSection,
+ private val smartSpaceSection: SmartSpaceSection,
+ private val notificationSection: NotificationSection,
+ private val lockSection: LockSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
+ private val bottomAreaSection: BottomAreaSection,
+ private val settingsMenuSection: SettingsMenuSection,
+ private val clockInteractor: KeyguardClockInteractor,
+ private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
+ private val weatherClockSection: WeatherClockSection,
+) : ComposableLockscreenSceneBlueprint {
+ override val id: String = SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+
+ @Composable
+ override fun SceneScope.Content(modifier: Modifier) {
+ val isUdfpsVisible = viewModel.isUdfpsVisible
+ val burnIn = rememberBurnIn(clockInteractor)
+ val resources = LocalContext.current.resources
+
+ LockscreenLongPress(
+ viewModel = viewModel.longPress,
+ modifier = modifier,
+ ) { onSettingsMenuPlaced ->
+ Layout(
+ content = {
+ // Constrained to above the lock icon.
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ // TODO: Add weather clock for small and large clock
+ Column(
+ modifier = Modifier.fillMaxHeight().weight(weight = 1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ with(smartSpaceSection) {
+ SmartSpace(
+ burnInParams = burnIn.parameters,
+ onTopChanged = burnIn.onSmartspaceTopChanged,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ top = {
+ viewModel.getSmartSpacePaddingTop(resources)
+ },
+ )
+ .padding(
+ bottom =
+ dimensionResource(
+ R.dimen
+ .keyguard_status_view_bottom_margin
+ )
+ ),
+ )
+ }
+ }
+ with(notificationSection) {
+ val splitShadeTopMargin: Dp =
+ if (Flags.centralizedStatusBarHeightFix()) {
+ largeScreenHeaderHelper.getLargeScreenHeaderHeight().dp
+ } else {
+ dimensionResource(
+ id = R.dimen.large_screen_shade_header_height
+ )
+ }
+ Notifications(
+ modifier =
+ Modifier.fillMaxHeight()
+ .weight(weight = 1f)
+ .padding(top = splitShadeTopMargin)
+ )
+ }
+ }
+
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
+
+ with(lockSection) { LockIcon() }
+
+ // Aligned to bottom and constrained to below the lock icon.
+ Column(modifier = Modifier.fillMaxWidth()) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ with(bottomAreaSection) {
+ IndicationArea(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ // Aligned to bottom and NOT constrained by the lock icon.
+ with(bottomAreaSection) {
+ Shortcut(isStart = true, applyPadding = true)
+ Shortcut(isStart = false, applyPadding = true)
+ }
+ with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
+ },
+ modifier = Modifier.fillMaxSize(),
+ ) { measurables, constraints ->
+ check(measurables.size == 6)
+ val aboveLockIconMeasurable = measurables[0]
+ val lockIconMeasurable = measurables[1]
+ val belowLockIconMeasurable = measurables[2]
+ val startShortcutMeasurable = measurables[3]
+ val endShortcutMeasurable = measurables[4]
+ val settingsMenuMeasurable = measurables[5]
+
+ val noMinConstraints =
+ constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ )
+ val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+ val lockIconBounds =
+ IntRect(
+ left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+ top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+ right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+ bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+ )
+
+ val aboveLockIconPlaceable =
+ aboveLockIconMeasurable.measure(
+ noMinConstraints.copy(maxHeight = lockIconBounds.top)
+ )
+ val belowLockIconPlaceable =
+ belowLockIconMeasurable.measure(
+ noMinConstraints.copy(
+ maxHeight =
+ (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
+ )
+ )
+ val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
+ val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
+ val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ aboveLockIconPlaceable.place(
+ x = 0,
+ y = 0,
+ )
+ lockIconPlaceable.place(
+ x = lockIconBounds.left,
+ y = lockIconBounds.top,
+ )
+ belowLockIconPlaceable.place(
+ x = 0,
+ y = constraints.maxHeight - belowLockIconPlaceable.height,
+ )
+ startShortcutPleaceable.place(
+ x = 0,
+ y = constraints.maxHeight - startShortcutPleaceable.height,
+ )
+ endShortcutPleaceable.place(
+ x = constraints.maxWidth - endShortcutPleaceable.width,
+ y = constraints.maxHeight - endShortcutPleaceable.height,
+ )
+ settingsMenuPlaceable.place(
+ x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
+ y = constraints.maxHeight - settingsMenuPlaceable.height,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Module
+interface WeatherClockBlueprintModule {
+ @Binds
+ @IntoSet
+ fun blueprint(blueprint: WeatherClockBlueprint): ComposableLockscreenSceneBlueprint
+}
+
+@Module
+interface SplitShadeWeatherClockBlueprintModule {
+ @Binds
+ @IntoSet
+ fun blueprint(blueprint: SplitShadeWeatherClockBlueprint): ComposableLockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
index 8bd0d45..97d5b41 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -35,7 +35,6 @@
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
-import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
@@ -55,7 +54,6 @@
private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val alphaViewModel: AodAlphaViewModel,
) {
/**
* Renders a single lockscreen shortcut.
@@ -104,7 +102,6 @@
content {
IndicationArea(
indicationAreaViewModel = indicationAreaViewModel,
- alphaViewModel = alphaViewModel,
indicationController = indicationController,
)
}
@@ -183,7 +180,6 @@
@Composable
private fun IndicationArea(
indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- alphaViewModel: AodAlphaViewModel,
indicationController: KeyguardIndicationController,
modifier: Modifier = Modifier,
) {
@@ -196,7 +192,6 @@
KeyguardIndicationAreaBinder.bind(
view = view,
viewModel = indicationAreaViewModel,
- aodAlphaViewModel = alphaViewModel,
indicationController = indicationController,
)
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
similarity index 93%
rename from packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
rename to packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index fa07baf..335c915 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -18,6 +18,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -38,14 +39,17 @@
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import javax.inject.Inject
-class ClockSection
+/** Provides small clock and large clock composables for the default clock face. */
+class DefaultClockSection
@Inject
constructor(
private val viewModel: KeyguardClockViewModel,
private val clockInteractor: KeyguardClockInteractor,
private val aodBurnInViewModel: AodBurnInViewModel,
+ private val lockscreenSmartspaceController: LockscreenSmartspaceController,
) {
@Composable
@@ -151,6 +155,7 @@
(newClockView.parent as? ViewGroup)?.removeView(newClockView)
it.addView(newClockView)
},
+ modifier = Modifier.fillMaxSize()
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index be6f022..4c76fb4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -37,7 +37,6 @@
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
@@ -78,15 +77,16 @@
if (DeviceEntryUdfpsRefactor.isEnabled) {
DeviceEntryIconView(context, null).apply {
id = R.id.device_entry_icon_view
- DeviceEntryIconViewBinder.bind(
- applicationScope,
- this,
- deviceEntryIconViewModel.get(),
- deviceEntryForegroundViewModel.get(),
- deviceEntryBackgroundViewModel.get(),
- falsingManager.get(),
- vibratorHelper.get(),
- )
+ // TODO: b/326624996 Bind the DeviceEntryIcon
+ // DeviceEntryIconViewBinder.bind(
+ // applicationScope,
+ // this,
+ // deviceEntryIconViewModel.get(),
+ // deviceEntryForegroundViewModel.get(),
+ // deviceEntryBackgroundViewModel.get(),
+ // falsingManager.get(),
+ // vibratorHelper.get(),
+ // )
}
} else {
// keyguardBottomAreaRefactor()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
new file mode 100644
index 0000000..2e7bc2a
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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.keyguard.ui.composable.section
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+/** Provides small clock and large clock composables for the weather clock layout. */
+class WeatherClockSection @Inject constructor() {
+ @Composable
+ fun SceneScope.Time(
+ modifier: Modifier = Modifier,
+ ) {
+ // TODO: compose view
+ }
+
+ @Composable
+ fun SceneScope.Date(
+ modifier: Modifier = Modifier,
+ ) {
+ // TODO: compose view
+ }
+
+ @Composable
+ fun SceneScope.Weather(
+ modifier: Modifier = Modifier,
+ ) {
+ // TODO: compose view
+ }
+
+ @Composable
+ fun SceneScope.DndAlarmStatus(
+ modifier: Modifier = Modifier,
+ ) {
+ // TODO: compose view
+ }
+
+ @Composable
+ fun SceneScope.Temperature(
+ modifier: Modifier = Modifier,
+ ) {
+ // TODO: compose view
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt
new file mode 100644
index 0000000..453ff02
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/VolumeSlidersModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume
+
+import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
+import com.android.systemui.volume.panel.component.volume.ui.composable.VolumeSlidersComponent
+import com.android.systemui.volume.panel.domain.AlwaysAvailableCriteria
+import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface VolumeSlidersModule {
+
+ @Binds
+ @IntoMap
+ @StringKey(VolumePanelComponents.VOLUME_SLIDERS)
+ fun bindVolumePanelUiComponent(component: VolumeSlidersComponent): VolumePanelUiComponent
+
+ @Binds
+ @IntoMap
+ @StringKey(VolumePanelComponents.VOLUME_SLIDERS)
+ fun bindComponentAvailabilityCriteria(
+ criteria: AlwaysAvailableCriteria
+ ): ComponentAvailabilityCriteria
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt
new file mode 100644
index 0000000..a197a4b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+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.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.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+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.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformSliderColors
+import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
+
+private const val EXPAND_DURATION_MILLIS = 500
+private const val COLLAPSE_DURATION_MILLIS = 300
+
+/** Volume sliders laid out in a collapsable column */
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun ColumnVolumeSliders(
+ viewModels: List<SliderViewModel>,
+ sliderColors: PlatformSliderColors,
+ isExpandable: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ require(viewModels.isNotEmpty())
+ var isExpanded: Boolean by remember { mutableStateOf(false) }
+ val transition = updateTransition(isExpanded, label = "CollapsableSliders")
+ Column(modifier = modifier) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ val sliderViewModel: SliderViewModel = viewModels.first()
+ val sliderState by viewModels.first().slider.collectAsState()
+ VolumeSlider(
+ modifier = Modifier.weight(1f),
+ state = sliderState,
+ onValueChangeFinished = { sliderViewModel.onValueChangeFinished(sliderState, it) },
+ sliderColors = sliderColors,
+ )
+
+ if (isExpandable) {
+ ExpandButton(
+ isExpanded = isExpanded,
+ onExpandedChanged = { isExpanded = it },
+ sliderColors,
+ Modifier,
+ )
+ }
+ }
+ transition.AnimatedVisibility(
+ visible = { it },
+ enter =
+ expandVertically(
+ animationSpec = tween(durationMillis = EXPAND_DURATION_MILLIS),
+ expandFrom = Alignment.CenterVertically,
+ ),
+ exit =
+ shrinkVertically(
+ animationSpec = tween(durationMillis = COLLAPSE_DURATION_MILLIS),
+ shrinkTowards = Alignment.CenterVertically,
+ ),
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ for (index in 1..viewModels.lastIndex) {
+ val sliderViewModel: SliderViewModel = viewModels[index]
+ val sliderState by sliderViewModel.slider.collectAsState()
+ transition.AnimatedVisibility(
+ visible = { it },
+ enter = enterTransition(index = index, totalCount = viewModels.size),
+ exit = exitTransition(index = index, totalCount = viewModels.size)
+ ) {
+ VolumeSlider(
+ modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
+ state = sliderState,
+ onValueChangeFinished = {
+ sliderViewModel.onValueChangeFinished(sliderState, it)
+ },
+ sliderColors = sliderColors,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ExpandButton(
+ isExpanded: Boolean,
+ onExpandedChanged: (Boolean) -> Unit,
+ sliderColors: PlatformSliderColors,
+ modifier: Modifier = Modifier,
+) {
+ IconButton(
+ modifier = modifier.size(64.dp),
+ onClick = { onExpandedChanged(!isExpanded) },
+ colors =
+ IconButtonDefaults.filledIconButtonColors(
+ containerColor = sliderColors.indicatorColor,
+ contentColor = sliderColors.iconColor
+ ),
+ ) {
+ Icon(
+ painter =
+ painterResource(
+ if (isExpanded) {
+ R.drawable.ic_filled_arrow_down
+ } else {
+ R.drawable.ic_filled_arrow_up
+ }
+ ),
+ contentDescription = null,
+ )
+ }
+}
+
+private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
+ val enterDelay = ((totalCount - index + 1) * 10).coerceAtLeast(0)
+ val enterDuration = (EXPAND_DURATION_MILLIS - enterDelay).coerceAtLeast(100)
+ return slideInVertically(
+ initialOffsetY = { (it * 0.25).toInt() },
+ animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
+ ) +
+ scaleIn(
+ initialScale = 0.9f,
+ animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
+ ) +
+ expandVertically(
+ initialHeight = { (it * 0.65).toInt() },
+ animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
+ clip = false,
+ expandFrom = Alignment.CenterVertically,
+ ) +
+ fadeIn(
+ animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
+ )
+}
+
+private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
+ val exitDuration = (COLLAPSE_DURATION_MILLIS - (totalCount - index + 1) * 10).coerceAtLeast(100)
+ return slideOutVertically(
+ targetOffsetY = { (it * 0.25).toInt() },
+ animationSpec = tween(durationMillis = exitDuration),
+ ) +
+ scaleOut(
+ targetScale = 0.9f,
+ animationSpec = tween(durationMillis = exitDuration),
+ ) +
+ shrinkVertically(
+ targetHeight = { (it * 0.65).toInt() },
+ animationSpec = tween(durationMillis = exitDuration),
+ clip = false,
+ shrinkTowards = Alignment.CenterVertically,
+ ) +
+ fadeOut(animationSpec = tween(durationMillis = exitDuration))
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt
new file mode 100644
index 0000000..910ee72
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.ui.composable
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformSliderColors
+import com.android.compose.grid.VerticalGrid
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
+
+@Composable
+fun GridVolumeSliders(
+ viewModels: List<SliderViewModel>,
+ sliderColors: PlatformSliderColors,
+ modifier: Modifier = Modifier,
+) {
+ require(viewModels.isNotEmpty())
+ VerticalGrid(
+ modifier = modifier,
+ columns = 2,
+ verticalSpacing = 16.dp,
+ horizontalSpacing = 24.dp,
+ ) {
+ for (sliderViewModel in viewModels) {
+ val sliderState = sliderViewModel.slider.collectAsState().value
+ VolumeSlider(
+ modifier = Modifier.fillMaxWidth(),
+ state = sliderState,
+ onValueChangeFinished = { sliderViewModel.onValueChangeFinished(sliderState, it) },
+ sliderColors = sliderColors,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
new file mode 100644
index 0000000..5925b14
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import com.android.compose.PlatformSlider
+import com.android.compose.PlatformSliderColors
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
+
+@Composable
+fun VolumeSlider(
+ state: SliderState,
+ onValueChangeFinished: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ sliderColors: PlatformSliderColors,
+) {
+ var value by remember { mutableFloatStateOf(state.value) }
+ PlatformSlider(
+ modifier = modifier,
+ value = value,
+ valueRange = state.valueRange,
+ onValueChange = { value = it },
+ onValueChangeFinished = { onValueChangeFinished(value) },
+ enabled = state.isEnabled,
+ icon = { isDragging ->
+ if (isDragging) {
+ Text(text = value.toInt().toString())
+ } else {
+ state.icon?.let { Icon(icon = it) }
+ }
+ },
+ colors = sliderColors,
+ label = {
+ Column(modifier = Modifier.animateContentSize()) {
+ Text(state.label, style = MaterialTheme.typography.titleMedium)
+
+ state.disabledMessage?.let { message ->
+ AnimatedVisibility(
+ !state.isEnabled,
+ enter = expandVertically { it },
+ exit = shrinkVertically { it },
+ ) {
+ Text(text = message, style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+ }
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt
new file mode 100644
index 0000000..6213dc5
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.ui.composable
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.android.compose.PlatformSliderDefaults
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
+import com.android.systemui.volume.panel.component.volume.ui.viewmodel.AudioVolumeComponentViewModel
+import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
+import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
+import javax.inject.Inject
+
+class VolumeSlidersComponent
+@Inject
+constructor(
+ private val viewModel: AudioVolumeComponentViewModel,
+) : ComposeVolumePanelUiComponent {
+
+ @Composable
+ override fun VolumePanelComposeScope.Content(modifier: Modifier) {
+ val sliderViewModels: List<SliderViewModel> by viewModel.sliderViewModels.collectAsState()
+ if (sliderViewModels.isEmpty()) {
+ return
+ }
+ if (isLargeScreen) {
+ GridVolumeSliders(
+ viewModels = sliderViewModels,
+ sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
+ modifier = modifier.fillMaxWidth(),
+ )
+ } else {
+ ColumnVolumeSliders(
+ viewModels = sliderViewModels,
+ sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
+ isExpandable = true,
+ modifier = modifier.fillMaxWidth(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
index 98ef067..0a651c8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
@@ -51,10 +51,8 @@
verticalArrangement = Arrangement.spacedBy(space = spacing, alignment = Alignment.Top)
) {
for (component in layout.headerComponents) {
- AnimatedVisibility(component.isVisible) {
- with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier.weight(1f))
- }
+ AnimatedVisibility(visible = component.isVisible) {
+ with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
}
}
Row(
@@ -62,9 +60,12 @@
horizontalArrangement = Arrangement.spacedBy(spacing),
) {
for (component in layout.footerComponents) {
- AnimatedVisibility(component.isVisible) {
+ AnimatedVisibility(
+ visible = component.isVisible,
+ modifier = Modifier.weight(1f),
+ ) {
with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier.weight(1f))
+ Content(Modifier)
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
index 2285128..4d07379 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
@@ -17,7 +17,6 @@
package com.android.systemui.volume.panel.ui.composable
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -34,14 +33,12 @@
modifier: Modifier = Modifier,
) {
Column(
- modifier = modifier.animateContentSize(),
+ modifier = modifier,
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
for (component in layout.headerComponents) {
AnimatedVisibility(component.isVisible) {
- with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier.weight(1f))
- }
+ with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
}
}
for (component in layout.contentComponents) {
@@ -55,9 +52,12 @@
horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp),
) {
for (component in layout.footerComponents) {
- AnimatedVisibility(component.isVisible) {
+ AnimatedVisibility(
+ visible = component.isVisible,
+ modifier = Modifier.weight(1f),
+ ) {
with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier.weight(1f))
+ Content(Modifier)
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt
index 8df8d2e..af69091 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt
@@ -29,7 +29,7 @@
/** Is true when Volume Panel is using large-screen layout and false the otherwise. */
val isLargeScreen: Boolean
- get() = state.isWideScreen
+ get() = state.isLargeScreen
}
val VolumePanelComposeScope.isPortrait: Boolean
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 259f349..5034631 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -44,12 +44,18 @@
import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.LockscreenShadeTransitionController
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
@@ -58,6 +64,10 @@
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -68,6 +78,7 @@
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@@ -120,6 +131,9 @@
@Mock private lateinit var shadeInteractor: ShadeInteractor
@Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
@Mock private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
+ private lateinit var powerRepository: FakePowerRepository
+ private lateinit var powerInteractor: PowerInteractor
+ private lateinit var testScope: TestScope
private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
@@ -127,6 +141,15 @@
@Before
fun setup() {
+ testScope = TestScope(StandardTestDispatcher())
+ powerRepository = FakePowerRepository()
+ powerInteractor =
+ PowerInteractor(
+ powerRepository,
+ mock(FalsingCollector::class.java),
+ mock(ScreenOffAnimationController::class.java),
+ statusBarStateController,
+ )
whenever(inflater.inflate(R.layout.udfps_view, null, false)).thenReturn(udfpsView)
whenever(inflater.inflate(R.layout.udfps_bp_view, null))
.thenReturn(mock(UdfpsBpView::class.java))
@@ -178,6 +201,8 @@
{ defaultUdfpsTouchOverlayViewModel },
shadeInteractor,
udfpsOverlayInteractor,
+ powerInteractor,
+ testScope,
)
block()
}
@@ -277,6 +302,66 @@
}
}
+ @Test
+ fun showUdfpsOverlay_awake() =
+ testScope.runTest {
+ withReason(REASON_AUTH_KEYGUARD) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ controllerOverlay.show(udfpsController, overlayParams)
+ runCurrent()
+ verify(windowManager).addView(any(), any())
+ }
+ }
+
+ @Test
+ fun showUdfpsOverlay_whileGoingToSleep() =
+ testScope.runTest {
+ withReason(REASON_AUTH_KEYGUARD) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.STARTING_TO_SLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ controllerOverlay.show(udfpsController, overlayParams)
+ runCurrent()
+ verify(windowManager, never()).addView(any(), any())
+
+ // we hide to end the job that listens for the finishedGoingToSleep signal
+ controllerOverlay.hide()
+ }
+ }
+
+ @Test
+ fun showUdfpsOverlay_afterFinishedGoingToSleep() =
+ testScope.runTest {
+ withReason(REASON_AUTH_KEYGUARD) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.STARTING_TO_SLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ controllerOverlay.show(udfpsController, overlayParams)
+ runCurrent()
+ verify(windowManager, never()).addView(any(), any())
+
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.ASLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+ verify(windowManager)
+ .addView(eq(controllerOverlay.getTouchOverlay()), layoutParamsCaptor.capture())
+ }
+ }
+
private fun showUdfpsOverlay() {
val didShow = controllerOverlay.show(udfpsController, overlayParams)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 529403a..561cdbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -86,6 +86,7 @@
import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
@@ -94,10 +95,15 @@
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.data.repository.FakePowerRepository;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
+import com.android.systemui.power.shared.model.WakeSleepReason;
+import com.android.systemui.power.shared.model.WakefulnessState;
import com.android.systemui.res.R;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
@@ -125,6 +131,8 @@
import java.util.ArrayList;
import java.util.List;
+import kotlinx.coroutines.CoroutineScope;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWithLooper(setAsMainLooper = true)
@@ -238,6 +246,8 @@
private ScreenLifecycle.Observer mScreenObserver;
private FingerprintSensorPropertiesInternal mOpticalProps;
private FingerprintSensorPropertiesInternal mUltrasonicProps;
+ private PowerInteractor mPowerInteractor;
+ private FakePowerRepository mPowerRepository;
@Mock
private InputManager mInputManager;
@Mock
@@ -253,6 +263,19 @@
@Before
public void setUp() {
+ mPowerRepository = new FakePowerRepository();
+ mPowerInteractor = new PowerInteractor(
+ mPowerRepository,
+ mock(FalsingCollector.class),
+ mock(ScreenOffAnimationController.class),
+ mStatusBarStateController
+ );
+ mPowerRepository.updateWakefulness(
+ WakefulnessState.AWAKE,
+ WakeSleepReason.POWER_BUTTON,
+ WakeSleepReason.OTHER,
+ /* powerButtonLaunchGestureTriggered */ false
+ );
mContext.getOrCreateTestableResources()
.addOverride(com.android.internal.R.bool.config_ignoreUdfpsVote, false);
@@ -346,7 +369,9 @@
mKeyguardTransitionInteractor,
mDeviceEntryUdfpsTouchOverlayViewModel,
mDefaultUdfpsTouchOverlayViewModel,
- mUdfpsOverlayInteractor
+ mUdfpsOverlayInteractor,
+ mPowerInteractor,
+ mock(CoroutineScope.class)
);
verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index a8fe16b..d86b35d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -20,6 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dock.DockManager
@@ -33,6 +34,7 @@
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
@@ -50,7 +52,7 @@
private lateinit var underTest: CommunalSceneStartable
@Before
- fun setUp() =
+ fun setUp() {
with(kosmos) {
underTest =
CommunalSceneStartable(
@@ -61,7 +63,15 @@
bgScope = applicationCoroutineScope,
)
.apply { start() }
+
+ // Make communal available so that communalInteractor.desiredScene accurately reflects
+ // scene changes instead of just returning Blank.
+ with(kosmos.testScope) {
+ launch { setCommunalAvailable(true) }
+ testScheduler.runCurrent()
+ }
}
+ }
@Test
fun keyguardGoesAway_forceBlankScene() =
@@ -249,4 +259,10 @@
fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
runCurrent()
}
+
+ private suspend fun TestScope.enableCommunal() =
+ with(kosmos) {
+ setCommunalAvailable(true)
+ runCurrent()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 4156d83..cd29652 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -18,7 +18,9 @@
package com.android.systemui.communal.domain.interactor
import android.app.smartspace.SmartspaceTarget
+import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
+import android.os.UserHandle
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -51,6 +53,8 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
@@ -96,6 +100,7 @@
private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter
private lateinit var sceneInteractor: SceneInteractor
+ private lateinit var userTracker: FakeUserTracker
private lateinit var underTest: CommunalInteractor
@@ -113,6 +118,7 @@
editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter
communalPrefsRepository = kosmos.fakeCommunalPrefsRepository
sceneInteractor = kosmos.sceneInteractor
+ userTracker = kosmos.fakeUserTracker
whenever(mainUser.isMain).thenReturn(true)
whenever(secondaryUser.isMain).thenReturn(false)
@@ -207,25 +213,19 @@
keyguardRepository.setKeyguardOccluded(false)
tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
- // Widgets are available.
- val widgets =
- listOf(
- CommunalWidgetContentModel(
- appWidgetId = 0,
- priority = 30,
- providerInfo = mock(),
- ),
- CommunalWidgetContentModel(
- appWidgetId = 1,
- priority = 20,
- providerInfo = mock(),
- ),
- CommunalWidgetContentModel(
- appWidgetId = 2,
- priority = 10,
- providerInfo = mock(),
- ),
- )
+ val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK)
+ userRepository.setUserInfos(userInfos)
+ userTracker.set(
+ userInfos = userInfos,
+ selectedUserIndex = 0,
+ )
+ runCurrent()
+
+ // Widgets available.
+ val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
+ val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
+ val widgets = listOf(widget1, widget2, widget3)
widgetRepository.setCommunalWidgets(widgets)
val widgetContent by collectLastValue(underTest.widgetContent)
@@ -455,6 +455,9 @@
@Test
fun listensToSceneChange() =
testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
var desiredScene = collectLastValue(underTest.desiredScene)
runCurrent()
assertThat(desiredScene()).isEqualTo(CommunalSceneKey.Blank)
@@ -479,6 +482,30 @@
}
@Test
+ fun desiredScene_communalNotAvailable_returnsBlank() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ val desiredScene by collectLastValue(underTest.desiredScene)
+
+ underTest.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(desiredScene).isEqualTo(CommunalSceneKey.Communal)
+
+ kosmos.setCommunalAvailable(false)
+ runCurrent()
+
+ // Scene returns blank when communal is not available.
+ assertThat(desiredScene).isEqualTo(CommunalSceneKey.Blank)
+
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ // After re-enabling, scene goes back to Communal.
+ assertThat(desiredScene).isEqualTo(CommunalSceneKey.Communal)
+ }
+
+ @Test
fun transitionProgress_onTargetScene_fullProgress() =
testScope.runTest {
val targetScene = CommunalSceneKey.Blank
@@ -604,8 +631,28 @@
}
@Test
+ fun isCommunalShowing() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ var isCommunalShowing = collectLastValue(underTest.isCommunalShowing)
+ runCurrent()
+ assertThat(isCommunalShowing()).isEqualTo(false)
+
+ underTest.onSceneChanged(CommunalSceneKey.Communal)
+
+ isCommunalShowing = collectLastValue(underTest.isCommunalShowing)
+ runCurrent()
+ assertThat(isCommunalShowing()).isEqualTo(true)
+ }
+
+ @Test
fun isCommunalShowing_whenSceneContainerDisabled() =
testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
// Verify default is false
val isCommunalShowing by collectLastValue(underTest.isCommunalShowing)
runCurrent()
@@ -752,6 +799,38 @@
verify(editWidgetsActivityStarter).startActivity(widgetKey)
}
+ @Test
+ fun filterWidgets_whenUserProfileRemoved() =
+ testScope.runTest {
+ // Keyguard showing, and tutorial completed.
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setKeyguardOccluded(false)
+ tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
+
+ // Only main user exists.
+ val userInfos = listOf(MAIN_USER_INFO)
+ userRepository.setUserInfos(userInfos)
+ userTracker.set(
+ userInfos = userInfos,
+ selectedUserIndex = 0,
+ )
+ runCurrent()
+
+ val widgetContent by collectLastValue(underTest.widgetContent)
+ // Given three widgets, and one of them is associated with pre-existing work profile.
+ val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
+ val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
+ val widgets = listOf(widget1, widget2, widget3)
+ widgetRepository.setCommunalWidgets(widgets)
+
+ // One widget is filtered out and the remaining two link to main user id.
+ assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
+ widgetContent!!.forEachIndexed { _, model ->
+ assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
+ }
+ }
+
private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
val timer = mock(SmartspaceTarget::class.java)
whenever(timer.smartspaceTargetId).thenReturn(id)
@@ -760,4 +839,17 @@
whenever(timer.creationTimeMillis).thenReturn(timestamp)
return timer
}
+
+ private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
+ mock<CommunalWidgetContentModel> {
+ whenever(this.appWidgetId).thenReturn(appWidgetId)
+ val providerInfo = mock<AppWidgetProviderInfo>()
+ whenever(providerInfo.profile).thenReturn(UserHandle(userId))
+ whenever(this.providerInfo).thenReturn(providerInfo)
+ }
+
+ private companion object {
+ val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
index 5211c55..8b78592 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
@@ -16,7 +16,6 @@
package com.android.systemui.communal.domain.interactor
-import android.content.pm.UserInfo
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED
@@ -61,7 +60,6 @@
communalInteractor = kosmos.communalInteractor
userRepository = kosmos.fakeUserRepository
- userRepository.setUserInfos(listOf(MAIN_USER_INFO))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
@@ -72,7 +70,7 @@
fun tutorialUnavailable_whenKeyguardNotVisible() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
- setCommunalAvailable(true)
+ kosmos.setCommunalAvailable(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
keyguardRepository.setKeyguardShowing(false)
assertThat(isTutorialAvailable).isFalse()
@@ -82,10 +80,7 @@
fun tutorialUnavailable_whenTutorialIsCompleted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
- setCommunalAvailable(true)
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
assertThat(isTutorialAvailable).isFalse()
}
@@ -94,7 +89,7 @@
fun tutorialUnavailable_whenCommunalNotAvailable() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
- setCommunalAvailable(false)
+ kosmos.setCommunalAvailable(false)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
keyguardRepository.setKeyguardShowing(true)
assertThat(isTutorialAvailable).isFalse()
@@ -104,10 +99,7 @@
fun tutorialAvailable_whenTutorialNotStarted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
- setCommunalAvailable(true)
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+ kosmos.setCommunalAvailable(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
assertThat(isTutorialAvailable).isTrue()
}
@@ -116,10 +108,7 @@
fun tutorialAvailable_whenTutorialIsStarted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
- setCommunalAvailable(true)
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
assertThat(isTutorialAvailable).isTrue()
}
@@ -129,10 +118,9 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED)
}
@@ -142,10 +130,10 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
+
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED)
}
@@ -155,10 +143,9 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
}
@@ -168,7 +155,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ kosmos.setCommunalAvailable(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
@@ -181,8 +168,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
@@ -195,8 +181,7 @@
testScope.runTest {
val tutorialSettingState by
collectLastValue(communalTutorialRepository.tutorialSettingState)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
- communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ goToCommunal()
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
@@ -204,17 +189,8 @@
assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
}
- private suspend fun setCommunalAvailable(available: Boolean) {
- if (available) {
- keyguardRepository.setIsEncryptedOrLockdown(false)
- userRepository.setSelectedUserInfo(MAIN_USER_INFO)
- keyguardRepository.setKeyguardShowing(true)
- } else {
- keyguardRepository.setIsEncryptedOrLockdown(true)
- }
- }
-
- private companion object {
- val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ private suspend fun goToCommunal() {
+ kosmos.setCommunalAvailable(true)
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 352bacc..5ee88cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -17,6 +17,9 @@
package com.android.systemui.communal.view.viewmodel
import android.app.smartspace.SmartspaceTarget
+import android.appwidget.AppWidgetProviderInfo
+import android.content.pm.UserInfo
+import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -39,6 +42,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
@@ -59,6 +63,7 @@
class CommunalEditModeViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var providerInfo: AppWidgetProviderInfo
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -78,6 +83,11 @@
widgetRepository = kosmos.fakeCommunalWidgetRepository
smartspaceRepository = kosmos.fakeSmartspaceRepository
mediaRepository = kosmos.fakeCommunalMediaRepository
+ kosmos.fakeUserTracker.set(
+ userInfos = listOf(MAIN_USER_INFO),
+ selectedUserIndex = 0,
+ )
+ whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
underTest =
CommunalEditModeViewModel(
@@ -100,12 +110,12 @@
CommunalWidgetContentModel(
appWidgetId = 0,
priority = 30,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
CommunalWidgetContentModel(
appWidgetId = 1,
priority = 20,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
)
widgetRepository.setCommunalWidgets(widgets)
@@ -156,12 +166,12 @@
CommunalWidgetContentModel(
appWidgetId = 0,
priority = 30,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
CommunalWidgetContentModel(
appWidgetId = 1,
priority = 20,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
)
widgetRepository.setCommunalWidgets(widgets)
@@ -205,4 +215,8 @@
underTest.onReorderWidgetCancel()
verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
+
+ private companion object {
+ val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index cc322d0..1e523dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -17,7 +17,9 @@
package com.android.systemui.communal.view.viewmodel
import android.app.smartspace.SmartspaceTarget
+import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
+import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -45,13 +47,13 @@
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -71,6 +73,7 @@
class CommunalViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var user: UserInfo
+ @Mock private lateinit var providerInfo: AppWidgetProviderInfo
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -98,6 +101,12 @@
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+ kosmos.fakeUserTracker.set(
+ userInfos = listOf(MAIN_USER_INFO),
+ selectedUserIndex = 0,
+ )
+ whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
+
underTest =
CommunalViewModel(
testScope,
@@ -147,12 +156,12 @@
CommunalWidgetContentModel(
appWidgetId = 0,
priority = 30,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
CommunalWidgetContentModel(
appWidgetId = 1,
priority = 20,
- providerInfo = mock(),
+ providerInfo = providerInfo,
),
)
widgetRepository.setCommunalWidgets(widgets)
@@ -225,4 +234,8 @@
userRepository.setUserInfos(listOf(user))
userRepository.setSelectedUserInfo(user)
}
+
+ private companion object {
+ val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 8488843..2c9d72c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -16,7 +16,9 @@
package com.android.systemui.communal.widgets
+import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
+import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
@@ -32,6 +34,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
@@ -65,7 +68,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
+ kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
@@ -76,6 +79,7 @@
CommunalAppWidgetHostStartable(
appWidgetHost,
kosmos.communalInteractor,
+ kosmos.fakeUserTracker,
kosmos.applicationCoroutineScope,
kosmos.testDispatcher,
)
@@ -170,6 +174,46 @@
}
}
+ @Test
+ fun removeWidgetsForDeletedProfile_whenCommunalIsAvailable() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is available and work profile is configured.
+ setCommunalAvailable(true)
+ kosmos.fakeUserTracker.set(
+ userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK),
+ selectedUserIndex = 0,
+ )
+ val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
+ val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
+ val widgets = listOf(widget1, widget2, widget3)
+ fakeCommunalWidgetRepository.setCommunalWidgets(widgets)
+
+ underTest.start()
+ runCurrent()
+
+ val communalWidgets by
+ collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
+ assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+
+ // Unlock the device and remove work profile.
+ fakeKeyguardRepository.setKeyguardShowing(false)
+ kosmos.fakeUserTracker.set(
+ userInfos = listOf(MAIN_USER_INFO),
+ selectedUserIndex = 0,
+ )
+ runCurrent()
+
+ // Communal becomes available.
+ fakeKeyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ // Widget created for work profile is removed.
+ assertThat(communalWidgets).containsExactly(widget2, widget3)
+ }
+ }
+
private suspend fun setCommunalAvailable(available: Boolean) =
with(kosmos) {
fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
@@ -179,7 +223,16 @@
fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id)
}
+ private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
+ mock<CommunalWidgetContentModel> {
+ whenever(this.appWidgetId).thenReturn(appWidgetId)
+ val providerInfo = mock<AppWidgetProviderInfo>()
+ whenever(providerInfo.profile).thenReturn(UserHandle(userId))
+ whenever(this.providerInfo).thenReturn(providerInfo)
+ }
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
}
}
diff --git a/packages/SystemUI/res/drawable/ic_call.xml b/packages/SystemUI/res/drawable/ic_call.xml
new file mode 100644
index 0000000..859506a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_call.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_filled_arrow_down.xml b/packages/SystemUI/res/drawable/ic_filled_arrow_down.xml
new file mode 100644
index 0000000..c85965f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_filled_arrow_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="24dp"
+ android:height="24dp">
+ <path
+ android:pathData="M7 10l5 5 5 -5z"
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_filled_arrow_up.xml b/packages/SystemUI/res/drawable/ic_filled_arrow_up.xml
new file mode 100644
index 0000000..8ee7e13
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_filled_arrow_up.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="24dp"
+ android:height="24dp">
+ <path
+ android:pathData="M7 14l5-5 5 5z"
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_music_note_off.xml b/packages/SystemUI/res/drawable/ic_music_note_off.xml
new file mode 100644
index 0000000..d583576
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_music_note_off.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M792,904L56,168L112,112L848,848L792,904ZM560,446L480,366L480,120L720,120L720,280L560,280L560,446ZM400,840Q334,840 287,793Q240,746 240,680Q240,614 287,567Q334,520 400,520Q423,520 442.5,525.5Q462,531 480,542L480,480L560,560L560,680Q560,746 513,793Q466,840 400,840Z" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_volume_off.xml b/packages/SystemUI/res/drawable/ic_volume_off.xml
new file mode 100644
index 0000000..209f684
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_volume_off.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M792,904L671,783Q646,799 618,810.5Q590,822 560,829L560,747Q574,742 587.5,737Q601,732 613,725L480,592L480,800L280,600L120,600L120,360L248,360L56,168L112,112L848,848L792,904ZM784,672L726,614Q743,583 751.5,549Q760,515 760,479Q760,385 705,311Q650,237 560,211L560,129Q684,157 762,254.5Q840,352 840,479Q840,532 825.5,581Q811,630 784,672ZM650,538L560,448L560,318Q607,340 633.5,384Q660,428 660,480Q660,495 657.5,509.5Q655,524 650,538ZM480,368L376,264L480,160L480,368ZM400,606L400,512L328,440L328,440L200,440L200,520L314,520L400,606ZM364,476L364,476L364,476L364,476L364,476L364,476L364,476L364,476Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index f1017d8..b438315 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -53,4 +53,7 @@
<item>bottom_start:home</item>
<item>bottom_end:create_note</item>
</string-array>
+
+ <!-- Whether volume panel should use the large screen layout or not -->
+ <bool name="volume_panel_is_large_screen">true</bool>
</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 65c69f7..beaa708 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1010,4 +1010,7 @@
<!-- Whether to use a machine learning model for back gesture falsing. -->
<bool name="config_useBackGestureML">true</bool>
+
+ <!-- Whether volume panel should use the large screen layout or not -->
+ <bool name="volume_panel_is_large_screen">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 53ad344..4263d94 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1513,6 +1513,11 @@
<string name="volume_ringer_status_vibrate">Vibrate</string>
<string name="volume_ringer_status_silent">Mute</string>
+ <!-- Media device casting volume slider label [CHAR_LIMIT=20] -->
+ <string name="media_device_cast">Cast</string>
+ <!-- A message shown when the notification volume changing is disabled because of the muted ring stream [CHAR_LIMIT=40]-->
+ <string name="stream_notification_unavailable">Unavailable because ring is muted</string>
+
<!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on vibrate. [CHAR_LIMIT=NONE] -->
<!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on silent (muted). [CHAR_LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8b2a0ec..05e07a7 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -377,7 +377,9 @@
if (mode == ZenMode.OFF) SysuiR.string::dnd_is_off.name
else SysuiR.string::dnd_is_on.name
).also { data ->
- clock?.run { events.onZenDataChanged(data) }
+ mainExecutor.execute {
+ clock?.run { events.onZenDataChanged(data) }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 476497d..10d1891 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -19,6 +19,7 @@
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.util.TypedValue;
@@ -150,18 +151,22 @@
}
private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) {
- StateListDrawable background = (StateListDrawable) mPasswordEntry.getBackground();
- GradientDrawable stateDrawable = (GradientDrawable) background.getStateDrawable(0);
+ Drawable background = mPasswordEntry.getBackground();
+ if (!(background instanceof StateListDrawable)) return;
+ Drawable stateDrawable = ((StateListDrawable) background).getStateDrawable(0);
+ if (!(stateDrawable instanceof GradientDrawable gradientDrawable)) return;
+
int color = getResources().getColor(R.color.bouncer_password_focus_color);
if (!isAnyKeyboardConnected) {
- stateDrawable.setStroke(0, color);
+ gradientDrawable.setStroke(0, color);
} else {
int strokeWidthInDP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3,
getResources().getDisplayMetrics());
- stateDrawable.setStroke(strokeWidthInDP, color);
+ gradientDrawable.setStroke(strokeWidthInDP, color);
}
}
+
@Override
protected void onViewDetached() {
super.onViewDetached();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 27f9106fd..6299739 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -464,9 +464,6 @@
Bundle fragmentArgs = new Bundle();
fragmentArgs.putStringArray("targets", targets.toArray(new String[0]));
args.putBundle(":settings:show_fragment_args", fragmentArgs);
- // TODO: b/318748373 - The fragment should set its own title using the targets
- args.putString(
- ":settings:show_fragment_title", "Accessibility Shortcut");
intent.replaceExtras(args);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return intent;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 716209d..2c3ebe9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -80,6 +80,7 @@
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
@@ -90,6 +91,7 @@
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -116,6 +118,7 @@
import javax.inject.Inject;
+import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.ExperimentalCoroutinesApi;
/**
@@ -173,6 +176,8 @@
mDefaultUdfpsTouchOverlayViewModel;
@NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
@NonNull private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
+ @NonNull private final PowerInteractor mPowerInteractor;
+ @NonNull private final CoroutineScope mScope;
@NonNull private final InputManager mInputManager;
@NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
@NonNull private final SelectedUserInteractor mSelectedUserInteractor;
@@ -296,7 +301,9 @@
mDeviceEntryUdfpsTouchOverlayViewModel,
mDefaultUdfpsTouchOverlayViewModel,
mShadeInteractor,
- mUdfpsOverlayInteractor
+ mUdfpsOverlayInteractor,
+ mPowerInteractor,
+ mScope
)));
}
@@ -678,7 +685,9 @@
@NonNull KeyguardTransitionInteractor keyguardTransitionInteractor,
Lazy<DeviceEntryUdfpsTouchOverlayViewModel> deviceEntryUdfpsTouchOverlayViewModel,
Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel,
- @NonNull UdfpsOverlayInteractor udfpsOverlayInteractor) {
+ @NonNull UdfpsOverlayInteractor udfpsOverlayInteractor,
+ @NonNull PowerInteractor powerInteractor,
+ @Application CoroutineScope scope) {
mContext = context;
mExecution = execution;
mVibrator = vibrator;
@@ -720,6 +729,8 @@
mShadeInteractor = shadeInteractor;
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUdfpsOverlayInteractor = udfpsOverlayInteractor;
+ mPowerInteractor = powerInteractor;
+ mScope = scope;
mInputManager = inputManager;
mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate;
mSelectedUserInteractor = selectedUserInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index a209eae..921e395 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -44,6 +44,7 @@
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.udfpsViewPerformance
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -53,10 +54,13 @@
import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -67,7 +71,13 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import dagger.Lazy
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
private const val TAG = "UdfpsControllerOverlay"
@@ -82,36 +92,45 @@
@ExperimentalCoroutinesApi
@UiThread
class UdfpsControllerOverlay @JvmOverloads constructor(
- private val context: Context,
- private val inflater: LayoutInflater,
- private val windowManager: WindowManager,
- private val accessibilityManager: AccessibilityManager,
- private val statusBarStateController: StatusBarStateController,
- private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val dialogManager: SystemUIDialogManager,
- private val dumpManager: DumpManager,
- private val transitionController: LockscreenShadeTransitionController,
- private val configurationController: ConfigurationController,
- private val keyguardStateController: KeyguardStateController,
- private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
- private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
- val requestId: Long,
- @RequestReason val requestReason: Int,
- private val controllerCallback: IUdfpsOverlayControllerCallback,
- private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
- private val activityTransitionAnimator: ActivityTransitionAnimator,
- private val primaryBouncerInteractor: PrimaryBouncerInteractor,
- private val alternateBouncerInteractor: AlternateBouncerInteractor,
- private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
- private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
- private val transitionInteractor: KeyguardTransitionInteractor,
- private val selectedUserInteractor: SelectedUserInteractor,
- private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
- private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
- private val shadeInteractor: ShadeInteractor,
- private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
+ private val context: Context,
+ private val inflater: LayoutInflater,
+ private val windowManager: WindowManager,
+ private val accessibilityManager: AccessibilityManager,
+ private val statusBarStateController: StatusBarStateController,
+ private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val dialogManager: SystemUIDialogManager,
+ private val dumpManager: DumpManager,
+ private val transitionController: LockscreenShadeTransitionController,
+ private val configurationController: ConfigurationController,
+ private val keyguardStateController: KeyguardStateController,
+ private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+ private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+ val requestId: Long,
+ @RequestReason val requestReason: Int,
+ private val controllerCallback: IUdfpsOverlayControllerCallback,
+ private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
+ private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+ private val alternateBouncerInteractor: AlternateBouncerInteractor,
+ private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
+ private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
+ private val transitionInteractor: KeyguardTransitionInteractor,
+ private val selectedUserInteractor: SelectedUserInteractor,
+ private val deviceEntryUdfpsTouchOverlayViewModel:
+ Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
+ private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
+ private val shadeInteractor: ShadeInteractor,
+ private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
+ private val powerInteractor: PowerInteractor,
+ @Application private val scope: CoroutineScope,
) {
+ private val isFinishedGoingToSleep: Flow<Unit> =
+ powerInteractor.detailedWakefulness
+ .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP }
+ .map { } // map to Unit
+ private var listenForAsleepJob: Job? = null
+ private var addViewRunnable: Runnable? = null
private var overlayViewLegacy: UdfpsView? = null
private set
private var overlayTouchView: UdfpsTouchOverlay? = null
@@ -192,7 +211,8 @@
if (requestReason.isImportantForAccessibility()) {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
- windowManager.addView(this, coreLayoutParams.updateDimensions(null))
+
+ addViewNowOrLater(this, null)
when (requestReason) {
REASON_AUTH_KEYGUARD ->
UdfpsTouchOverlayBinder.bind(
@@ -225,7 +245,7 @@
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
- windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
+ addViewNowOrLater(this, animation)
sensorRect = sensorBounds
}
}
@@ -257,6 +277,41 @@
return false
}
+ private fun addViewNowOrLater(view: View, animation: UdfpsAnimationViewController<*>?) {
+ if (udfpsViewPerformance()) {
+ addViewRunnable = kotlinx.coroutines.Runnable {
+ windowManager.addView(
+ view,
+ coreLayoutParams.updateDimensions(animation)
+ )
+ }
+ if (powerInteractor.detailedWakefulness.value.internalWakefulnessState
+ != WakefulnessState.STARTING_TO_SLEEP) {
+ addViewIfPending()
+ } else {
+ listenForAsleepJob?.cancel()
+ listenForAsleepJob = scope.launch {
+ isFinishedGoingToSleep.collect {
+ addViewIfPending()
+ }
+ }
+ }
+ } else {
+ windowManager.addView(
+ view,
+ coreLayoutParams.updateDimensions(animation)
+ )
+ }
+ }
+
+ private fun addViewIfPending() {
+ addViewRunnable?.let {
+ listenForAsleepJob?.cancel()
+ it.run()
+ }
+ addViewRunnable = null
+ }
+
fun inflateUdfpsAnimation(
view: UdfpsView,
controller: UdfpsController
@@ -368,6 +423,7 @@
overlayViewLegacy = null
overlayTouchView = null
overlayTouchListener = null
+ listenForAsleepJob?.cancel()
return wasShowing
}
@@ -412,7 +468,8 @@
if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
if (!shouldRotate(animation)) {
Log.v(
- TAG, "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
+ TAG,
+ "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
" animation=$animation" +
" isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
" isOccluded=${keyguardStateController.isOccluded}"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index 0c0ed77..435df5f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -51,6 +51,8 @@
* There is never more than one instance of the FingerprintProperty at any given time.
*/
interface FingerprintPropertyRepository {
+ /** Whether the fingerprint properties have been initialized yet. */
+ val propertiesInitialized: StateFlow<Boolean>
/** The id of fingerprint sensor. */
val sensorId: Flow<Int>
@@ -105,7 +107,16 @@
.stateIn(
applicationScope,
started = SharingStarted.Eagerly,
- initialValue = DEFAULT_PROPS,
+ initialValue = UNINITIALIZED_PROPS,
+ )
+
+ override val propertiesInitialized: StateFlow<Boolean> =
+ props
+ .map { it != UNINITIALIZED_PROPS }
+ .stateIn(
+ applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = props.value != UNINITIALIZED_PROPS,
)
override val sensorId: Flow<Int> = props.map { it.sensorId }
@@ -124,6 +135,17 @@
companion object {
private const val TAG = "FingerprintPropertyRepositoryImpl"
+ private val UNINITIALIZED_PROPS =
+ FingerprintSensorPropertiesInternal(
+ -2 /* sensorId */,
+ SensorProperties.STRENGTH_CONVENIENCE,
+ 0 /* maxEnrollmentsPerUser */,
+ listOf<ComponentInfoInternal>(),
+ FingerprintSensorProperties.TYPE_UNKNOWN,
+ false /* halControlsIllumination */,
+ true /* resetLockoutRequiresHardwareAuthToken */,
+ listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
+ )
private val DEFAULT_PROPS =
FingerprintSensorPropertiesInternal(
-1 /* sensorId */,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
index ff9cdbd..8bc0c54 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
@@ -25,6 +25,7 @@
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
@@ -39,6 +40,7 @@
configurationInteractor: ConfigurationInteractor,
displayStateInteractor: DisplayStateInteractor,
) {
+ val propertiesInitialized: StateFlow<Boolean> = repository.propertiesInitialized
val isUdfps: Flow<Boolean> = repository.sensorType.map { it.isUdfps() }
/**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index b0cc3bd..cd5b124 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -438,9 +438,20 @@
// Play haptics
launch {
- viewModel.hapticsToPlay.collect { hapticFeedbackConstant ->
- if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
- vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant)
+ viewModel.hapticsToPlay.collect { haptics ->
+ if (haptics.hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
+ if (haptics.flag != null) {
+ vibratorHelper.performHapticFeedback(
+ view,
+ haptics.hapticFeedbackConstant,
+ haptics.flag,
+ )
+ } else {
+ vibratorHelper.performHapticFeedback(
+ view,
+ haptics.hapticFeedbackConstant,
+ )
+ }
viewModel.clearHaptics()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 788991d..c933e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -53,6 +53,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** ViewModel for BiometricPrompt. */
@@ -144,9 +145,10 @@
private val _forceLargeSize = MutableStateFlow(false)
private val _forceMediumSize = MutableStateFlow(false)
- private val _hapticsToPlay = MutableStateFlow(HapticFeedbackConstants.NO_HAPTICS)
+ private val _hapticsToPlay =
+ MutableStateFlow(HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, /* flag= */ null))
- /** Event fired to the view indicating a [HapticFeedbackConstants] to be played */
+ /** Event fired to the view indicating a [HapticsToPlay] */
val hapticsToPlay = _hapticsToPlay.asStateFlow()
/** The current position of the prompt */
@@ -686,16 +688,26 @@
}
private fun vibrateOnSuccess() {
- _hapticsToPlay.value = HapticFeedbackConstants.CONFIRM
+ _hapticsToPlay.value =
+ HapticsToPlay(
+ HapticFeedbackConstants.CONFIRM,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
+ )
}
private fun vibrateOnError() {
- _hapticsToPlay.value = HapticFeedbackConstants.REJECT
+ _hapticsToPlay.value =
+ HapticsToPlay(
+ HapticFeedbackConstants.REJECT,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
+ )
}
- /** Clears the [hapticsToPlay] variable by setting it to the NO_HAPTICS default. */
+ /** Clears the [hapticsToPlay] variable by setting its constant to the NO_HAPTICS default. */
fun clearHaptics() {
- _hapticsToPlay.value = HapticFeedbackConstants.NO_HAPTICS
+ _hapticsToPlay.update { previous ->
+ HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, previous.flag)
+ }
}
companion object {
@@ -724,3 +736,9 @@
val isStarted: Boolean
get() = this == Normal || this == Delayed
}
+
+/**
+ * The state of haptic feedback to play. It is composed by a [HapticFeedbackConstants] and a
+ * [HapticFeedbackConstants] flag.
+ */
+data class HapticsToPlay(val hapticFeedbackConstant: Int, val flag: Int?)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
index 00bbb20..6af0fa0 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
@@ -40,6 +40,7 @@
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.MediaOutputConstants;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.media.controls.util.MediaDataUtils;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.res.R;
@@ -74,7 +75,7 @@
private final SystemUIDialog.Factory mSystemUIDialogFactory;
private final String mCurrentBroadcastApp;
private final String mOutputPackageName;
- private final Executor mExecutor;
+ private final Executor mBgExecutor;
private boolean mShouldLaunchLeBroadcastDialog;
private Button mSwitchBroadcast;
@@ -159,7 +160,7 @@
MediaOutputDialogFactory mediaOutputDialogFactory,
@Nullable LocalBluetoothManager localBluetoothManager,
UiEventLogger uiEventLogger,
- Executor executor,
+ @Background Executor bgExecutor,
BroadcastSender broadcastSender,
SystemUIDialog.Factory systemUIDialogFactory,
@Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp,
@@ -171,7 +172,7 @@
mCurrentBroadcastApp = currentBroadcastApp;
mOutputPackageName = outputPkgName;
mUiEventLogger = uiEventLogger;
- mExecutor = executor;
+ mBgExecutor = bgExecutor;
mBroadcastSender = broadcastSender;
if (DEBUG) {
@@ -187,7 +188,7 @@
@Override
public void onStart(SystemUIDialog dialog) {
mDialogs.add(dialog);
- registerBroadcastCallBack(mExecutor, mBroadcastCallback);
+ registerBroadcastCallBack(mBgExecutor, mBroadcastCallback);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index 2af49cf..b269967 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -143,7 +143,7 @@
mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
int availableHeight = mTextPreview.getHeight()
- (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
- mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
+ mTextPreview.setMaxLines(Math.max(availableHeight / mTextPreview.getLineHeight(), 1));
return true;
});
super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index d0044a4..5d52541 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -29,6 +29,7 @@
import com.android.systemui.communal.shared.model.CommunalContentSize.HALF
import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD
import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
@@ -45,6 +46,7 @@
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.settings.UserTracker
import com.android.systemui.smartspace.data.repository.SmartspaceRepository
import com.android.systemui.util.kotlin.BooleanFlowOperators.and
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
@@ -59,6 +61,7 @@
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@@ -82,6 +85,7 @@
communalSettingsInteractor: CommunalSettingsInteractor,
private val appWidgetHost: CommunalAppWidgetHost,
private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
+ private val userTracker: UserTracker,
sceneInteractor: SceneInteractor,
sceneContainerFlags: SceneContainerFlags,
@CommunalLog logBuffer: LogBuffer,
@@ -125,8 +129,13 @@
/**
* Target scene as requested by the underlying [SceneTransitionLayout] or through
* [onSceneChanged].
+ *
+ * If [isCommunalAvailable] is false, will return [CommunalSceneKey.Blank]
*/
- val desiredScene: StateFlow<CommunalSceneKey> = communalRepository.desiredScene
+ val desiredScene: Flow<CommunalSceneKey> =
+ communalRepository.desiredScene.combine(isCommunalAvailable) { scene, available ->
+ if (available) scene else CommunalSceneKey.Blank
+ }
/** Transition state of the hub mode. */
val transitionState: StateFlow<ObservableCommunalTransitionState> =
@@ -262,10 +271,16 @@
fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) =
widgetRepository.updateWidgetOrder(widgetIdToPriorityMap)
+ /** All widgets present in db. */
+ val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
+ isCommunalAvailable.flatMapLatest { available ->
+ if (!available) emptyFlow() else widgetRepository.communalWidgets
+ }
+
/** A list of widget content to be displayed in the communal hub. */
val widgetContent: Flow<List<CommunalContentModel.Widget>> =
widgetRepository.communalWidgets.map { widgets ->
- widgets.map Widget@{ widget ->
+ filterWidgetsByExistingUsers(widgets).map Widget@{ widget ->
return@Widget CommunalContentModel.Widget(
appWidgetId = widget.appWidgetId,
providerInfo = widget.providerInfo,
@@ -345,6 +360,19 @@
return@combine ongoingContent
}
+ /**
+ * Filter and retain widgets associated with an existing user, safeguarding against displaying
+ * stale data following user deletion.
+ */
+ private fun filterWidgetsByExistingUsers(
+ list: List<CommunalWidgetContentModel>,
+ ): List<CommunalWidgetContentModel> {
+ val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
+ return list.filter { widget ->
+ currentUserIds.contains(widget.providerInfo.profile?.identifier)
+ }
+ }
+
companion object {
/**
* The user activity timeout which should be used when the communal hub is opened. A value
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 8a7b5eb..3ec9a26 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -34,7 +34,7 @@
private val communalInteractor: CommunalInteractor,
val mediaHost: MediaHost,
) {
- val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
+ val currentScene: Flow<CommunalSceneKey> = communalInteractor.desiredScene
/** Whether widgets are currently being re-ordered. */
open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 4ddd768..8390d62 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -18,11 +18,14 @@
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -37,6 +40,7 @@
constructor(
private val appWidgetHost: CommunalAppWidgetHost,
private val communalInteractor: CommunalInteractor,
+ private val userTracker: UserTracker,
@Background private val bgScope: CoroutineScope,
@Main private val uiDispatcher: CoroutineDispatcher
) : CoreStartable {
@@ -47,6 +51,14 @@
.pairwise(false)
.filter { (previous, new) -> previous != new }
.onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) }
+ .sample(communalInteractor.communalWidgets, ::Pair)
+ .onEach { (withPrev, widgets) ->
+ val (_, isActive) = withPrev
+ // The validation is performed once the hub becomes active.
+ if (isActive) {
+ validateWidgetsAndDeleteOrphaned(widgets)
+ }
+ }
.launchIn(bgScope)
appWidgetHost.appWidgetIdToRemove
@@ -63,4 +75,15 @@
appWidgetHost.stopListening()
}
}
+
+ /**
+ * Ensure the existence of all associated users for widgets, and remove widgets belonging to
+ * users who have been deleted.
+ */
+ private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) {
+ val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
+ widgets
+ .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) }
+ .onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
index 0f038e1..bc07b95 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
@@ -36,6 +36,7 @@
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.settings.ControlsSettingsRepository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.VibratorHelper
@@ -47,16 +48,16 @@
@SysUISingleton
class ControlActionCoordinatorImpl @Inject constructor(
- private val context: Context,
- private val bgExecutor: DelayableExecutor,
- @Main private val uiExecutor: DelayableExecutor,
- private val activityStarter: ActivityStarter,
- private val broadcastSender: BroadcastSender,
- private val keyguardStateController: KeyguardStateController,
- private val taskViewFactory: Optional<TaskViewFactory>,
- private val controlsMetricsLogger: ControlsMetricsLogger,
- private val vibrator: VibratorHelper,
- private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val context: Context,
+ @Background private val bgExecutor: DelayableExecutor,
+ @Main private val uiExecutor: DelayableExecutor,
+ private val activityStarter: ActivityStarter,
+ private val broadcastSender: BroadcastSender,
+ private val keyguardStateController: KeyguardStateController,
+ private val taskViewFactory: Optional<TaskViewFactory>,
+ private val controlsMetricsLogger: ControlsMetricsLogger,
+ private val vibrator: VibratorHelper,
+ private val controlsSettingsRepository: ControlsSettingsRepository,
) : ControlActionCoordinator {
private var dialog: Dialog? = null
private var pendingAction: Action? = null
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index e893177..1157d97 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -126,6 +126,7 @@
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
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;
@@ -358,6 +359,7 @@
VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
+ SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
CommonNotifCollection notifCollection,
NotifPipeline notifPipeline,
SysUiState sysUiState,
@@ -376,6 +378,7 @@
visualInterruptionDecisionProvider,
zenModeController,
notifUserManager,
+ sensitiveNotificationProtectionController,
notifCollection,
notifPipeline,
sysUiState,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index e35c5a6..301942f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -170,7 +170,6 @@
KeyguardIndicationAreaBinder.bind(
notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area),
keyguardIndicationAreaViewModel,
- aodAlphaViewModel,
indicationController,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 56d64a2..bc3f0cc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -15,12 +15,15 @@
*
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.keyguard.domain.interactor
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
+import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
@@ -29,7 +32,9 @@
import com.android.systemui.statusbar.policy.SplitShadeStateController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
@@ -41,6 +46,7 @@
@Application private val applicationScope: CoroutineScope,
private val context: Context,
private val splitShadeStateController: SplitShadeStateController,
+ private val clockInteractor: KeyguardClockInteractor,
) {
/** The current blueprint for the lockscreen. */
@@ -58,6 +64,7 @@
.onStart { emit(Unit) }
.collect { updateBlueprint() }
}
+ applicationScope.launch { clockInteractor.currentClock.collect { updateBlueprint() } }
}
/**
@@ -67,12 +74,17 @@
private fun updateBlueprint() {
val useSplitShade =
splitShadeStateController.shouldUseSplitNotificationShade(context.resources)
+ // TODO(b/326098079): Make ID a constant value.
+ val useWeatherClockLayout =
+ clockInteractor.currentClock.value?.config?.id == "DIGITAL_CLOCK_WEATHER" &&
+ ComposeLockscreen.isEnabled
val blueprintId =
- if (useSplitShade) {
- SplitShadeKeyguardBlueprint.ID
- } else {
- DefaultKeyguardBlueprint.DEFAULT
+ when {
+ useWeatherClockLayout && useSplitShade -> SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+ useWeatherClockLayout -> WEATHER_CLOCK_BLUEPRINT_ID
+ useSplitShade -> SplitShadeKeyguardBlueprint.ID
+ else -> DefaultKeyguardBlueprint.DEFAULT
}
transitionToBlueprint(blueprintId)
@@ -107,4 +119,13 @@
fun getCurrentBlueprint(): KeyguardBlueprint {
return keyguardBlueprintRepository.blueprint.value
}
+
+ companion object {
+ /**
+ * These values live here because classes in the composable package do not exist in some
+ * systems.
+ */
+ const val WEATHER_CLOCK_BLUEPRINT_ID = "weather-clock"
+ const val SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID = "split-shade-weather-clock"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 873cc84..d0821c9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -22,11 +22,14 @@
import android.util.StateSet
import android.view.HapticFeedbackConstants
import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.common.ui.view.LongPressHandlingView
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
@@ -51,6 +54,8 @@
@JvmStatic
fun bind(
applicationScope: CoroutineScope,
+ keyguardRootView: ConstraintLayout,
+ section: DefaultDeviceEntrySection,
view: DeviceEntryIconView,
viewModel: DeviceEntryIconViewModel,
fgViewModel: DeviceEntryForegroundViewModel,
@@ -76,6 +81,23 @@
}
}
+ keyguardRootView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.positionAtUdfpsLocation.collect { supportsUdfps ->
+ section.removeViews(keyguardRootView)
+ keyguardRootView.addView(view)
+ val constraintSet = ConstraintSet().apply { clone(keyguardRootView) }
+ section.applyConstraintsAfterPropertiesInitialized(
+ constraintSet,
+ supportsUdfps,
+ )
+ constraintSet.applyTo(keyguardRootView)
+ }
+ }
+ }
+ }
+
view.repeatWhenAttached {
// Repeat on CREATED so that the view will always observe the entire
// GONE => AOD transition (even though the view may not be visible until the middle
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 7c1368a..841f52d 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
@@ -23,7 +23,7 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
+import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
@@ -51,7 +51,6 @@
fun bind(
view: ViewGroup,
viewModel: KeyguardIndicationAreaViewModel,
- aodAlphaViewModel: AodAlphaViewModel,
indicationController: KeyguardIndicationController,
): DisposableHandle {
indicationController.setIndicationArea(view)
@@ -68,30 +67,10 @@
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
- if (keyguardBottomAreaRefactor()) {
- aodAlphaViewModel.alpha.collect { alpha ->
- view.apply {
- this.importantForAccessibility =
- if (alpha == 0f) {
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- } else {
- View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
- }
- this.alpha = alpha
- }
- }
- } else {
- viewModel.alpha.collect { alpha ->
- view.apply {
- this.importantForAccessibility =
- if (alpha == 0f) {
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- } else {
- View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
- }
- this.alpha = alpha
- }
- }
+ // Do not independently apply alpha, as [KeyguardRootViewModel] should work
+ // for this and all its children
+ if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+ viewModel.alpha.collect { alpha -> view.alpha = alpha }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt
index 78099d9..a53c6d7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt
@@ -50,6 +50,15 @@
)
}
+ override fun setAlpha(alpha: Float) {
+ super.setAlpha(alpha)
+
+ if (alpha == 0f) {
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ } else {
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+ }
private fun indicationTopRow(): KeyguardIndicationTextView {
return KeyguardIndicationTextView(context, attrs).apply {
id = R.id.keyguard_indication_text
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
index 4f1a754..b4e57cc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
@@ -17,10 +17,13 @@
package com.android.systemui.keyguard.ui.view.layout.blueprints
-import com.android.systemui.communal.ui.view.layout.blueprints.DefaultCommunalBlueprint
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
import dagger.Binds
import dagger.Module
+import dagger.Provides
import dagger.multibindings.IntoSet
@Module
@@ -43,9 +46,25 @@
shortcutsBesideUdfpsLockscreenBlueprint: ShortcutsBesideUdfpsKeyguardBlueprint
): KeyguardBlueprint
- @Binds
- @IntoSet
- abstract fun bindDefaultCommunalBlueprint(
- defaultCommunalBlueprint: DefaultCommunalBlueprint
- ): KeyguardBlueprint
+ companion object {
+ /** This is a place holder for weather clock in compose. */
+ @Provides
+ @IntoSet
+ fun bindWeatherClockBlueprintPlaceHolder(): KeyguardBlueprint {
+ return object : KeyguardBlueprint {
+ override val id: String = WEATHER_CLOCK_BLUEPRINT_ID
+ override val sections: List<KeyguardSection> = listOf()
+ }
+ }
+
+ /** This is a place holder for weather clock in compose. */
+ @Provides
+ @IntoSet
+ fun bindSplitShadeWeatherClockBlueprintPlaceHolder(): KeyguardBlueprint {
+ return object : KeyguardBlueprint {
+ override val id: String = SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+ override val sections: List<KeyguardSection> = listOf()
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 3fc9b42..d70ecce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -26,7 +26,6 @@
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.LockIconView
import com.android.keyguard.LockIconViewController
import com.android.systemui.Flags.keyguardBottomAreaRefactor
@@ -56,7 +55,6 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val authController: AuthController,
private val windowManager: WindowManager,
private val context: Context,
@@ -69,6 +67,7 @@
private val falsingManager: Lazy<FalsingManager>,
private val vibratorHelper: Lazy<VibratorHelper>,
) : KeyguardSection() {
+ private var deviceEntryIconView: DeviceEntryIconView? = null
private val deviceEntryIconViewId = R.id.device_entry_icon_view
override fun addViews(constraintLayout: ConstraintLayout) {
@@ -80,21 +79,22 @@
notificationPanelView.removeView(it)
}
- val view =
- if (DeviceEntryUdfpsRefactor.isEnabled) {
+ if (DeviceEntryUdfpsRefactor.isEnabled) {
+ deviceEntryIconView =
DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
- } else {
- // keyguardBottomAreaRefactor()
- LockIconView(context, null).apply { id = R.id.lock_icon_view }
- }
- constraintLayout.addView(view)
+ } else {
+ // keyguardBottomAreaRefactor()
+ constraintLayout.addView(LockIconView(context, null).apply { id = R.id.lock_icon_view })
+ }
}
override fun bindData(constraintLayout: ConstraintLayout) {
if (DeviceEntryUdfpsRefactor.isEnabled) {
- constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let {
+ deviceEntryIconView?.let {
DeviceEntryIconViewBinder.bind(
applicationScope,
+ constraintLayout,
+ this,
it,
deviceEntryIconViewModel.get(),
deviceEntryForegroundViewModel.get(),
@@ -111,7 +111,22 @@
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- val isUdfpsSupported = keyguardUpdateMonitor.isUdfpsSupported
+ if (!DeviceEntryUdfpsRefactor.isEnabled) {
+ applyConstraints(constraintSet, authController.isUdfpsSupported)
+ }
+ }
+
+ fun applyConstraintsAfterPropertiesInitialized(
+ constraintSet: ConstraintSet,
+ isUdfpsSupported: Boolean
+ ) {
+ if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
+ return
+ }
+ applyConstraints(constraintSet, isUdfpsSupported)
+ }
+
+ private fun applyConstraints(constraintSet: ConstraintSet, isUdfpsSupported: Boolean) {
val scaleFactor: Float = authController.scaleFactor
val mBottomPaddingPx =
context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index ea05c1d..3361343 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -25,7 +25,6 @@
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
-import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -37,7 +36,6 @@
constructor(
private val context: Context,
private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val aodAlphaViewModel: AodAlphaViewModel,
private val indicationController: KeyguardIndicationController,
) : KeyguardSection() {
private val indicationAreaViewId = R.id.keyguard_indication_area
@@ -56,7 +54,6 @@
KeyguardIndicationAreaBinder.bind(
constraintLayout.requireViewById(R.id.keyguard_indication_area),
keyguardIndicationAreaViewModel,
- aodAlphaViewModel,
indicationController,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index c9cf0c3..dc327ff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -19,6 +19,7 @@
import android.animation.FloatEvaluator
import android.animation.IntEvaluator
import com.android.keyguard.KeyguardViewController
+import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
@@ -34,11 +35,10 @@
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -60,6 +60,7 @@
private val keyguardViewController: Lazy<KeyguardViewController>,
private val deviceEntryInteractor: DeviceEntryInteractor,
private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+ private val fingerprintPropertyInteractor: FingerprintPropertyInteractor,
) {
private val intEvaluator = IntEvaluator()
private val floatEvaluator = FloatEvaluator()
@@ -178,21 +179,16 @@
}
}
- private val isUnlocked: Flow<Boolean> =
- deviceEntryInteractor.isUnlocked.flatMapLatest { isUnlocked ->
- if (!isUnlocked) {
- flowOf(false)
+ /** Whether the device entry icon should be positioned at the location of UDFPS. */
+ val positionAtUdfpsLocation: Flow<Boolean> =
+ fingerprintPropertyInteractor.propertiesInitialized.flatMapLatest { initialized ->
+ if (initialized) {
+ fingerprintPropertyInteractor.isUdfps
} else {
- flow {
- // delay in case device ends up transitioning away from the lock screen;
- // we don't want to animate to the unlocked icon and just let the
- // icon fade with the transition to GONE
- delay(UNLOCKED_DELAY_MS)
- emit(true)
- }
+ // Don't update the position of the icon until properties are initialized.
+ emptyFlow()
}
}
-
val iconType: Flow<DeviceEntryIconView.IconType> =
combine(
deviceEntryUdfpsInteractor.isListeningForUdfps,
@@ -245,10 +241,6 @@
DeviceEntryIconView.AccessibilityHintType.NONE
}
}
-
- companion object {
- const val UNLOCKED_DELAY_MS = 50L
- }
}
data class BurnInOffsets(
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index cc3729b..8b7c85b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -119,6 +119,16 @@
return factory.create("LSShadeTransitionLog", 50);
}
+ /** */
+ @Provides
+ @SysUISingleton
+ @SensitiveNotificationProtectionLog
+ public static LogBuffer provideSensitiveNotificationProtectionLogBuffer(
+ LogBufferFactory factory
+ ) {
+ return factory.create("SensitiveNotificationProtectionLog", 10);
+ }
+
/** Provides a logging buffer for shade window messages. */
@Provides
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SensitiveNotificationProtectionLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/SensitiveNotificationProtectionLog.kt
new file mode 100644
index 0000000..54e87cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SensitiveNotificationProtectionLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for SensitiveNotificationProtection. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class SensitiveNotificationProtectionLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
index 9206af2..ba7d410 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
@@ -300,10 +300,17 @@
private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
val currentMediaContainer = view ?: return
- val previousVisibility = currentMediaContainer.visibility
- currentMediaContainer.visibility = newVisibility
- if (previousVisibility != newVisibility && currentMediaContainer is MediaContainerView) {
- visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
+ val isVisible = newVisibility == View.VISIBLE
+
+ if (currentMediaContainer is MediaContainerView) {
+ val previousVisibility = currentMediaContainer.visibility
+
+ currentMediaContainer.setKeyguardVisibility(isVisible)
+ if (previousVisibility != newVisibility) {
+ visibilityChangedListener?.invoke(isVisible)
+ }
+ } else {
+ currentMediaContainer.visibility = newVisibility
}
}
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 e8ad4d3..4e940f1 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
@@ -260,7 +260,6 @@
private TurbulenceNoiseController mTurbulenceNoiseController;
private LoadingEffect mLoadingEffect;
private final GlobalSettings mGlobalSettings;
- private final Random mRandom = new Random();
private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig;
private boolean mWasPlaying = false;
private boolean mButtonClicked = false;
@@ -1294,13 +1293,14 @@
mMediaViewHolder.getTurbulenceNoiseView();
int width = targetView.getWidth();
int height = targetView.getHeight();
+ Random random = new Random();
return new TurbulenceNoiseAnimationConfig(
/* gridCount= */ 2.14f,
TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
- /* noiseOffsetX= */ mRandom.nextFloat(),
- /* noiseOffsetY= */ mRandom.nextFloat(),
- /* noiseOffsetZ= */ mRandom.nextFloat(),
+ /* noiseOffsetX= */ random.nextFloat(),
+ /* noiseOffsetY= */ random.nextFloat(),
+ /* noiseOffsetZ= */ random.nextFloat(),
/* noiseMoveSpeedX= */ 0.42f,
/* noiseMoveSpeedY= */ 0f,
TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
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 3b989d9..dbd71f3 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
@@ -58,6 +58,7 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -583,12 +584,14 @@
UserHandle.USER_ALL
)
- // Listen to the communal UI state.
+ // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is
+ // available, ie. not disabled and able to be shown.
coroutineScope.launch {
- communalInteractor.isCommunalShowing.collect { value ->
- isCommunalShowing = value
- updateDesiredLocation(forceNoAnimation = true)
- }
+ and(communalInteractor.isCommunalShowing, communalInteractor.isCommunalAvailable)
+ .collect { value ->
+ isCommunalShowing = value
+ updateDesiredLocation()
+ }
}
}
@@ -1150,12 +1153,16 @@
when {
mediaFlags.isSceneContainerEnabled() -> desiredLocation
dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
+
+ // UMO should show in communal unless the shade is expanding or visible.
+ isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB
(qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
- // TODO(b/311234666): revisit logic once interactions between the hub and
- // shade/keyguard state are finalized
+
+ // Communal does not have its own StatusBarState so it should always have higher
+ // priority for the UMO over the lockscreen.
isCommunalShowing -> LOCATION_COMMUNAL_HUB
onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
else -> LOCATION_QQS
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 416eae1..4f062af 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -72,7 +72,7 @@
context: Context,
logger: MediaTttReceiverLogger,
windowManager: WindowManager,
- mainExecutor: DelayableExecutor,
+ @Main mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
dumpManager: DumpManager,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index bee3152..fb5339d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -31,10 +31,13 @@
import android.view.WindowManagerGlobal
import com.android.app.tracing.coroutines.launch
import com.android.internal.infra.ServiceConnector
+import com.android.systemui.Flags.screenshotActionDismissSystemWindows
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.statusbar.phone.CentralSurfaces
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
@@ -46,9 +49,11 @@
@Inject
constructor(
private val context: Context,
+ private val activityManagerWrapper: ActivityManagerWrapper,
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val displayTracker: DisplayTracker,
+ private val keyguardController: ScreenshotKeyguardController,
) {
/**
* Execute the given intent with startActivity while performing operations for screenshot action
@@ -74,7 +79,14 @@
user: UserHandle,
overrideTransition: Boolean,
) {
- dismissKeyguard()
+ if (screenshotActionDismissSystemWindows()) {
+ keyguardController.dismiss()
+ activityManagerWrapper.closeSystemWindows(
+ CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT
+ )
+ } else {
+ dismissKeyguard()
+ }
if (user == myUserHandle()) {
withContext(mainDispatcher) { context.startActivity(intent, options) }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt
new file mode 100644
index 0000000..7696bbe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.screenshot
+
+import android.content.Context
+import android.content.Intent
+import com.android.internal.infra.ServiceConnector
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+
+open class ScreenshotKeyguardController @Inject constructor(context: Context) {
+ private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+ ServiceConnector.Impl(
+ context,
+ Intent(context, ScreenshotProxyService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ context.userId,
+ IScreenshotProxy.Stub::asInterface
+ )
+
+ suspend fun dismiss() {
+ val completion = CompletableDeferred<Unit>()
+ val onDoneBinder =
+ object : IOnDoneCallback.Stub() {
+ override fun onDone(success: Boolean) {
+ completion.complete(Unit)
+ }
+ }
+ proxyConnector.post { it.dismissKeyguard(onDoneBinder) }
+ completion.await()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index 86f6523..d5ab306 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -17,15 +17,16 @@
import android.content.Intent
import android.os.IBinder
+import android.os.RemoteException
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launch
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shade.ShadeExpansionStateManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import com.android.app.tracing.coroutines.launch
import kotlinx.coroutines.withContext
/** Provides state from the main SystemUI process on behalf of the Screenshot process. */
@@ -56,7 +57,13 @@
private suspend fun executeAfterDismissing(callback: IOnDoneCallback) =
withContext(mMainDispatcher) {
activityStarter.executeRunnableDismissingKeyguard(
- Runnable { callback.onDone(true) },
+ {
+ try {
+ callback.onDone(true)
+ } catch (e: RemoteException) {
+ Log.w(TAG, "Failed to complete callback transaction", e)
+ }
+ },
null,
true /* dismissShade */,
true /* afterKeyguardGone */,
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index f2fa0ef..125f7fc 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -121,7 +121,6 @@
@GuardedBy("callbacks")
private val callbacks: MutableList<DataItem> = ArrayList()
- private var beforeUserSwitchingJob: Job? = null
private var userSwitchingJob: Job? = null
private var afterUserSwitchingJob: Job? = null
@@ -194,14 +193,7 @@
private fun registerUserSwitchObserver() {
iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() {
override fun onBeforeUserSwitching(newUserId: Int) {
- if (isBackgroundUserSwitchEnabled) {
- beforeUserSwitchingJob?.cancel()
- beforeUserSwitchingJob = appScope.launch(backgroundContext) {
- handleBeforeUserSwitching(newUserId)
- }
- } else {
- handleBeforeUserSwitching(newUserId)
- }
+ handleBeforeUserSwitching(newUserId)
}
override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
@@ -233,15 +225,24 @@
@WorkerThread
protected open fun handleBeforeUserSwitching(newUserId: Int) {
- Assert.isNotMainThread()
setUserIdInternal(newUserId)
val list = synchronized(callbacks) {
callbacks.toList()
}
+ val latch = CountDownLatch(list.size)
list.forEach {
- it.callback.get()?.onBeforeUserSwitching(newUserId)
+ val callback = it.callback.get()
+ if (callback != null) {
+ it.executor.execute {
+ callback.onBeforeUserSwitching(newUserId)
+ latch.countDown()
+ }
+ } else {
+ latch.countDown()
+ }
}
+ latch.await()
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ColorUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ColorUpdateLogger.kt
index c416d43..0f0ab2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ColorUpdateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ColorUpdateLogger.kt
@@ -108,7 +108,7 @@
fun trim() {
if (events.size > maxEventsPerFrame) {
- events.removeFirst()
+ events.removeAt(0)
trimmedEvents++
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index bd659d2..b9d1dde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -53,6 +53,7 @@
mediaCoordinator: MediaCoordinator,
preparationCoordinator: PreparationCoordinator,
remoteInputCoordinator: RemoteInputCoordinator,
+ rowAlertTimeCoordinator: RowAlertTimeCoordinator,
rowAppearanceCoordinator: RowAppearanceCoordinator,
stackCoordinator: StackCoordinator,
shadeEventCoordinator: ShadeEventCoordinator,
@@ -69,9 +70,7 @@
private val mCoordinators: MutableList<Coordinator> = ArrayList()
private val mOrderedSections: MutableList<NotifSectioner> = ArrayList()
- /**
- * Creates all the coordinators.
- */
+ /** Creates all the coordinators. */
init {
// Attach core coordinators.
mCoreCoordinators.add(dataStoreCoordinator)
@@ -89,6 +88,7 @@
mCoordinators.add(groupCountCoordinator)
mCoordinators.add(groupWhenCoordinator)
mCoordinators.add(mediaCoordinator)
+ mCoordinators.add(rowAlertTimeCoordinator)
mCoordinators.add(rowAppearanceCoordinator)
mCoordinators.add(stackCoordinator)
mCoordinators.add(shadeEventCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt
new file mode 100644
index 0000000..12de339
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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.statusbar.notification.collection.coordinator
+
+import android.util.ArrayMap
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.render.NotifRowController
+import javax.inject.Inject
+import kotlin.math.max
+
+/**
+ * A small coordinator which ensures the "alerted" bell shows not just for recently alerted entries,
+ * but also on the summary for every such entry.
+ */
+@CoordinatorScope
+class RowAlertTimeCoordinator @Inject constructor() : Coordinator {
+
+ private val latestAlertTimeBySummary = ArrayMap<NotificationEntry, Long>()
+
+ override fun attach(pipeline: NotifPipeline) {
+ pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilterListener)
+ pipeline.addOnAfterRenderEntryListener(::onAfterRenderEntry)
+ }
+
+ private fun onBeforeFinalizeFilterListener(entries: List<ListEntry>) {
+ latestAlertTimeBySummary.clear()
+ entries.asSequence().filterIsInstance<GroupEntry>().forEach { groupEntry ->
+ val summary = checkNotNull(groupEntry.summary)
+ latestAlertTimeBySummary[summary] = groupEntry.calculateLatestAlertTime()
+ }
+ }
+
+ private fun onAfterRenderEntry(entry: NotificationEntry, controller: NotifRowController) {
+ // Show the "alerted" bell icon based on the latest group member for summaries
+ val lastAudiblyAlerted = latestAlertTimeBySummary[entry] ?: entry.lastAudiblyAlertedMs
+ controller.setLastAudibleMs(lastAudiblyAlerted)
+ }
+
+ private fun GroupEntry.calculateLatestAlertTime(): Long {
+ val lastChildAlertedTime = children.maxOf { it.lastAudiblyAlertedMs }
+ val summaryAlertedTime = checkNotNull(summary).lastAudiblyAlertedMs
+ return max(lastChildAlertedTime, summaryAlertedTime)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
index f2b8482..df694bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
@@ -75,7 +75,5 @@
(mAutoExpandFirstNotification && entry == entryToExpand))
// Show/hide the feedback icon
controller.setFeedbackIcon(mAssistantFeedbackController.getFeedbackIcon(entry))
- // Show the "alerted" bell icon
- controller.setLastAudibleMs(entry.lastAudiblyAlertedMs)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 8189fe0..dfe6cd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -25,6 +25,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -92,7 +93,7 @@
@Inject
public VisualStabilityCoordinator(
- DelayableExecutor delayableExecutor,
+ @Background DelayableExecutor delayableExecutor,
DumpManager dumpManager,
HeadsUpManager headsUpManager,
ShadeAnimationInteractor shadeAnimationInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 76e5fd3..a5f42bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -45,10 +45,12 @@
* icons and keeping the icon assets themselves up to date as notifications change.
*
* TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry.
- * Long-term, it should probably live somewhere in the content inflation pipeline.
+ * Long-term, it should probably live somewhere in the content inflation pipeline.
*/
@SysUISingleton
-class IconManager @Inject constructor(
+class IconManager
+@Inject
+constructor(
private val notifCollection: CommonNotifCollection,
private val launcherApps: LauncherApps,
private val iconBuilder: IconBuilder
@@ -59,30 +61,30 @@
notifCollection.addCollectionListener(entryListener)
}
- private val entryListener = object : NotifCollectionListener {
- override fun onEntryInit(entry: NotificationEntry) {
- entry.addOnSensitivityChangedListener(sensitivityListener)
+ private val entryListener =
+ object : NotifCollectionListener {
+ override fun onEntryInit(entry: NotificationEntry) {
+ entry.addOnSensitivityChangedListener(sensitivityListener)
+ }
+
+ override fun onEntryCleanUp(entry: NotificationEntry) {
+ entry.removeOnSensitivityChangedListener(sensitivityListener)
+ }
+
+ override fun onRankingApplied() {
+ // rankings affect whether a conversation is important, which can change the icons
+ recalculateForImportantConversationChange()
+ }
}
- override fun onEntryCleanUp(entry: NotificationEntry) {
- entry.removeOnSensitivityChangedListener(sensitivityListener)
- }
-
- override fun onRankingApplied() {
- // rankings affect whether a conversation is important, which can change the icons
- recalculateForImportantConversationChange()
- }
- }
-
- private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener {
- entry -> updateIconsSafe(entry)
- }
+ private val sensitivityListener =
+ NotificationEntry.OnSensitivityChangedListener { entry -> updateIconsSafe(entry) }
private fun recalculateForImportantConversationChange() {
for (entry in notifCollection.allNotifs) {
val isImportant = isImportantConversation(entry)
- if (entry.icons.areIconsAvailable &&
- isImportant != entry.icons.isImportantConversation
+ if (
+ entry.icons.areIconsAvailable && isImportant != entry.icons.isImportantConversation
) {
updateIconsSafe(entry)
}
@@ -97,34 +99,35 @@
* @throws InflationException Exception if required icons are not valid or specified
*/
@Throws(InflationException::class)
- fun createIcons(entry: NotificationEntry) = traceSection("IconManager.createIcons") {
- // Construct the status bar icon view.
- val sbIcon = iconBuilder.createIconView(entry)
- sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ fun createIcons(entry: NotificationEntry) =
+ traceSection("IconManager.createIcons") {
+ // Construct the status bar icon view.
+ val sbIcon = iconBuilder.createIconView(entry)
+ sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
- // Construct the shelf icon view.
- val shelfIcon = iconBuilder.createIconView(entry)
- shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
- shelfIcon.visibility = View.INVISIBLE
+ // Construct the shelf icon view.
+ val shelfIcon = iconBuilder.createIconView(entry)
+ shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ shelfIcon.visibility = View.INVISIBLE
- // Construct the aod icon view.
- val aodIcon = iconBuilder.createIconView(entry)
- aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
- aodIcon.setIncreasedSize(true)
+ // Construct the aod icon view.
+ val aodIcon = iconBuilder.createIconView(entry)
+ aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ aodIcon.setIncreasedSize(true)
- // Set the icon views' icons
- val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
+ // Set the icon views' icons
+ val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
- try {
- setIcon(entry, normalIconDescriptor, sbIcon)
- setIcon(entry, sensitiveIconDescriptor, shelfIcon)
- setIcon(entry, sensitiveIconDescriptor, aodIcon)
- entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons)
- } catch (e: InflationException) {
- entry.icons = IconPack.buildEmptyPack(entry.icons)
- throw e
+ try {
+ setIcon(entry, normalIconDescriptor, sbIcon)
+ setIcon(entry, sensitiveIconDescriptor, shelfIcon)
+ setIcon(entry, sensitiveIconDescriptor, aodIcon)
+ entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons)
+ } catch (e: InflationException) {
+ entry.icons = IconPack.buildEmptyPack(entry.icons)
+ throw e
+ }
}
- }
/**
* Update the notification icons.
@@ -133,33 +136,33 @@
* @throws InflationException Exception if required icons are not valid or specified
*/
@Throws(InflationException::class)
- fun updateIcons(entry: NotificationEntry) = traceSection("IconManager.updateIcons") {
- if (!entry.icons.areIconsAvailable) {
- return@traceSection
- }
- entry.icons.smallIconDescriptor = null
- entry.icons.peopleAvatarDescriptor = null
+ fun updateIcons(entry: NotificationEntry) =
+ traceSection("IconManager.updateIcons") {
+ if (!entry.icons.areIconsAvailable) {
+ return@traceSection
+ }
+ entry.icons.smallIconDescriptor = null
+ entry.icons.peopleAvatarDescriptor = null
- val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
- val notificationContentDescription = entry.sbn.notification?.let {
- iconBuilder.getIconContentDescription(it)
- }
+ val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
+ val notificationContentDescription =
+ entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
- entry.icons.statusBarIcon?.let {
- it.setNotification(entry.sbn, notificationContentDescription)
- setIcon(entry, normalIconDescriptor, it)
- }
+ entry.icons.statusBarIcon?.let {
+ it.setNotification(entry.sbn, notificationContentDescription)
+ setIcon(entry, normalIconDescriptor, it)
+ }
- entry.icons.shelfIcon?.let {
- it.setNotification(entry.sbn, notificationContentDescription)
- setIcon(entry, normalIconDescriptor, it)
- }
+ entry.icons.shelfIcon?.let {
+ it.setNotification(entry.sbn, notificationContentDescription)
+ setIcon(entry, sensitiveIconDescriptor, it)
+ }
- entry.icons.aodIcon?.let {
- it.setNotification(entry.sbn, notificationContentDescription)
- setIcon(entry, sensitiveIconDescriptor, it)
+ entry.icons.aodIcon?.let {
+ it.setNotification(entry.sbn, notificationContentDescription)
+ setIcon(entry, sensitiveIconDescriptor, it)
+ }
}
- }
private fun updateIconsSafe(entry: NotificationEntry) {
try {
@@ -173,11 +176,12 @@
@Throws(InflationException::class)
private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> {
val iconDescriptor = getIconDescriptor(entry, redact = false)
- val sensitiveDescriptor = if (entry.isSensitive) {
- getIconDescriptor(entry, redact = true)
- } else {
- iconDescriptor
- }
+ val sensitiveDescriptor =
+ if (entry.isSensitive) {
+ getIconDescriptor(entry, redact = true)
+ } else {
+ iconDescriptor
+ }
return Pair(iconDescriptor, sensitiveDescriptor)
}
@@ -197,14 +201,15 @@
}
val icon =
- (if (showPeopleAvatar) {
- createPeopleAvatar(entry)
- } else {
- n.smallIcon
- }) ?: throw InflationException(
- "No icon in notification from " + entry.sbn.packageName)
+ (if (showPeopleAvatar) {
+ createPeopleAvatar(entry)
+ } else {
+ n.smallIcon
+ })
+ ?: throw InflationException("No icon in notification from " + entry.sbn.packageName)
- val ic = StatusBarIcon(
+ val ic =
+ StatusBarIcon(
entry.sbn.user,
entry.sbn.packageName,
icon,
@@ -282,8 +287,8 @@
/**
* Determines if this icon shows a conversation based on the sensitivity of the icon, its
- * context and the user's indicated sensitivity preference. If we're using a fall back icon
- * of the small icon, we don't consider this to be showing a conversation
+ * context and the user's indicated sensitivity preference. If we're using a fall back icon of
+ * the small icon, we don't consider this to be showing a conversation
*
* @param iconView The icon that shows the conversation.
*/
@@ -293,19 +298,20 @@
iconDescriptor: StatusBarIcon
): Boolean {
val usedInSensitiveContext =
- iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
+ iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
- return isImportantConversation(entry) && !isSmallIcon &&
- (!usedInSensitiveContext || !entry.isSensitive)
+ return isImportantConversation(entry) &&
+ !isSmallIcon &&
+ (!usedInSensitiveContext || !entry.isSensitive)
}
private fun isImportantConversation(entry: NotificationEntry): Boolean {
// Also verify that the Notification is MessagingStyle, since we're going to access
// MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON).
return entry.ranking.channel != null &&
- entry.ranking.channel.isImportantConversation &&
- entry.sbn.notification.isStyle(MessagingStyle::class.java) &&
- entry.key !in unimportantConversationKeys
+ entry.ranking.channel.isImportantConversation &&
+ entry.sbn.notification.isStyle(MessagingStyle::class.java) &&
+ entry.key !in unimportantConversationKeys
}
override fun setUnimportantConversations(keys: Collection<String>) {
@@ -323,8 +329,8 @@
interface ConversationIconManager {
/**
* Sets the complete current set of notification keys which should (for the purposes of icon
- * presentation) be considered unimportant. This tells the icon manager to remove the avatar
- * of a group from which the priority notification has been removed.
+ * presentation) be considered unimportant. This tells the icon manager to remove the avatar of
+ * a group from which the priority notification has been removed.
*/
fun setUnimportantConversations(keys: Collection<String>)
-}
\ No newline at end of file
+}
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 d828ad7..decb244 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
@@ -849,6 +849,7 @@
public void setNotificationGroupWhen(long whenMillis) {
if (mIsSummaryWithChildren) {
mChildrenContainer.setNotificationGroupWhen(whenMillis);
+ mPublicLayout.setNotificationWhen(whenMillis);
} else {
Log.w(TAG, "setNotificationGroupWhen( whenMillis: " + whenMillis + ")"
+ " mIsSummaryWithChildren: false"
@@ -2704,6 +2705,7 @@
}
private void onAttachedChildrenCountChanged() {
+ final boolean wasSummary = mIsSummaryWithChildren;
mIsSummaryWithChildren = mChildrenContainer != null
&& mChildrenContainer.getNotificationChildCount() > 0;
if (mIsSummaryWithChildren) {
@@ -2714,6 +2716,10 @@
isConversation());
}
}
+ if (!mIsSummaryWithChildren && wasSummary) {
+ // Reset the 'when' once the row stops being a summary
+ mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().when);
+ }
getShowingLayout().updateBackgroundColor(false /* animate */);
mPrivateLayout.updateExpandButtons(isExpandable());
updateChildrenAppearance();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 402ea51..3742482 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -59,6 +59,7 @@
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
@@ -2314,6 +2315,13 @@
return false;
}
+ public void setNotificationWhen(long whenMillis) {
+ NotificationViewWrapper wrapper = getNotificationViewWrapper();
+ if (wrapper instanceof NotificationHeaderViewWrapper headerViewWrapper) {
+ headerViewWrapper.setNotificationWhen(whenMillis);
+ }
+ }
+
private static class RemoteInputViewData {
@Nullable RemoteInputView mView;
@Nullable RemoteInputViewController mController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
index bae5baa..5551ab4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
@@ -22,6 +22,8 @@
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
+import android.util.Log
+import com.android.systemui.Flags
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -87,4 +89,63 @@
) {
// No animation, it doesn't need it, this would be local
}
+
+ override fun setVisibility(visibility: Int) {
+ if (Flags.bindKeyguardMediaVisibility()) {
+ if (isVisibilityValid(visibility)) {
+ super.setVisibility(visibility)
+ }
+ } else {
+ super.setVisibility(visibility)
+ }
+
+ assertMediaContainerVisibility(visibility)
+ }
+
+ /**
+ * visibility should be aligned with MediaContainerView visibility on the keyguard.
+ */
+ private fun isVisibilityValid(visibility: Int): Boolean {
+ val currentViewState = viewState as? MediaContainerViewState ?: return true
+ val shouldBeGone = !currentViewState.shouldBeVisible
+ return if (shouldBeGone) visibility == GONE else visibility != GONE
+ }
+
+ /**
+ * b/298213983
+ * MediaContainerView's visibility is changed to VISIBLE when it should be GONE.
+ * This method check this state and logs.
+ */
+ private fun assertMediaContainerVisibility(visibility: Int) {
+ val currentViewState = viewState
+
+ if (currentViewState is MediaContainerViewState) {
+ if (!currentViewState.shouldBeVisible && visibility == VISIBLE) {
+ Log.wtf("MediaContainerView", "MediaContainerView should be GONE " +
+ "but its visibility changed to VISIBLE")
+ }
+ }
+ }
+
+ fun setKeyguardVisibility(isVisible: Boolean) {
+ val currentViewState = viewState
+ if (currentViewState is MediaContainerViewState) {
+ currentViewState.shouldBeVisible = isVisible
+ }
+
+ visibility = if (isVisible) VISIBLE else GONE
+ }
+
+ override fun createExpandableViewState(): ExpandableViewState = MediaContainerViewState()
+
+ class MediaContainerViewState : ExpandableViewState() {
+ var shouldBeVisible: Boolean = false
+
+ override fun copyFrom(viewState: ViewState) {
+ super.copyFrom(viewState)
+ if (viewState is MediaContainerViewState) {
+ shouldBeVisible = viewState.shouldBeVisible
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/SceneContainerFlagsExtension.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/SceneContainerFlagsExtension.kt
index 5a71bd6..7dfd53e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/SceneContainerFlagsExtension.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/SceneContainerFlagsExtension.kt
@@ -18,7 +18,7 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlags
-private const val FLEXI_NOTIFS = false
+private const val FLEXI_NOTIFS = true
/**
* Returns whether flexiglass is displaying notifications, which is currently an optional piece of
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 5191053..566c030 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -75,6 +75,24 @@
}
}
+ // Required to capture keyguard media changes and ensure the notification count is correct
+ val layoutChangeListener =
+ object : View.OnLayoutChangeListener {
+ override fun onLayoutChange(
+ view: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ viewModel.notificationStackChanged()
+ }
+ }
+
val burnInParams = MutableStateFlow(BurnInParameters())
val viewState =
ViewStateAccessor(
@@ -170,6 +188,7 @@
}
insets
}
+ view.addOnLayoutChangeListener(layoutChangeListener)
return object : DisposableHandle {
override fun dispose() {
@@ -177,6 +196,7 @@
disposableHandleMainImmediate.dispose()
controller.setOnHeightChangedRunnable(null)
view.setOnApplyWindowInsetsListener(null)
+ view.removeOnLayoutChangeListener(layoutChangeListener)
}
}
}
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 b772158..db15144 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1375,7 +1375,10 @@
|| !mKeyguardStateController.canDismissLockScreen()
|| mKeyguardViewMediator.isAnySimPinSecure()
|| (mQsController.getExpanded() && trackingTouch)
- || mShadeSurface.getBarState() == StatusBarState.SHADE_LOCKED) {
+ || mShadeSurface.getBarState() == StatusBarState.SHADE_LOCKED
+ // This last one causes a race condition when the shade resets. Don't send a 0
+ // and let StatusBarStateController process a keyguard state change instead
+ || 1f - fraction == 0f) {
return;
}
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 4fd33ba..5610ed9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -55,6 +55,7 @@
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -138,7 +139,7 @@
Context context,
@DisplayId int displayId,
Handler mainThreadHandler,
- Executor uiBgExecutor,
+ @Background Executor uiBgExecutor,
NotificationVisibilityProvider visibilityProvider,
HeadsUpManager headsUpManager,
ActivityStarter activityStarter,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index f73d089..3e3ea85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -315,8 +315,8 @@
// TTL for satellite polling is one hour
const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
- // Let the system boot up (5s) and stabilize before we check for system support
- const val MIN_UPTIME: Long = 1000 * 5
+ // Let the system boot up and stabilize before we check for system support
+ const val MIN_UPTIME: Long = 1000 * 60
private const val TAG = "DeviceBasedSatelliteRepo"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
index a078dd5..2ad4d36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
@@ -23,6 +23,7 @@
import android.content.Context
import android.content.Intent
import android.net.Uri
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.res.R
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject
@@ -34,7 +35,7 @@
class BatteryStateNotifier @Inject constructor(
val controller: BatteryController,
val noMan: NotificationManager,
- val delayableExecutor: DelayableExecutor,
+ @Background val delayableExecutor: DelayableExecutor,
val context: Context
) : BatteryController.BatteryStateChangeCallback {
var stateUnknown = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 6956a7d..18ec68b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -51,6 +51,7 @@
public class SensitiveNotificationProtectionControllerImpl
implements SensitiveNotificationProtectionController {
private static final String LOG_TAG = "SNPC";
+ private final SensitiveNotificationProtectionControllerLogger mLogger;
private final ArraySet<String> mExemptPackages = new ArraySet<>();
private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
private volatile MediaProjectionInfo mProjection;
@@ -66,6 +67,7 @@
if (mDisableScreenShareProtections) {
Log.w(LOG_TAG,
"Screen share protections disabled, ignoring projectionstart");
+ mLogger.logProjectionStart(false, info.getPackageName());
return;
}
@@ -73,6 +75,7 @@
// Launch cookie only set (non-null) if sharing single app/task
updateProjectionStateAndNotifyListeners(
(info.getLaunchCookie() == null) ? info : null);
+ mLogger.logProjectionStart(isSensitiveStateActive(), info.getPackageName());
} finally {
Trace.endSection();
}
@@ -82,6 +85,7 @@
public void onStop(MediaProjectionInfo info) {
Trace.beginSection("SNPC.onProjectionStop");
try {
+ mLogger.logProjectionStop();
updateProjectionStateAndNotifyListeners(null);
} finally {
Trace.endSection();
@@ -96,7 +100,10 @@
MediaProjectionManager mediaProjectionManager,
IActivityManager activityManager,
@Main Handler mainHandler,
- @Background Executor bgExecutor) {
+ @Background Executor bgExecutor,
+ SensitiveNotificationProtectionControllerLogger logger) {
+ mLogger = logger;
+
if (!screenshareNotificationHiding()) {
return;
}
@@ -202,8 +209,6 @@
return false;
}
- // TODO(b/316955558): Add disabled by developer option
-
return !mExemptPackages.contains(projection.getPackageName());
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerLogger.kt
new file mode 100644
index 0000000..70c5239
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerLogger.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.statusbar.policy
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.SensitiveNotificationProtectionLog
+import javax.inject.Inject
+
+/** Logger for [SensitiveNotificationProtectionController]. */
+class SensitiveNotificationProtectionControllerLogger
+@Inject
+constructor(@SensitiveNotificationProtectionLog private val buffer: LogBuffer) {
+ fun logProjectionStart(protectionEnabled: Boolean, pkg: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ bool1 = protectionEnabled
+ str1 = pkg
+ },
+ { "Projection started - protection enabled:$bool1, pkg=$str1" }
+ )
+ }
+
+ fun logProjectionStop() {
+ buffer.log(TAG, LogLevel.DEBUG, {}, { "Projection ended - protection disabled" })
+ }
+}
+
+private const val TAG = "SNPC"
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
index 6124f63..2cad844 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
@@ -124,15 +124,6 @@
}
/**
- * Provide a Background-Thread Executor by default.
- */
- @Provides
- @SysUISingleton
- public static Executor provideExecutor(@Background Looper looper) {
- return new ExecutorImpl(looper);
- }
-
- /**
* Provide a BroadcastRunning Executor (for sending and receiving broadcasts).
*/
@Provides
@@ -174,15 +165,6 @@
}
/**
- * Provide a Background-Thread Executor by default.
- */
- @Provides
- @SysUISingleton
- public static DelayableExecutor provideDelayableExecutor(@Background Looper looper) {
- return new ExecutorImpl(looper);
- }
-
- /**
* Provide a Background-Thread Executor.
*/
@Provides
@@ -193,15 +175,6 @@
}
/**
- * Provide a Background-Thread Executor by default.
- */
- @Provides
- @SysUISingleton
- public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) {
- return new RepeatableExecutorImpl(exec);
- }
-
- /**
* Provide a Background-Thread Executor.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
index 9b72eb7..5979f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
@@ -28,6 +28,7 @@
import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock;
@@ -94,10 +95,12 @@
}
};
+ // TODO: b/326449074 - Ensure the DelayableExecutor is on the correct thread, and update the
+ // qualifier (to @Main) or name (to bgExecutor) to be consistent with that.
@Inject
public PersistentConnectionManager(
SystemClock clock,
- DelayableExecutor mainExecutor,
+ @Background DelayableExecutor mainExecutor,
DumpManager dumpManager,
@Named(DUMPSYS_NAME) String dumpsysName,
@Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index 8431fbc..9dedf5c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -18,9 +18,6 @@
import android.content.Context
import android.media.AudioManager
-import com.android.settingslib.media.data.repository.SpatializerRepository
-import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
-import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
@@ -66,16 +63,5 @@
notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
): AudioVolumeInteractor =
AudioVolumeInteractor(audioRepository, notificationsSoundPolicyInteractor)
-
- @Provides
- fun provdieSpatializerRepository(
- audioManager: AudioManager,
- @Background backgroundContext: CoroutineContext,
- ): SpatializerRepository =
- SpatializerRepositoryImpl(audioManager.spatializer, backgroundContext)
-
- @Provides
- fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor =
- SpatializerInteractor(repository)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt
new file mode 100644
index 0000000..18a9161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.volume.dagger
+
+import android.media.AudioManager
+import android.media.Spatializer
+import com.android.settingslib.media.data.repository.SpatializerRepository
+import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
+import com.android.settingslib.media.domain.interactor.SpatializerInteractor
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+
+/** Spatializer module. */
+@Module
+interface SpatializerModule {
+ companion object {
+ @Provides
+ fun provideSpatializer(
+ audioManager: AudioManager,
+ ): Spatializer = audioManager.spatializer
+
+ @Provides
+ fun provdieSpatializerRepository(
+ spatializer: Spatializer,
+ @Background backgroundContext: CoroutineContext,
+ ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, backgroundContext)
+
+ @Provides
+ fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor =
+ SpatializerInteractor(repository)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index c6aee42..64a5644 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -59,7 +59,8 @@
AudioModule.class,
AncModule.class,
CaptioningModule.class,
- MediaDevicesModule.class
+ MediaDevicesModule.class,
+ SpatializerModule.class,
},
subcomponents = {
VolumePanelComponent.class
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
index f11ac5e..9d801fc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
@@ -22,6 +22,7 @@
const val MEDIA_OUTPUT: VolumePanelComponentKey = "media_output"
const val BOTTOM_BAR: VolumePanelComponentKey = "bottom_bar"
+ const val VOLUME_SLIDERS: VolumePanelComponentKey = "volume_sliders"
const val CAPTIONING: VolumePanelComponentKey = "captioning"
const val ANC: VolumePanelComponentKey = "anc"
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt
index 52736c6..0c91bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt
@@ -24,7 +24,7 @@
class VolumeSliderInteractor @Inject constructor() {
/** mimic percentage volume setting */
- private val displayValueRange: ClosedFloatingPointRange<Float> = 0f..100f
+ val displayValueRange: ClosedFloatingPointRange<Float> = 0f..100f
/**
* Translates [volume], that belongs to [volumeRange] to the value that belongs to
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
new file mode 100644
index 0000000..faf7434
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.slider.ui.viewmodel
+
+import android.content.Context
+import android.media.AudioManager
+import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Models a particular slider state. */
+class AudioStreamSliderViewModel
+@AssistedInject
+constructor(
+ @Assisted private val audioStreamWrapper: FactoryAudioStreamWrapper,
+ @Assisted private val coroutineScope: CoroutineScope,
+ private val context: Context,
+ private val audioVolumeInteractor: AudioVolumeInteractor,
+ private val volumeSliderInteractor: VolumeSliderInteractor,
+) : SliderViewModel {
+
+ private val audioStream = audioStreamWrapper.audioStream
+ private val iconsByStream =
+ mapOf(
+ AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_music_note,
+ AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_call,
+ AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_ring_volume,
+ AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer,
+ AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm,
+ )
+ private val mutedIconsByStream =
+ mapOf(
+ AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_volume_off,
+ AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_volume_off,
+ AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_volume_off,
+ AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_off,
+ AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_off,
+ )
+ private val labelsByStream =
+ mapOf(
+ AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music,
+ AudioStream(AudioManager.STREAM_VOICE_CALL) to R.string.stream_voice_call,
+ AudioStream(AudioManager.STREAM_RING) to R.string.stream_ring,
+ AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification,
+ AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm,
+ )
+ private val disabledTextByStream =
+ mapOf(
+ AudioStream(AudioManager.STREAM_NOTIFICATION) to
+ R.string.stream_notification_unavailable,
+ )
+
+ private var value = 0f
+ override val slider: StateFlow<SliderState> =
+ combine(
+ audioVolumeInteractor.getAudioStream(audioStream),
+ audioVolumeInteractor.canChangeVolume(audioStream),
+ ) { model, isEnabled ->
+ model.toState(value, isEnabled)
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
+
+ override fun onValueChangeFinished(state: SliderState, newValue: Float) {
+ val audioViewModel = state as? State
+ audioViewModel ?: return
+ coroutineScope.launch {
+ value = newValue
+ val volume =
+ volumeSliderInteractor.translateValueToVolume(
+ newValue,
+ audioViewModel.audioStreamModel.volumeRange
+ )
+ audioVolumeInteractor.setVolume(audioStream, volume)
+ }
+ }
+
+ private fun AudioStreamModel.toState(value: Float, isEnabled: Boolean): State {
+ return State(
+ value =
+ volumeSliderInteractor.processVolumeToValue(
+ volume,
+ volumeRange,
+ value,
+ isMuted,
+ ),
+ valueRange = volumeSliderInteractor.displayValueRange,
+ icon = getIcon(this),
+ label = labelsByStream[audioStream]?.let(context::getString)
+ ?: error("No label for the stream: $audioStream"),
+ disabledMessage = disabledTextByStream[audioStream]?.let(context::getString),
+ isEnabled = isEnabled,
+ audioStreamModel = this,
+ )
+ }
+
+ private fun getIcon(model: AudioStreamModel): Icon {
+ val isMutedOrNoVolume = model.isMuted || model.volume == model.minVolume
+ val iconRes =
+ if (isMutedOrNoVolume) {
+ mutedIconsByStream
+ } else {
+ iconsByStream
+ }[audioStream]
+ ?: error("No icon for the stream: $audioStream")
+ return Icon.Resource(iconRes, null)
+ }
+
+ private val AudioStreamModel.volumeRange: IntRange
+ get() = minVolume..maxVolume
+
+ private data class State(
+ override val value: Float,
+ override val valueRange: ClosedFloatingPointRange<Float>,
+ override val icon: Icon,
+ override val label: String,
+ override val disabledMessage: String?,
+ override val isEnabled: Boolean,
+ val audioStreamModel: AudioStreamModel,
+ ) : SliderState
+
+ private data object EmptyState : SliderState {
+ override val value: Float = 0f
+ override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
+ override val icon: Icon? = null
+ override val label: String = ""
+ override val disabledMessage: String? = null
+ override val isEnabled: Boolean = true
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(
+ audioStream: FactoryAudioStreamWrapper,
+ coroutineScope: CoroutineScope,
+ ): AudioStreamSliderViewModel
+ }
+
+ /**
+ * AudioStream is a value class that compiles into a primitive. This fail AssistedFactory build
+ * when using [AudioStream] directly because it expects another type.
+ */
+ class FactoryAudioStreamWrapper(val audioStream: AudioStream)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
new file mode 100644
index 0000000..ae93826
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.slider.ui.viewmodel
+
+import android.content.Context
+import com.android.settingslib.volume.domain.model.RoutingSession
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
+import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+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.stateIn
+import kotlinx.coroutines.launch
+
+class CastVolumeSliderViewModel
+@AssistedInject
+constructor(
+ @Assisted private val routingSession: RoutingSession,
+ @Assisted private val coroutineScope: CoroutineScope,
+ private val context: Context,
+ mediaOutputInteractor: MediaOutputInteractor,
+ private val volumeSliderInteractor: VolumeSliderInteractor,
+ private val castVolumeInteractor: CastVolumeInteractor,
+) : SliderViewModel {
+
+ private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax
+ private val value = MutableStateFlow(0f)
+
+ override val slider: StateFlow<SliderState> =
+ combine(value, mediaOutputInteractor.currentConnectedDevice) { value, _ ->
+ getCurrentState(value)
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState(value.value))
+
+ override fun onValueChangeFinished(state: SliderState, newValue: Float) {
+ coroutineScope.launch {
+ value.value = newValue
+ castVolumeInteractor.setVolume(
+ routingSession,
+ volumeSliderInteractor.translateValueToVolume(newValue, volumeRange),
+ )
+ }
+ }
+
+ private fun getCurrentState(value: Float): State {
+ return State(
+ value =
+ volumeSliderInteractor.processVolumeToValue(
+ volume = routingSession.routingSessionInfo.volume,
+ volumeRange = volumeRange,
+ currentValue = value,
+ isMuted = false,
+ ),
+ valueRange = volumeSliderInteractor.displayValueRange,
+ icon = Icon.Resource(R.drawable.ic_cast, null),
+ label = context.getString(R.string.media_device_cast),
+ isEnabled = true,
+ )
+ }
+
+ private data class State(
+ override val value: Float,
+ override val valueRange: ClosedFloatingPointRange<Float>,
+ override val icon: Icon,
+ override val label: String,
+ override val isEnabled: Boolean,
+ ) : SliderState {
+ override val disabledMessage: String?
+ get() = null
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(
+ routingSession: RoutingSession,
+ coroutineScope: CoroutineScope,
+ ): CastVolumeSliderViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
new file mode 100644
index 0000000..6e9794b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.slider.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Models a state of a volume slider.
+ *
+ * @property disabledMessage is shown when [isEnabled] is false
+ */
+sealed interface SliderState {
+ val value: Float
+ val valueRange: ClosedFloatingPointRange<Float>
+ val icon: Icon?
+ val label: String
+ val disabledMessage: String?
+ val isEnabled: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt
new file mode 100644
index 0000000..0c4b322
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.slider.ui.viewmodel
+
+import kotlinx.coroutines.flow.StateFlow
+
+/** Controls the behaviour of a volume slider. */
+interface SliderViewModel {
+
+ val slider: StateFlow<SliderState>
+
+ fun onValueChangeFinished(state: SliderState, newValue: Float)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
new file mode 100644
index 0000000..2824323
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.volume.panel.component.volume.ui.viewmodel
+
+import android.media.AudioManager
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
+import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
+
+/**
+ * Controls the behaviour of the whole audio
+ * [com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class AudioVolumeComponentViewModel
+@Inject
+constructor(
+ @VolumePanelScope private val scope: CoroutineScope,
+ castVolumeInteractor: CastVolumeInteractor,
+ private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
+ private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
+) {
+
+ private val remoteSessionsViewModels: Flow<List<SliderViewModel>> =
+ castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions ->
+ coroutineScope {
+ emit(
+ routingSessions.map { routingSession ->
+ castVolumeSliderViewModelFactory.create(routingSession, this)
+ }
+ )
+ }
+ }
+ private val streamViewModels: Flow<List<SliderViewModel>> =
+ flowOf(
+ listOf(
+ AudioStream(AudioManager.STREAM_MUSIC),
+ AudioStream(AudioManager.STREAM_VOICE_CALL),
+ AudioStream(AudioManager.STREAM_RING),
+ AudioStream(AudioManager.STREAM_NOTIFICATION),
+ AudioStream(AudioManager.STREAM_ALARM),
+ )
+ )
+ .transformLatest { streams ->
+ coroutineScope {
+ emit(
+ streams.map { stream ->
+ streamSliderViewModelFactory.create(
+ AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream),
+ this,
+ )
+ }
+ )
+ }
+ }
+
+ val sliderViewModels: StateFlow<List<SliderViewModel>> =
+ combine(remoteSessionsViewModels, streamViewModels) {
+ remoteSessionsViewModels,
+ streamViewModels ->
+ remoteSessionsViewModels + streamViewModels
+ }
+ .stateIn(scope, SharingStarted.Eagerly, emptyList())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
index df4972a..f31ee86 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
@@ -20,6 +20,7 @@
import com.android.systemui.volume.panel.component.bottombar.BottomBarModule
import com.android.systemui.volume.panel.component.captioning.CaptioningModule
import com.android.systemui.volume.panel.component.mediaoutput.MediaOutputModule
+import com.android.systemui.volume.panel.component.volume.VolumeSlidersModule
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.DomainModule
@@ -48,6 +49,7 @@
// Components modules
BottomBarModule::class,
AncModule::class,
+ VolumeSlidersModule::class,
CaptioningModule::class,
MediaOutputModule::class,
]
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
index 0d65c42..57ea997 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
@@ -52,6 +52,7 @@
return setOf(
VolumePanelComponents.ANC,
VolumePanelComponents.CAPTIONING,
+ VolumePanelComponents.VOLUME_SLIDERS,
VolumePanelComponents.MEDIA_OUTPUT,
VolumePanelComponents.BOTTOM_BAR,
)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
index 7f33a6b..f57e293 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
@@ -23,12 +23,12 @@
* State of the Volume Panel itself.
*
* @property orientation is current Volume Panel orientation
- * @property isWideScreen is true when Volume Panel should use wide-screen layout and false the
+ * @property isLargeScreen is true when Volume Panel should use wide-screen layout and false the
* otherwise
*/
data class VolumePanelState(
@Orientation val orientation: Int,
- val isWideScreen: Boolean,
+ val isLargeScreen: Boolean,
val isVisible: Boolean,
) {
init {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
index 3c5b75c..5ae827f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
@@ -76,7 +76,7 @@
VolumePanelState(
orientation = configuration.orientation,
isVisible = isVisible,
- isWideScreen = !resources.getBoolean(R.bool.config_edgeToEdgeBottomSheetDialog),
+ isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen),
)
}
.stateIn(
@@ -85,7 +85,7 @@
VolumePanelState(
orientation = resources.configuration.orientation,
isVisible = mutablePanelVisibility.value,
- isWideScreen = !resources.getBoolean(R.bool.config_edgeToEdgeBottomSheetDialog)
+ isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen)
),
)
val componentsLayout: Flow<ComponentsLayout> =
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 139d190..65dede8 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -25,6 +25,7 @@
import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
+import static com.android.server.notification.Flags.screenshareNotificationHiding;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -69,6 +70,7 @@
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleEntry;
@@ -102,6 +104,7 @@
private final NotificationVisibilityProvider mVisibilityProvider;
private final VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider;
private final NotificationLockscreenUserManager mNotifUserManager;
+ private final SensitiveNotificationProtectionController mSensitiveNotifProtectionController;
private final CommonNotifCollection mCommonNotifCollection;
private final NotifPipeline mNotifPipeline;
private final NotifPipelineFlags mNotifPipelineFlags;
@@ -111,6 +114,7 @@
// TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
private final List<NotifCallback> mCallbacks = new ArrayList<>();
private final StatusBarWindowCallback mStatusBarWindowCallback;
+ private final Runnable mSensitiveStateChangedListener;
private boolean mPanelExpanded;
/**
@@ -130,6 +134,7 @@
VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
+ SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
CommonNotifCollection notifCollection,
NotifPipeline notifPipeline,
SysUiState sysUiState,
@@ -149,6 +154,7 @@
visualInterruptionDecisionProvider,
zenModeController,
notifUserManager,
+ sensitiveNotificationProtectionController,
notifCollection,
notifPipeline,
sysUiState,
@@ -173,6 +179,7 @@
VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
+ SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
CommonNotifCollection notifCollection,
NotifPipeline notifPipeline,
SysUiState sysUiState,
@@ -188,6 +195,7 @@
mVisibilityProvider = visibilityProvider;
mVisualInterruptionDecisionProvider = visualInterruptionDecisionProvider;
mNotifUserManager = notifUserManager;
+ mSensitiveNotifProtectionController = sensitiveNotificationProtectionController;
mCommonNotifCollection = notifCollection;
mNotifPipeline = notifPipeline;
mNotifPipelineFlags = notifPipelineFlags;
@@ -251,6 +259,22 @@
};
notificationShadeWindowController.registerCallback(mStatusBarWindowCallback);
+ mSensitiveStateChangedListener = new Runnable() {
+ @Override
+ public void run() {
+ if (!screenshareNotificationHiding()) {
+ return;
+ }
+ bubbles.onSensitiveNotificationProtectionStateChanged(
+ mSensitiveNotifProtectionController.isSensitiveStateActive());
+ }
+ };
+
+ if (screenshareNotificationHiding()) {
+ mSensitiveNotifProtectionController
+ .registerSensitiveStateListener(mSensitiveStateChangedListener);
+ }
+
mSysuiProxy = new Bubbles.SysuiProxy() {
@Override
public void isNotificationPanelExpand(Consumer<Boolean> callback) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index c2ed7d4..976cd5bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -89,6 +90,7 @@
mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance,
mSecureSettings));
mMenuView.setTranslationY(halfScreenHeight);
+ doNothing().when(mMenuView).gotoEditScreen();
mMenuViewLayer = spy(new MenuViewLayer(
mContext, stubWindowManager, mAccessibilityManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index ce4db8f..3862b0f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -160,6 +160,7 @@
new MenuView(mSpyContext, mMenuViewModel, menuViewAppearance, mSecureSettings));
// Ensure tests don't actually update metrics.
doNothing().when(mMenuView).incrementTexMetric(any(), anyInt());
+ doNothing().when(mMenuView).gotoEditScreen();
mMenuViewLayer = spy(new MenuViewLayer(mSpyContext, mStubWindowManager,
mStubAccessibilityManager, mMenuViewModel, menuViewAppearance, mMenuView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index 7c97f53..1ce6525 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -80,6 +81,7 @@
mUiModeManager.setNightMode(MODE_NIGHT_YES);
mSpyContext = spy(mContext);
+ doNothing().when(mSpyContext).startActivity(any());
final SecureSettings secureSettings = TestUtils.mockSecureSettings();
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
secureSettings);
@@ -179,8 +181,6 @@
@Test
@EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT)
public void gotoEditScreen_sendsIntent() {
- // Notably, this shouldn't crash the settings app,
- // because the button target args are configured.
mMenuView.gotoEditScreen();
verify(mSpyContext).startActivity(any());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index dc438d7..47dc70f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -83,11 +83,13 @@
val strength by collectLastValue(repository.strength)
val sensorType by collectLastValue(repository.sensorType)
val sensorLocations by collectLastValue(repository.sensorLocations)
+ val propertiesInitialized by collectLastValue(repository.propertiesInitialized)
- // Assert default properties.
- assertThat(sensorId).isEqualTo(-1)
+ // Assert uninitialized properties.
+ assertThat(sensorId).isEqualTo(-2)
assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
+ assertThat(propertiesInitialized).isEqualTo(false)
val fingerprintProps =
listOf(
@@ -129,6 +131,7 @@
assertThat(sensorId).isEqualTo(1)
assertThat(strength).isEqualTo(SensorStrength.STRONG)
assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
+ assertThat(propertiesInitialized).isEqualTo(true)
assertThat(sensorLocations?.size).isEqualTo(2)
assertThat(sensorLocations).containsKey("display_id_1")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index ff68fe3..140849b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -241,19 +241,27 @@
viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
- val confirmConstant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(confirmConstant)
+ val confirmHaptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(confirmHaptics?.hapticFeedbackConstant)
.isEqualTo(
if (expectConfirmation) HapticFeedbackConstants.NO_HAPTICS
else HapticFeedbackConstants.CONFIRM
)
+ assertThat(confirmHaptics?.flag)
+ .isEqualTo(
+ if (expectConfirmation) null
+ else HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ )
if (expectConfirmation) {
viewModel.confirmAuthenticated()
}
- val confirmedConstant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(confirmedConstant).isEqualTo(HapticFeedbackConstants.CONFIRM)
+ val confirmedHaptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(confirmedHaptics?.hapticFeedbackConstant)
+ .isEqualTo(HapticFeedbackConstants.CONFIRM)
+ assertThat(confirmedHaptics?.flag)
+ .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
}
@Test
@@ -265,16 +273,21 @@
viewModel.confirmAuthenticated()
}
- val currentConstant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.CONFIRM)
+ val currentHaptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(currentHaptics?.hapticFeedbackConstant)
+ .isEqualTo(HapticFeedbackConstants.CONFIRM)
+ assertThat(currentHaptics?.flag)
+ .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
}
@Test
fun playErrorHaptic_SetsRejectConstant() = runGenericTest {
viewModel.showTemporaryError("test", "messageAfterError", false)
- val currentConstant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT)
+ val currentHaptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(currentHaptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT)
+ assertThat(currentHaptics?.flag)
+ .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
}
@Test
@@ -800,8 +813,9 @@
hapticFeedback = true,
)
- val constant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(constant).isEqualTo(HapticFeedbackConstants.REJECT)
+ val haptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT)
+ assertThat(haptics?.flag).isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
}
@Test
@@ -813,8 +827,8 @@
hapticFeedback = false,
)
- val constant by collectLastValue(viewModel.hapticsToPlay)
- assertThat(constant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS)
+ val haptics by collectLastValue(viewModel.hapticsToPlay)
+ assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS)
}
private suspend fun TestScope.showTemporaryErrors(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 9df00d3..b0d8de3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -19,17 +19,23 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
+import com.android.systemui.plugins.clocks.ClockConfig
+import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -38,6 +44,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -54,6 +61,8 @@
@Mock private lateinit var splitShadeStateController: SplitShadeStateController
@Mock private lateinit var keyguardBlueprintRepository: KeyguardBlueprintRepository
+ @Mock private lateinit var clockInteractor: KeyguardClockInteractor
+ @Mock private lateinit var clockController: ClockController
@Before
fun setup() {
@@ -61,6 +70,8 @@
testScope = TestScope(StandardTestDispatcher())
whenever(keyguardBlueprintRepository.configurationChange).thenReturn(configurationFlow)
whenever(keyguardBlueprintRepository.refreshTransition).thenReturn(refreshTransition)
+ whenever(clockInteractor.currentClock).thenReturn(MutableStateFlow(clockController))
+ clockInteractor.currentClock
underTest =
KeyguardBlueprintInteractor(
@@ -68,6 +79,7 @@
testScope.backgroundScope,
mContext,
splitShadeStateController,
+ clockInteractor,
)
}
@@ -102,6 +114,77 @@
}
@Test
+ fun composeLockscreenOff_DoesAppliesSplitShadeWeatherClockBlueprint() {
+ testScope.runTest {
+ mSetFlagsRule.disableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ whenever(clockController.config)
+ .thenReturn(
+ ClockConfig(
+ id = "DIGITAL_CLOCK_WEATHER",
+ name = "clock",
+ description = "clock",
+ )
+ )
+ whenever(splitShadeStateController.shouldUseSplitNotificationShade(any()))
+ .thenReturn(true)
+
+ reset(keyguardBlueprintRepository)
+ configurationFlow.tryEmit(Unit)
+ runCurrent()
+
+ verify(keyguardBlueprintRepository, never())
+ .applyBlueprint(SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID)
+ }
+ }
+
+ @Test
+ fun testDoesAppliesSplitShadeWeatherClockBlueprint() {
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ whenever(clockController.config)
+ .thenReturn(
+ ClockConfig(
+ id = "DIGITAL_CLOCK_WEATHER",
+ name = "clock",
+ description = "clock",
+ )
+ )
+ whenever(splitShadeStateController.shouldUseSplitNotificationShade(any()))
+ .thenReturn(true)
+
+ reset(keyguardBlueprintRepository)
+ configurationFlow.tryEmit(Unit)
+ runCurrent()
+
+ verify(keyguardBlueprintRepository)
+ .applyBlueprint(SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID)
+ }
+ }
+
+ @Test
+ fun testAppliesWeatherClockBlueprint() {
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+ whenever(clockController.config)
+ .thenReturn(
+ ClockConfig(
+ id = "DIGITAL_CLOCK_WEATHER",
+ name = "clock",
+ description = "clock",
+ )
+ )
+ whenever(splitShadeStateController.shouldUseSplitNotificationShade(any()))
+ .thenReturn(false)
+
+ reset(keyguardBlueprintRepository)
+ configurationFlow.tryEmit(Unit)
+ runCurrent()
+
+ verify(keyguardBlueprintRepository).applyBlueprint(WEATHER_CLOCK_BLUEPRINT_ID)
+ }
+ }
+
+ @Test
fun testRefreshBlueprint() {
underTest.refreshBlueprint()
verify(keyguardBlueprintRepository).refreshBlueprint()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 699284e..17d602c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -18,15 +18,16 @@
package com.android.systemui.keyguard.ui.view.layout.sections
import android.graphics.Point
+import android.testing.TestableLooper
import android.view.WindowManager
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.LockIconViewController
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
@@ -51,9 +52,9 @@
@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class DefaultDeviceEntrySectionTest : SysuiTestCase() {
- @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var authController: AuthController
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var windowManager: WindowManager
@Mock private lateinit var notificationPanelView: NotificationPanelView
@@ -61,11 +62,12 @@
@Mock private lateinit var lockIconViewController: LockIconViewController
@Mock private lateinit var falsingManager: FalsingManager
private lateinit var underTest: DefaultDeviceEntrySection
+ private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
-
+ fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
featureFlags =
@@ -73,7 +75,6 @@
underTest =
DefaultDeviceEntrySection(
TestScope().backgroundScope,
- keyguardUpdateMonitor,
authController,
windowManager,
context,
@@ -91,6 +92,7 @@
@Test
fun addViewsConditionally_migrateFlagOn() {
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
val constraintLayout = ConstraintLayout(context, null)
underTest.addViews(constraintLayout)
assertThat(constraintLayout.childCount).isGreaterThan(0)
@@ -102,7 +104,9 @@
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
val constraintLayout = ConstraintLayout(context, null)
underTest.addViews(constraintLayout)
- assertThat(constraintLayout.childCount).isGreaterThan(0)
+
+ // No views are added initially because fingerprint properties aren't initialized yet.
+ assertThat(constraintLayout.childCount).isEqualTo(0)
}
@Test
@@ -130,7 +134,7 @@
fun applyConstraints_udfps_refactor_on() {
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
val cs = ConstraintSet()
- underTest.applyConstraints(cs)
+ underTest.applyConstraintsAfterPropertiesInitialized(cs, false)
val constraint = cs.getConstraint(R.id.device_entry_icon_view)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
index 1205dce..711f90f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
@@ -23,7 +23,6 @@
import androidx.test.filters.SmallTest
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -40,7 +39,6 @@
class DefaultIndicationAreaSectionTest : SysuiTestCase() {
@Mock private lateinit var keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel
- @Mock private lateinit var aodAlphaViewModel: AodAlphaViewModel
@Mock private lateinit var indicationController: KeyguardIndicationController
private lateinit var underTest: DefaultIndicationAreaSection
@@ -52,7 +50,6 @@
DefaultIndicationAreaSection(
context,
keyguardIndicationAreaViewModel,
- aodAlphaViewModel,
indicationController,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
index 85291b8..45f49f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
@@ -24,8 +24,10 @@
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardViewController
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
import com.android.systemui.dreams.DreamOverlayStateController
@@ -508,6 +510,10 @@
@Test
fun testCommunalLocation() =
testScope.runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
runCurrent()
verify(mediaCarouselController)
@@ -533,6 +539,66 @@
}
@Test
+ fun testCommunalLocation_showsOverLockscreen() =
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ // Device is on lock screen.
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+ // UMO goes to communal even over the lock screen.
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ runCurrent()
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+ nullable(),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ }
+
+ @Test
+ fun testCommunalLocation_showsUntilQsExpands() =
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ // Device is on lock screen.
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ runCurrent()
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+ nullable(),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ clearInvocations(mediaCarouselController)
+
+ // Start opening the shade.
+ mediaHierarchyManager.qsExpansion = 0.1f
+ runCurrent()
+
+ // UMO goes to the shade instead.
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_QS),
+ any(MediaHostState::class.java),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ }
+
+ @Test
fun testQsExpandedChanged_noQqsMedia() {
// When we are looking at QQS with active media
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt
new file mode 100644
index 0000000..0c32470
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 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.screenshot
+
+import android.content.Intent
+import android.os.Process.myUserHandle
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.util.mockito.mock
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+class ActionIntentExecutorTest : SysuiTestCase() {
+
+ private val scheduler = TestCoroutineScheduler()
+ private val mainDispatcher = StandardTestDispatcher(scheduler)
+ private val testScope = TestScope(mainDispatcher)
+ private val testableContext = TestableContext(mContext)
+
+ private val activityManagerWrapper = mock<ActivityManagerWrapper>()
+ private val displayTracker = mock<DisplayTracker>()
+ private val keyguardController = mock<ScreenshotKeyguardController>()
+
+ private val actionIntentExecutor =
+ ActionIntentExecutor(
+ testableContext,
+ activityManagerWrapper,
+ testScope,
+ mainDispatcher,
+ displayTracker,
+ keyguardController,
+ )
+
+ @Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS)
+ fun launchIntent_callsCloseSystemWindows() =
+ testScope.runTest {
+ val intent = Intent(Intent.ACTION_EDIT).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
+ val userHandle = myUserHandle()
+
+ actionIntentExecutor.launchIntent(intent, null, userHandle, false)
+ scheduler.advanceUntilIdle()
+
+ verify(activityManagerWrapper)
+ .closeSystemWindows(CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index 032ec74..774aa51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -371,7 +371,6 @@
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
captor.value.onUserSwitching(newID, userSwitchingReply)
assertThat(callback.calledOnUserChanging).isEqualTo(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 1dc5f7d..665fc75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -26,17 +26,20 @@
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.fakeCommunalRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.testKosmos
@@ -45,6 +48,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.After
import org.junit.Assert.assertThrows
@@ -114,6 +118,14 @@
BOTTOM_SWIPE_REGION_WIDTH
)
+ // Make communal available so that communalInteractor.desiredScene accurately reflects
+ // scene changes instead of just returning Blank.
+ mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
+ with(kosmos.testScope) {
+ launch { kosmos.setCommunalAvailable(true) }
+ testScheduler.runCurrent()
+ }
+
initAndAttachContainerView()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinatorTest.kt
new file mode 100644
index 0000000..7daadb0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinatorTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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.statusbar.notification.collection.coordinator
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
+import com.android.systemui.statusbar.notification.collection.render.NotifRowController
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class RowAlertTimeCoordinatorTest : SysuiTestCase() {
+ private lateinit var coordinator: RowAlertTimeCoordinator
+ private lateinit var beforeFinalizeFilterListener: OnBeforeFinalizeFilterListener
+ private lateinit var afterRenderEntryListener: OnAfterRenderEntryListener
+
+ @Mock private lateinit var pipeline: NotifPipeline
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ coordinator = RowAlertTimeCoordinator()
+ coordinator.attach(pipeline)
+ beforeFinalizeFilterListener = withArgCaptor {
+ verify(pipeline).addOnBeforeFinalizeFilterListener(capture())
+ }
+ afterRenderEntryListener = withArgCaptor {
+ verify(pipeline).addOnAfterRenderEntryListener(capture())
+ }
+ }
+
+ @Test
+ fun testSetLastAudiblyAlerted() {
+ val entry1 = NotificationEntryBuilder().setLastAudiblyAlertedMs(10).build()
+ val entry2 = NotificationEntryBuilder().setLastAudiblyAlertedMs(20).build()
+ val summary = NotificationEntryBuilder().setLastAudiblyAlertedMs(5).build()
+ val child1 = NotificationEntryBuilder().setLastAudiblyAlertedMs(0).build()
+ val child2 = NotificationEntryBuilder().setLastAudiblyAlertedMs(8).build()
+ val group =
+ GroupEntryBuilder()
+ .setKey("group")
+ .setSummary(summary)
+ .addChild(child1)
+ .addChild(child2)
+ .build()
+
+ val entries = listOf(entry1, summary, child1, child2, entry2)
+
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry1, group, entry2))
+ val actualTimesSet =
+ entries.associateWith {
+ val rowController = mock<NotifRowController>()
+ afterRenderEntryListener.onAfterRenderEntry(it, rowController)
+ withArgCaptor<Long> {
+ verify(rowController).setLastAudibleMs(capture())
+ verifyNoMoreInteractions(rowController)
+ }
+ }
+ val expectedTimesSet =
+ mapOf(
+ entry1 to 10L,
+ entry2 to 20L,
+ summary to 8L,
+ child1 to 0L,
+ child2 to 8L,
+ )
+ assertThat(actualTimesSet).containsExactlyEntriesIn(expectedTimesSet)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
index fa669fc..a66f8ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
@@ -76,7 +76,7 @@
verify(pipeline).addOnAfterRenderEntryListener(capture())
}
whenever(assistantFeedbackController.getFeedbackIcon(any())).thenReturn(FeedbackIcon(1, 2))
- entry1 = NotificationEntryBuilder().setSection(section1).setLastAudiblyAlertedMs(17).build()
+ entry1 = NotificationEntryBuilder().setSection(section1).build()
entry2 = NotificationEntryBuilder().setSection(section2).build()
}
@@ -103,12 +103,6 @@
}
@Test
- fun testSetLastAudiblyAlerted() {
- afterRenderEntryListener.onAfterRenderEntry(entry1, controller1)
- verify(controller1).setLastAudibleMs(eq(17.toLong()))
- }
-
- @Test
fun testSetFeedbackIcon() {
afterRenderEntryListener.onAfterRenderEntry(entry1, controller1)
verify(controller1).setFeedbackIcon(eq(FeedbackIcon(1, 2)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index 8b99811..a12806b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -167,6 +167,20 @@
}
@Test
+ fun testUpdateIcons_sensitiveImportantConversation() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+ entry?.setSensitive(true, true)
+ entry?.channel?.isImportantConversation = true
+ entry?.let { iconManager.createIcons(it) }
+ // Updating the icons after creation shouldn't break anything
+ entry?.let { iconManager.updateIcons(it) }
+ assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+ assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+ assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+ }
+
+ @Test
fun testUpdateIcons_sensitivityChange() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
index 7592356..933b5b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -24,6 +24,7 @@
import androidx.test.filters.SmallTest
import com.android.server.notification.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.settings.FakeGlobalSettings
import com.android.systemui.util.time.FakeSystemClock
@@ -38,6 +39,8 @@
@RunWith(AndroidTestingRunner::class)
@DisableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase() {
+ private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer())
+
@Mock private lateinit var handler: Handler
@Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var mediaProjectionManager: MediaProjectionManager
@@ -54,7 +57,8 @@
mediaProjectionManager,
activityManager,
handler,
- FakeExecutor(FakeSystemClock())
+ FakeExecutor(FakeSystemClock()),
+ logger
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index a2af38f..4b4e315 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -34,6 +34,7 @@
import androidx.test.filters.SmallTest
import com.android.server.notification.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
@@ -63,6 +64,8 @@
@RunWithLooper
@EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
+ private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer())
+
@Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var mediaProjectionManager: MediaProjectionManager
@Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
@@ -93,7 +96,8 @@
mediaProjectionManager,
activityManager,
mockExecutorHandler(executor),
- executor
+ executor,
+ logger
)
// Process pending work (getting global setting and list of exemptions)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
index 203096a..08b49f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
@@ -166,4 +166,28 @@
assertThat(config.color).isEqualTo(expectedColor)
}
}
+
+ @Test
+ fun play_initializesShader() {
+ val expectedNoiseOffset = floatArrayOf(0.1f, 0.2f, 0.3f)
+ val config =
+ TurbulenceNoiseAnimationConfig(
+ noiseOffsetX = expectedNoiseOffset[0],
+ noiseOffsetY = expectedNoiseOffset[1],
+ noiseOffsetZ = expectedNoiseOffset[2]
+ )
+ val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+ val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
+
+ fakeExecutor.execute {
+ turbulenceNoiseController.play(SIMPLEX_NOISE, config)
+
+ assertThat(turbulenceNoiseView.noiseConfig).isNotNull()
+ val shader = turbulenceNoiseView.turbulenceNoiseShader!!
+ assertThat(shader).isNotNull()
+ assertThat(shader.noiseOffsetX).isEqualTo(expectedNoiseOffset[0])
+ assertThat(shader.noiseOffsetY).isEqualTo(expectedNoiseOffset[1])
+ assertThat(shader.noiseOffsetZ).isEqualTo(expectedNoiseOffset[2])
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index b25ac24..a930860 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -26,6 +26,7 @@
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.google.common.truth.Truth.assertThat;
@@ -46,6 +47,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -73,6 +75,8 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.service.dreams.IDreamManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.ZenModeConfig;
@@ -161,6 +165,7 @@
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
@@ -257,6 +262,8 @@
private NotificationShadeWindowView mNotificationShadeWindowView;
@Mock
private AuthController mAuthController;
+ @Mock
+ private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
private SysUiState mSysUiState;
private boolean mSysUiStateBubblesExpanded;
@@ -272,6 +279,8 @@
private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverArgumentCaptor;
@Captor
private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor;
+ @Captor
+ private ArgumentCaptor<Runnable> mSensitiveStateChangedListener;
private BubblesManager mBubblesManager;
private TestableBubbleController mBubbleController;
@@ -594,6 +603,7 @@
interruptionDecisionProvider,
mZenModeController,
mLockscreenUserManager,
+ mSensitiveNotificationProtectionController,
mCommonNotifCollection,
mNotifPipeline,
mSysUiState,
@@ -2203,6 +2213,33 @@
assertThat(mBubbleController.getLayerView().isExpanded()).isFalse();
}
+ @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Test
+ public void doesNotRegisterSensitiveStateListener() {
+ verifyZeroInteractions(mSensitiveNotificationProtectionController);
+ }
+
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Test
+ public void registerSensitiveStateListener() {
+ verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
+ }
+
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Test
+ public void onSensitiveNotificationProtectionStateChanged() {
+ verify(mSensitiveNotificationProtectionController, atLeastOnce())
+ .registerSensitiveStateListener(mSensitiveStateChangedListener.capture());
+
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+ mSensitiveStateChangedListener.getValue().run();
+ verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(true);
+
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+ mSensitiveStateChangedListener.getValue().run();
+ verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(false);
+ }
+
/** Creates a bubble using the userId and package. */
private Bubble createBubble(int userId, String pkg) {
final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 005cac4..f95c721 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -28,6 +28,7 @@
@SysUISingleton
class FakeFingerprintPropertyRepository @Inject constructor() : FingerprintPropertyRepository {
+ override val propertiesInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
override val sensorId = _sensorId.asStateFlow()
@@ -50,6 +51,7 @@
sensorType: FingerprintSensorType,
sensorLocations: Map<String, SensorLocationInternal>
) {
+ propertiesInitialized.value = true
_sensorId.value = sensorId
_strength.value = strength
_sensorType.value = sensorType
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 6af08d3..6ac702e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -31,6 +31,7 @@
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
+import com.android.systemui.settings.userTracker
import com.android.systemui.smartspace.data.repository.smartspaceRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
@@ -46,6 +47,7 @@
appWidgetHost = mock(),
keyguardInteractor = keyguardInteractor,
editWidgetsActivityStarter = editWidgetsActivityStarter,
+ userTracker = userTracker,
logBuffer = logcatLogBuffer("CommunalInteractor"),
tableLogBuffer = mock(),
communalSettingsInteractor = communalSettingsInteractor,
@@ -60,9 +62,11 @@
fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, available)
if (available) {
fakeUserRepository.asMainUser()
- with(fakeKeyguardRepository) {
- setIsEncryptedOrLockdown(false)
- setKeyguardShowing(true)
- }
+ } else {
+ fakeUserRepository.asDefaultUser()
+ }
+ with(fakeKeyguardRepository) {
+ setIsEncryptedOrLockdown(!available)
+ setKeyguardShowing(available)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
index d9a3192..8b0bba1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
@@ -29,5 +29,6 @@
applicationScope = applicationCoroutineScope,
context = applicationContext,
splitShadeStateController = splitShadeStateController,
+ clockInteractor = keyguardClockInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 73fd999..9fe6571 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
@@ -50,5 +51,6 @@
keyguardViewController = { statusBarKeyguardViewManager },
deviceEntryInteractor = deviceEntryInteractor,
deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+ fingerprintPropertyInteractor = fingerprintPropertyInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 931a59d..3e9ae4d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -119,6 +119,13 @@
yield()
}
+ /** Resets the current user to the default of [DEFAULT_SELECTED_USER_INFO]. */
+ suspend fun asDefaultUser(): UserInfo {
+ setUserInfos(listOf(DEFAULT_SELECTED_USER_INFO))
+ setSelectedUserInfo(DEFAULT_SELECTED_USER_INFO)
+ return DEFAULT_SELECTED_USER_INFO
+ }
+
/** Makes the current user [MAIN_USER]. */
suspend fun asMainUser(): UserInfo {
setUserInfos(listOf(MAIN_USER))
diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java
index c734680..ffc80ee 100644
--- a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java
@@ -16,6 +16,8 @@
package com.android.server.autofill.ui;
+import static android.service.autofill.FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE;
+
import static com.android.server.autofill.Helper.sVerbose;
import android.annotation.NonNull;
@@ -24,6 +26,7 @@
import android.content.IntentSender;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
+import android.service.autofill.Flags;
import android.service.autofill.InlinePresentation;
import android.text.TextUtils;
import android.util.Pair;
@@ -141,10 +144,12 @@
return new InlineFillUi(inlineFillUiInfo, inlineAuthentication,
maxInputLengthForAutofill);
} else if (response.getDatasets() != null) {
+ boolean ignoreHostSpec = Flags.autofillCredmanIntegration() && (
+ (response.getFlags() & FLAG_CREDENTIAL_MANAGER_RESPONSE) != 0);
SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions =
InlineSuggestionFactory.createInlineSuggestions(inlineFillUiInfo,
InlineSuggestionInfo.SOURCE_AUTOFILL, response.getDatasets(),
- uiCallback);
+ uiCallback, ignoreHostSpec);
return new InlineFillUi(inlineFillUiInfo, inlineSuggestions,
maxInputLengthForAutofill);
}
@@ -160,7 +165,8 @@
@NonNull InlineSuggestionUiCallback uiCallback) {
SparseArray<Pair<Dataset, InlineSuggestion>> inlineSuggestions =
InlineSuggestionFactory.createInlineSuggestions(inlineFillUiInfo,
- InlineSuggestionInfo.SOURCE_PLATFORM, datasets, uiCallback);
+ InlineSuggestionInfo.SOURCE_PLATFORM, datasets,
+ uiCallback, /* ignoreHostSpec= */ false);
return new InlineFillUi(inlineFillUiInfo, inlineSuggestions);
}
@@ -254,7 +260,7 @@
if (!TextUtils.isEmpty(mFilterText) && mFilterText.length() > mMaxInputLengthForAutofill) {
if (sVerbose) {
Slog.v(TAG, "Not showing inline suggestion when user entered more than "
- + mMaxInputLengthForAutofill + " characters");
+ + mMaxInputLengthForAutofill + " characters");
}
return new InlineSuggestionsResponse(inlineSuggestions);
}
diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java
index 52109ba..d3b9501 100644
--- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java
+++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java
@@ -16,6 +16,7 @@
package com.android.server.autofill.ui;
+import static android.service.autofill.FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE;
import static android.view.inputmethod.InlineSuggestionInfo.TYPE_SUGGESTION;
import static com.android.server.autofill.Helper.sDebug;
@@ -25,6 +26,7 @@
import android.content.IntentSender;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
+import android.service.autofill.Flags;
import android.service.autofill.InlinePresentation;
import android.util.Pair;
import android.util.Slog;
@@ -47,11 +49,14 @@
@NonNull InlineFillUi.InlineSuggestionUiCallback uiCallback) {
InlinePresentation inlineAuthentication = response.getInlinePresentation();
final int requestId = response.getRequestId();
+ boolean ignoreHostSpec = Flags.autofillCredmanIntegration() && (
+ (response.getFlags() & FLAG_CREDENTIAL_MANAGER_RESPONSE) != 0);
return createInlineSuggestion(inlineFillUiInfo, InlineSuggestionInfo.SOURCE_AUTOFILL,
InlineSuggestionInfo.TYPE_ACTION, () -> uiCallback.authenticate(requestId,
AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED),
- mergedInlinePresentation(inlineFillUiInfo.mInlineRequest, 0, inlineAuthentication),
+ mergedInlinePresentation(inlineFillUiInfo.mInlineRequest, 0, inlineAuthentication,
+ ignoreHostSpec),
createInlineSuggestionTooltip(inlineFillUiInfo.mInlineRequest,
inlineFillUiInfo, InlineSuggestionInfo.SOURCE_AUTOFILL,
response.getInlineTooltipPresentation()),
@@ -67,7 +72,7 @@
@NonNull InlineFillUi.InlineFillUiInfo inlineFillUiInfo,
@NonNull @InlineSuggestionInfo.Source String suggestionSource,
@NonNull List<Dataset> datasets,
- @NonNull InlineFillUi.InlineSuggestionUiCallback uiCallback) {
+ @NonNull InlineFillUi.InlineSuggestionUiCallback uiCallback, boolean ignoreHostSpec) {
if (sDebug) Slog.d(TAG, "createInlineSuggestions(source=" + suggestionSource + ") called");
final InlineSuggestionsRequest request = inlineFillUiInfo.mInlineRequest;
@@ -107,7 +112,8 @@
InlineSuggestion inlineSuggestion = createInlineSuggestion(
inlineFillUiInfo, suggestionSource, suggestionType,
() -> uiCallback.autofill(dataset, index),
- mergedInlinePresentation(request, datasetIndex, inlinePresentation),
+ mergedInlinePresentation(request, datasetIndex, inlinePresentation,
+ ignoreHostSpec),
inlineSuggestionTooltip,
uiCallback);
response.append(datasetIndex, Pair.create(dataset, inlineSuggestion));
@@ -141,16 +147,18 @@
*/
private static InlinePresentation mergedInlinePresentation(
@NonNull InlineSuggestionsRequest request,
- int index, @NonNull InlinePresentation inlinePresentation) {
+ int index, @NonNull InlinePresentation inlinePresentation, boolean ignoreHostSpec) {
final List<InlinePresentationSpec> specs = request.getInlinePresentationSpecs();
if (specs.isEmpty()) {
return inlinePresentation;
}
InlinePresentationSpec specFromHost = specs.get(Math.min(specs.size() - 1, index));
+ InlinePresentationSpec specToUse =
+ ignoreHostSpec ? inlinePresentation.getInlinePresentationSpec() : specFromHost;
InlinePresentationSpec mergedInlinePresentation = new InlinePresentationSpec.Builder(
inlinePresentation.getInlinePresentationSpec().getMinSize(),
inlinePresentation.getInlinePresentationSpec().getMaxSize()).setStyle(
- specFromHost.getStyle()).build();
+ specToUse.getStyle()).build();
return new InlinePresentation(inlinePresentation.getSlice(), mergedInlinePresentation,
inlinePresentation.isPinned());
@@ -211,7 +219,7 @@
inlineFillUiInfo, tooltipInline, () -> { /* no operation */ }, uiCallback);
final InlineSuggestionInfo tooltipInlineSuggestionInfo = new InlineSuggestionInfo(
mergedSpec, suggestionSource, /* autofillHints */ null, TYPE_SUGGESTION,
- /* pinned */ false, /* tooltip */ null);
+ /* pinned */ false, /* tooltip */ null);
return new InlineSuggestion(tooltipInlineSuggestionInfo, tooltipContentProvider);
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 7940ca6..94aab75 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -41,16 +41,18 @@
genrule {
name: "services.core.protologsrc",
srcs: [
+ ":protolog-impl",
":protolog-groups",
":services.core-sources-am-wm",
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
- "--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
- "--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
+ "--viewer-config-file-path /etc/core.protolog.pb " +
+ "--legacy-viewer-config-file-path /system/etc/protolog.conf.json.gz " +
+ "--legacy-output-file-path /data/misc/wmtrace/wm_log.winscope " +
"--output-srcjar $(out) " +
"$(locations :services.core-sources-am-wm)",
out: ["services.core.protolog.srcjar"],
@@ -67,25 +69,43 @@
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
- "--viewer-conf $(out) " +
+ "--viewer-config-type json " +
+ "--viewer-config $(out) " +
"$(locations :services.core-sources-am-wm)",
out: ["services.core.protolog.json"],
}
genrule {
- name: "checked-protolog.json",
+ name: "gen-core.protolog.pb",
srcs: [
- ":generate-protolog.json",
- ":services.core.protolog.json",
+ ":protolog-groups",
+ ":services.core-sources-am-wm",
],
- cmd: "cp $(location :generate-protolog.json) $(out) && " +
- "{ ! (diff $(out) $(location :services.core.protolog.json) | grep -q '^<') || " +
+ tools: ["protologtool"],
+ cmd: "$(location protologtool) generate-viewer-config " +
+ "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+ "--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
+ "--loggroups-jar $(location :protolog-groups) " +
+ "--viewer-config-type proto " +
+ "--viewer-config $(out) " +
+ "$(locations :services.core-sources-am-wm)",
+ out: ["core.protolog.pb"],
+}
+
+genrule {
+ name: "checked-core.protolog.pb",
+ srcs: [
+ ":gen-core.protolog.pb",
+ ":file-core.protolog.pb",
+ ],
+ cmd: "cp $(location :gen-core.protolog.pb) $(out) && " +
+ "{ ! (diff $(out) $(location :file-core.protolog.pb) | grep -q '^<') || " +
"{ echo -e '\\n\\n################################################################\\n#\\n" +
"# ERROR: ProtoLog viewer config is stale. To update it, run:\\n#\\n" +
- "# cp $${ANDROID_BUILD_TOP}/$(location :generate-protolog.json) " +
- "$${ANDROID_BUILD_TOP}/$(location :services.core.protolog.json)\\n#\\n" +
+ "# cp $${ANDROID_BUILD_TOP}/$(location :gen-core.protolog.pb) " +
+ "$${ANDROID_BUILD_TOP}/$(location :file-core.protolog.pb)\\n#\\n" +
"################################################################\\n\\n' >&2 && false; } }",
- out: ["services.core.protolog.json"],
+ out: ["core.protolog.pb"],
}
genrule {
@@ -157,7 +177,7 @@
required: [
"default_television.xml",
"gps_debug.conf",
- "protolog.conf.json.gz",
+ "core.protolog.pb",
],
static_libs: [
@@ -258,14 +278,7 @@
src: "java/com/android/server/location/gnss/gps_debug.conf",
}
-genrule {
- name: "services.core.json.gz",
- srcs: [":checked-protolog.json"],
- out: ["services.core.protolog.json.gz"],
- cmd: "gzip -c < $(in) > $(out)",
-}
-
prebuilt_etc {
- name: "protolog.conf.json.gz",
- src: ":services.core.json.gz",
+ name: "core.protolog.pb",
+ src: ":checked-core.protolog.pb",
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 767f54d..966fe5b 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -120,6 +120,7 @@
static final String[] sDeviceConfigAconfigScopes = new String[] {
"accessibility",
"android_core_networking",
+ "android_stylus",
"aoc",
"app_widgets",
"arc_next",
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 7b18fb6..32e89bc 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -86,7 +86,9 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
import android.view.Display;
+import android.view.DisplayInfo;
import android.view.IInputFilter;
import android.view.IInputFilterHost;
import android.view.IInputMonitorHost;
@@ -415,11 +417,15 @@
boolean mUseLargePointerIcons = false;
@GuardedBy("mLoadedPointerIconsByDisplayAndType")
final SparseArray<Context> mDisplayContexts = new SparseArray<>();
+ @GuardedBy("mLoadedPointerIconsByDisplayAndType")
+ final SparseIntArray mDisplayDensities = new SparseIntArray();
final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
-
+ synchronized (mLoadedPointerIconsByDisplayAndType) {
+ updateDisplayDensity(displayId);
+ }
}
@Override
@@ -427,14 +433,20 @@
synchronized (mLoadedPointerIconsByDisplayAndType) {
mLoadedPointerIconsByDisplayAndType.remove(displayId);
mDisplayContexts.remove(displayId);
+ mDisplayDensities.delete(displayId);
}
}
@Override
public void onDisplayChanged(int displayId) {
synchronized (mLoadedPointerIconsByDisplayAndType) {
- // The display density could have changed, so force all cached pointer icons to be
+ if (!updateDisplayDensity(displayId)) {
+ return;
+ }
+ // The display density changed, so force all cached pointer icons to be
// reloaded for the display.
+ Slog.i(TAG,
+ "Reloading pointer icons due to density change on display: " + displayId);
var iconsByType = mLoadedPointerIconsByDisplayAndType.get(displayId);
if (iconsByType == null) {
return;
@@ -444,6 +456,26 @@
}
mNative.reloadPointerIcons();
}
+
+ // Updates the cached display density for the given displayId, and returns true if
+ // the cached density changed.
+ @GuardedBy("mLoadedPointerIconsByDisplayAndType")
+ private boolean updateDisplayDensity(int displayId) {
+ final DisplayManager displayManager = Objects.requireNonNull(
+ mContext.getSystemService(DisplayManager.class));
+ final Display display = displayManager.getDisplay(displayId);
+ if (display == null) {
+ return false;
+ }
+ DisplayInfo info = new DisplayInfo();
+ display.getDisplayInfo(info);
+ final int oldDensity = mDisplayDensities.get(displayId, 0 /* default */);
+ if (oldDensity == info.logicalDensityDpi) {
+ return false;
+ }
+ mDisplayDensities.put(displayId, info.logicalDensityDpi);
+ return true;
+ }
};
/** Point of injection for test dependencies. */
@@ -613,9 +645,13 @@
mWiredAccessoryCallbacks.systemReady();
}
- Objects.requireNonNull(
- mContext.getSystemService(DisplayManager.class)).registerDisplayListener(
- mDisplayListener, mHandler);
+ final DisplayManager displayManager = Objects.requireNonNull(
+ mContext.getSystemService(DisplayManager.class));
+ displayManager.registerDisplayListener(mDisplayListener, mHandler);
+ final Display[] displays = displayManager.getDisplays();
+ for (int i = 0; i < displays.length; i++) {
+ mDisplayListener.onDisplayAdded(displays[i].getDisplayId());
+ }
mKeyboardLayoutManager.systemRunning();
mBatteryController.systemRunning();
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 75be068..a608049 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -463,11 +463,13 @@
com.android.internal.R.bool.config_useGnssHardwareProvider);
AbstractLocationProvider gnssProvider = null;
if (!useGnssHardwareProvider) {
+ // TODO: Create a separate config_enableGnssLocationOverlay config resource
+ // if we want to selectively enable a GNSS overlay but disable a fused overlay.
gnssProvider = ProxyLocationProvider.create(
mContext,
GPS_PROVIDER,
ACTION_GNSS_PROVIDER,
- com.android.internal.R.bool.config_useGnssHardwareProvider,
+ com.android.internal.R.bool.config_enableFusedLocationOverlay,
com.android.internal.R.string.config_gnssLocationProviderPackageName);
}
if (gnssProvider == null) {
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index ecb4fcc..40e538b 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -1618,6 +1618,17 @@
*/
@SuppressLint("AndroidFrameworkRequiresPermission")
public boolean isVisibleToCaller() {
+ // Anything sharing the system's UID can view all providers
+ if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+ return true;
+ }
+
+ // If an app mocked this provider, anybody can access it (the goal is
+ // to behave as if this provider didn't naturally exist).
+ if (mProvider.isMock()) {
+ return true;
+ }
+
for (String permission : mRequiredPermissions) {
if (mContext.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
return false;
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index b5c51af..796d8d7 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -340,6 +340,8 @@
static final String TAG = NetworkPolicyLogger.TAG;
private static final boolean LOGD = NetworkPolicyLogger.LOGD;
private static final boolean LOGV = NetworkPolicyLogger.LOGV;
+ // TODO: b/304347838 - Remove once the feature is in staging.
+ private static final boolean ALWAYS_RESTRICT_BACKGROUND_NETWORK = false;
/**
* No opportunistic quota could be calculated from user data plan or data settings.
@@ -1061,7 +1063,8 @@
}
// The flag is boot-stable.
- mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove();
+ mBackgroundNetworkRestricted = ALWAYS_RESTRICT_BACKGROUND_NETWORK
+ && Flags.networkBlockedForTopSleepingAndAbove();
if (mBackgroundNetworkRestricted) {
// Firewall rules and UidBlockedState will get updated in
// updateRulesForGlobalChangeAL below.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index fc7b873..3a7ac0b 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -8206,7 +8206,7 @@
try {
return mTelecomManager.isInManagedCall()
|| mTelecomManager.isInSelfManagedCall(pkg,
- UserHandle.getUserHandleForUid(uid), /* hasCrossUserAccess */ true);
+ /* hasCrossUserAccess */ true);
} catch (IllegalStateException ise) {
// Telecom is not ready (this is likely early boot), so there are no calls.
return false;
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java
index b22e37b..c8cb92b 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java
@@ -167,6 +167,9 @@
/** Config flag to track if battery saver's sticky behaviour is disabled. */
private final boolean mBatterySaverStickyBehaviourDisabled;
+ /** Config flag to track if "Battery Saver turned off" notification is enabled. */
+ private final boolean mBatterySaverTurnedOffNotificationEnabled;
+
/**
* Whether or not to end sticky battery saver upon reaching a level specified by
* {@link #mSettingBatterySaverStickyAutoDisableThreshold}.
@@ -250,6 +253,8 @@
mBatterySaverStickyBehaviourDisabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_batterySaverStickyBehaviourDisabled);
+ mBatterySaverTurnedOffNotificationEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_batterySaverTurnedOffNotificationEnabled);
mDynamicPowerSavingsDefaultDisableThreshold = mContext.getResources().getInteger(
com.android.internal.R.integer.config_dynamicPowerSavingsDefaultDisableThreshold);
}
@@ -858,6 +863,9 @@
@VisibleForTesting
void triggerStickyDisabledNotification() {
+ if (!mBatterySaverTurnedOffNotificationEnabled) {
+ return;
+ }
// The current lock is the PowerManager lock, which sits very low in the service lock
// hierarchy. We shouldn't call out to NotificationManager with the PowerManager lock.
runOnBgThread(() -> {
@@ -997,6 +1005,8 @@
ipw.println(mSettingBatterySaverTriggerThreshold);
ipw.print("mBatterySaverStickyBehaviourDisabled=");
ipw.println(mBatterySaverStickyBehaviourDisabled);
+ ipw.print("mBatterySaverTurnedOffNotificationEnabled=");
+ ipw.println(mBatterySaverTurnedOffNotificationEnabled);
ipw.print("mDynamicPowerSavingsDefaultDisableThreshold=");
ipw.println(mDynamicPowerSavingsDefaultDisableThreshold);
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 bf06c2a..8e3c6ac 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -1591,32 +1591,76 @@
@Override
public WakeLockStats getWakeLockStats() {
final long realtimeMs = mClock.elapsedRealtime();
- final long realtimeUs = realtimeMs * 1000;
List<WakeLockStats.WakeLock> uidWakeLockStats = new ArrayList<>();
+ List<WakeLockStats.WakeLock> uidAggregatedWakeLockStats = new ArrayList<>();
for (int i = mUidStats.size() - 1; i >= 0; i--) {
final Uid uid = mUidStats.valueAt(i);
+
+ // Converts unaggregated wakelocks.
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats =
uid.mWakelockStats.getMap();
for (int j = wakelockStats.size() - 1; j >= 0; j--) {
final String name = wakelockStats.keyAt(j);
final Uid.Wakelock wakelock = (Uid.Wakelock) wakelockStats.valueAt(j);
- final DualTimer timer = wakelock.mTimerPartial;
- if (timer != null) {
- final long totalTimeLockHeldMs =
- timer.getTotalTimeLocked(realtimeUs, STATS_SINCE_CHARGED) / 1000;
- if (totalTimeLockHeldMs != 0) {
- uidWakeLockStats.add(
- new WakeLockStats.WakeLock(uid.getUid(), name,
- timer.getCountLocked(STATS_SINCE_CHARGED),
- totalTimeLockHeldMs,
- timer.isRunningLocked()
- ? timer.getCurrentDurationMsLocked(realtimeMs)
- : 0));
- }
+ final WakeLockStats.WakeLock wakeLockItem =
+ createWakeLock(uid, name, /* isAggregated= */ false, wakelock.mTimerPartial,
+ realtimeMs);
+ if (wakeLockItem != null) {
+ uidWakeLockStats.add(wakeLockItem);
}
}
+
+ // Converts aggregated wakelocks.
+ final WakeLockStats.WakeLock aggregatedWakeLockItem =
+ createWakeLock(
+ uid,
+ WakeLockStats.WakeLock.NAME_AGGREGATED,
+ /* isAggregated= */ true,
+ uid.mAggregatedPartialWakelockTimer,
+ realtimeMs);
+ if (aggregatedWakeLockItem != null) {
+ uidAggregatedWakeLockStats.add(aggregatedWakeLockItem);
+ }
}
- return new WakeLockStats(uidWakeLockStats);
+ return new WakeLockStats(uidWakeLockStats, uidAggregatedWakeLockStats);
+ }
+
+ // Returns a valid {@code WakeLockStats.WakeLock} or null.
+ private WakeLockStats.WakeLock createWakeLock(
+ Uid uid, String name, boolean isAggregated, DualTimer timer, final long realtimeMs) {
+ if (timer == null) {
+ return null;
+ }
+ // Uses the primary timer for total wakelock data and used the sub timer for background
+ // wakelock data.
+ final WakeLockStats.WakeLockData totalWakeLockData = createWakeLockData(timer, realtimeMs);
+ final WakeLockStats.WakeLockData backgroundWakeLockData =
+ createWakeLockData(timer.getSubTimer(), realtimeMs);
+
+ return WakeLockStats.WakeLock.isDataValid(totalWakeLockData, backgroundWakeLockData)
+ ? new WakeLockStats.WakeLock(
+ uid.getUid(),
+ name,
+ isAggregated,
+ totalWakeLockData,
+ backgroundWakeLockData) : null;
+ }
+
+ @NonNull
+ private WakeLockStats.WakeLockData createWakeLockData(
+ DurationTimer timer, final long realtimeMs) {
+ if (timer == null) {
+ return WakeLockStats.WakeLockData.EMPTY;
+ }
+ final long totalTimeLockHeldMs =
+ timer.getTotalTimeLocked(realtimeMs * 1000, STATS_SINCE_CHARGED) / 1000;
+ if (totalTimeLockHeldMs == 0) {
+ return WakeLockStats.WakeLockData.EMPTY;
+ }
+ return new WakeLockStats.WakeLockData(
+ timer.getCountLocked(STATS_SINCE_CHARGED),
+ totalTimeLockHeldMs,
+ timer.isRunningLocked() ? timer.getCurrentDurationMsLocked(realtimeMs) : 0);
}
@Override
@@ -1817,9 +1861,8 @@
if (historyDirectory == null) {
mCheckinFile = null;
mStatsFile = null;
- mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_FILES,
- mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock,
- traceDelegate, eventLogger);
+ mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_BUFFER,
+ mStepDetailsCalculator, mClock, mMonotonicClock, traceDelegate, eventLogger);
} else {
mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
@@ -10962,8 +11005,8 @@
mStatsFile = null;
mCheckinFile = null;
mDailyFile = null;
- mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_FILES,
- mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock);
+ mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_BUFFER,
+ mStepDetailsCalculator, mClock, mMonotonicClock);
} else {
mStatsFile = new AtomicFile(new File(systemDir, "batterystats.bin"));
mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin"));
diff --git a/services/core/java/com/android/server/search/SearchManagerService.java b/services/core/java/com/android/server/search/SearchManagerService.java
index 7091c47..ecfc040 100644
--- a/services/core/java/com/android/server/search/SearchManagerService.java
+++ b/services/core/java/com/android/server/search/SearchManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.search;
import android.annotation.NonNull;
+import android.annotation.UserIdInt;
import android.app.ISearchManager;
import android.app.SearchManager;
import android.app.SearchableInfo;
@@ -24,6 +25,7 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.ContentObserver;
import android.os.Binder;
@@ -32,6 +34,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -47,6 +50,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -71,11 +75,6 @@
}
@Override
- public void onUserUnlocking(@NonNull TargetUser user) {
- mService.mHandler.post(() -> mService.onUnlockUser(user.getUserIdentifier()));
- }
-
- @Override
public void onUserStopped(@NonNull TargetUser user) {
mService.onCleanupUser(user.getUserIdentifier());
}
@@ -102,10 +101,6 @@
}
private Searchables getSearchables(int userId) {
- return getSearchables(userId, false);
- }
-
- private Searchables getSearchables(int userId, boolean forceUpdate) {
final long token = Binder.clearCallingIdentity();
try {
final UserManager um = mContext.getSystemService(UserManager.class);
@@ -122,21 +117,11 @@
Searchables searchables = mSearchables.get(userId);
if (searchables == null) {
searchables = new Searchables(mContext, userId);
- searchables.updateSearchableList();
- mSearchables.append(userId, searchables);
- } else if (forceUpdate) {
- searchables.updateSearchableList();
+ mSearchables.put(userId, searchables);
}
- return searchables;
- }
- }
- private void onUnlockUser(int userId) {
- try {
- getSearchables(userId, true);
- } catch (IllegalStateException ignored) {
- // We're just trying to warm a cache, so we don't mind if the user
- // was stopped or destroyed before we got here.
+ searchables.updateSearchableListIfNeeded();
+ return searchables;
}
}
@@ -150,28 +135,110 @@
* Refreshes the "searchables" list when packages are added/removed.
*/
class MyPackageMonitor extends PackageMonitor {
+ /**
+ * Packages that are appeared, disappeared, or modified for whatever reason.
+ */
+ private final ArrayList<String> mChangedPackages = new ArrayList<>();
+
+ /**
+ * {@code true} if one or more packages that contain {@link SearchableInfo} appeared.
+ */
+ private boolean mSearchablePackageAppeared = false;
@Override
- public void onSomePackagesChanged() {
- updateSearchables();
+ public void onBeginPackageChanges() {
+ clearPackageChangeState();
}
@Override
- public void onPackageModified(String pkg) {
- updateSearchables();
+ public void onPackageAppeared(String packageName, int reason) {
+ if (!mSearchablePackageAppeared) {
+ // Check if the new appeared package contains SearchableInfo.
+ mSearchablePackageAppeared =
+ hasSearchableForPackage(packageName, getChangingUserId());
+ }
+ mChangedPackages.add(packageName);
}
- private void updateSearchables() {
- final int changingUserId = getChangingUserId();
+ @Override
+ public void onPackageDisappeared(String packageName, int reason) {
+ mChangedPackages.add(packageName);
+ }
+
+ @Override
+ public void onPackageModified(String packageName) {
+ mChangedPackages.add(packageName);
+ }
+
+ @Override
+ public void onFinishPackageChanges() {
+ onFinishPackageChangesInternal();
+ clearPackageChangeState();
+ }
+
+ private void clearPackageChangeState() {
+ mChangedPackages.clear();
+ mSearchablePackageAppeared = false;
+ }
+
+ private boolean hasSearchableForPackage(String packageName, int userId) {
+ final List<ResolveInfo> searchList = querySearchableActivities(mContext,
+ new Intent(Intent.ACTION_SEARCH).setPackage(packageName), userId);
+ if (!searchList.isEmpty()) {
+ return true;
+ }
+
+ final List<ResolveInfo> webSearchList = querySearchableActivities(mContext,
+ new Intent(Intent.ACTION_WEB_SEARCH).setPackage(packageName), userId);
+ if (!webSearchList.isEmpty()) {
+ return true;
+ }
+
+ final List<ResolveInfo> globalSearchList = querySearchableActivities(mContext,
+ new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH).setPackage(packageName),
+ userId);
+ return !globalSearchList.isEmpty();
+ }
+
+ private boolean shouldRebuildSearchableList(@UserIdInt int changingUserId) {
+ // This method is guaranteed to be called only on getRegisteredHandler()
+ if (mSearchablePackageAppeared) {
+ return true;
+ }
+
+ ArraySet<String> knownSearchablePackageNames = new ArraySet<>();
synchronized (mSearchables) {
- // Update list of searchable activities
- for (int i = 0; i < mSearchables.size(); i++) {
- if (changingUserId == mSearchables.keyAt(i)) {
- mSearchables.valueAt(i).updateSearchableList();
- break;
- }
+ Searchables searchables = mSearchables.get(changingUserId);
+ if (searchables != null) {
+ knownSearchablePackageNames = searchables.getKnownSearchablePackageNames();
}
}
+
+ final int numOfPackages = mChangedPackages.size();
+ for (int i = 0; i < numOfPackages; i++) {
+ final String packageName = mChangedPackages.get(i);
+ if (knownSearchablePackageNames.contains(packageName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void onFinishPackageChangesInternal() {
+ final int changingUserId = getChangingUserId();
+ if (!shouldRebuildSearchableList(changingUserId)) {
+ return;
+ }
+
+ synchronized (mSearchables) {
+ // Invalidate the searchable list.
+ Searchables searchables = mSearchables.get(changingUserId);
+ if (searchables != null) {
+ searchables.invalidateSearchableList();
+ }
+ }
+
// Inform all listeners that the list of searchables has been updated.
Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
@@ -180,6 +247,17 @@
}
}
+ @NonNull
+ static List<ResolveInfo> querySearchableActivities(Context context, Intent searchIntent,
+ @UserIdInt int userId) {
+ final List<ResolveInfo> activities = context.getPackageManager()
+ .queryIntentActivitiesAsUser(searchIntent, PackageManager.GET_META_DATA
+ | PackageManager.MATCH_INSTANT
+ | PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId);
+ return activities;
+ }
+
+
class GlobalSearchProviderObserver extends ContentObserver {
private final ContentResolver mResolver;
@@ -196,7 +274,7 @@
public void onChange(boolean selfChange) {
synchronized (mSearchables) {
for (int i = 0; i < mSearchables.size(); i++) {
- mSearchables.valueAt(i).updateSearchableList();
+ mSearchables.valueAt(i).invalidateSearchableList();
}
}
Intent intent = new Intent(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
diff --git a/services/core/java/com/android/server/search/Searchables.java b/services/core/java/com/android/server/search/Searchables.java
index 7b39775..dc67339 100644
--- a/services/core/java/com/android/server/search/Searchables.java
+++ b/services/core/java/com/android/server/search/Searchables.java
@@ -35,8 +35,10 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import java.io.FileDescriptor;
@@ -62,7 +64,6 @@
private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
private Context mContext;
-
private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
private ArrayList<SearchableInfo> mSearchablesList = null;
private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null;
@@ -81,6 +82,12 @@
final private IPackageManager mPm;
// User for which this Searchables caches information
private int mUserId;
+ @GuardedBy("this")
+ private boolean mRebuildSearchables = true;
+
+ // Package names that are known to contain {@link SearchableInfo}
+ @GuardedBy("this")
+ private ArraySet<String> mKnownSearchablePackageNames = new ArraySet<>();
/**
*
@@ -224,7 +231,14 @@
*
* TODO: sort the list somehow? UI choice.
*/
- public void updateSearchableList() {
+ public void updateSearchableListIfNeeded() {
+ synchronized (this) {
+ if (!mRebuildSearchables) {
+ // The searchable list is valid, no need to rebuild.
+ return;
+ }
+ }
+
// These will become the new values at the end of the method
HashMap<ComponentName, SearchableInfo> newSearchablesMap
= new HashMap<ComponentName, SearchableInfo>();
@@ -232,6 +246,7 @@
= new ArrayList<SearchableInfo>();
ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
= new ArrayList<SearchableInfo>();
+ ArraySet<String> newKnownSearchablePackageNames = new ArraySet<>();
// Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers.
List<ResolveInfo> searchList;
@@ -264,6 +279,7 @@
mUserId);
if (searchable != null) {
newSearchablesList.add(searchable);
+ newKnownSearchablePackageNames.add(ai.packageName);
newSearchablesMap.put(searchable.getSearchActivity(), searchable);
if (searchable.shouldIncludeInGlobalSearch()) {
newSearchablesInGlobalSearchList.add(searchable);
@@ -286,16 +302,41 @@
synchronized (this) {
mSearchablesMap = newSearchablesMap;
mSearchablesList = newSearchablesList;
+ mKnownSearchablePackageNames = newKnownSearchablePackageNames;
mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
mGlobalSearchActivities = newGlobalSearchActivities;
mCurrentGlobalSearchActivity = newGlobalSearchActivity;
mWebSearchActivity = newWebSearchActivity;
+ for (ResolveInfo globalSearchActivity: mGlobalSearchActivities) {
+ mKnownSearchablePackageNames.add(
+ globalSearchActivity.getComponentInfo().packageName);
+ }
+ if (mCurrentGlobalSearchActivity != null) {
+ mKnownSearchablePackageNames.add(
+ mCurrentGlobalSearchActivity.getPackageName());
+ }
+ if (mWebSearchActivity != null) {
+ mKnownSearchablePackageNames.add(mWebSearchActivity.getPackageName());
+ }
+
+ mRebuildSearchables = false;
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
+ synchronized ArraySet<String> getKnownSearchablePackageNames() {
+ return mKnownSearchablePackageNames;
+ }
+
+ synchronized void invalidateSearchableList() {
+ mRebuildSearchables = true;
+
+ // Don't rebuild the searchable list, it will be rebuilt
+ // when the next updateSearchableList gets called.
+ }
+
/**
* Returns a sorted list of installed search providers as per
* the following heuristics:
@@ -532,6 +573,8 @@
pw.print(" "); pw.println(info.getSuggestAuthority());
}
}
+
+ pw.println("mRebuildSearchables = " + mRebuildSearchables);
}
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 7163319..5d17884 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -19,7 +19,7 @@
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
-import android.os.IExternalVibratorService;
+import android.os.ExternalVibrationScale;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -37,11 +37,13 @@
// Scale levels. Each level, except MUTE, is defined as the delta between the current setting
// and the default intensity for that type of vibration (i.e. current - default).
- private static final int SCALE_VERY_LOW = IExternalVibratorService.SCALE_VERY_LOW; // -2
- private static final int SCALE_LOW = IExternalVibratorService.SCALE_LOW; // -1
- private static final int SCALE_NONE = IExternalVibratorService.SCALE_NONE; // 0
- private static final int SCALE_HIGH = IExternalVibratorService.SCALE_HIGH; // 1
- private static final int SCALE_VERY_HIGH = IExternalVibratorService.SCALE_VERY_HIGH; // 2
+ private static final int SCALE_VERY_LOW =
+ ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW; // -2
+ private static final int SCALE_LOW = ExternalVibrationScale.ScaleLevel.SCALE_LOW; // -1
+ private static final int SCALE_NONE = ExternalVibrationScale.ScaleLevel.SCALE_NONE; // 0
+ private static final int SCALE_HIGH = ExternalVibrationScale.ScaleLevel.SCALE_HIGH; // 1
+ private static final int SCALE_VERY_HIGH =
+ ExternalVibrationScale.ScaleLevel.SCALE_VERY_HIGH; // 2
// Scale factors for each level.
private static final float SCALE_FACTOR_VERY_LOW = 0.6f;
@@ -83,9 +85,9 @@
* Calculates the scale to be applied to external vibration with given usage.
*
* @param usageHint one of VibrationAttributes.USAGE_*
- * @return one of IExternalVibratorService.SCALE_*
+ * @return one of ExternalVibrationScale.ScaleLevel.SCALE_*
*/
- public int getExternalVibrationScale(int usageHint) {
+ public int getExternalVibrationScaleLevel(int usageHint) {
int defaultIntensity = mSettingsController.getDefaultIntensity(usageHint);
int currentIntensity = mSettingsController.getCurrentIntensity(usageHint);
@@ -107,6 +109,22 @@
}
/**
+ * Returns the adaptive haptics scale that should be applied to the vibrations with
+ * the given usage. When no adaptive scales are available for the usages, then returns 1
+ * indicating no scaling will be applied
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*
+ * @return The adaptive haptics scale.
+ */
+ public float getAdaptiveHapticsScale(int usageHint) {
+ if (shouldApplyAdaptiveHapticsScale(usageHint)) {
+ return mAdaptiveHapticsScales.get(usageHint);
+ }
+
+ return 1f; // no scaling
+ }
+
+ /**
* Scale a {@link VibrationEffect} based on the given usage hint for this vibration.
*
* @param effect the effect to be scaled
@@ -152,9 +170,7 @@
}
// If adaptive haptics scaling is available for this usage, apply it to the segment.
- if (Flags.adaptiveHapticsEnabled()
- && mAdaptiveHapticsScales.size() > 0
- && mAdaptiveHapticsScales.contains(usageHint)) {
+ if (shouldApplyAdaptiveHapticsScale(usageHint)) {
float adaptiveScale = mAdaptiveHapticsScales.get(usageHint);
segment = segment.scaleLinearly(adaptiveScale);
}
@@ -224,6 +240,10 @@
mAdaptiveHapticsScales.clear();
}
+ private boolean shouldApplyAdaptiveHapticsScale(int usageHint) {
+ return Flags.adaptiveHapticsEnabled() && mAdaptiveHapticsScales.contains(usageHint);
+ }
+
/** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
private static int intensityToEffectStrength(int intensity) {
switch (intensity) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index be5d158..78e0ebb 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import static android.os.ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -35,6 +36,7 @@
import android.os.Build;
import android.os.CombinedVibration;
import android.os.ExternalVibration;
+import android.os.ExternalVibrationScale;
import android.os.Handler;
import android.os.IBinder;
import android.os.IExternalVibratorService;
@@ -277,7 +279,7 @@
context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
- if (ServiceManager.isDeclared(VIBRATOR_CONTROL_SERVICE)) {
+ if (injector.isServiceDeclared(VIBRATOR_CONTROL_SERVICE)) {
injector.addService(VIBRATOR_CONTROL_SERVICE, mVibratorControlService);
}
@@ -1427,6 +1429,10 @@
VibratorControllerHolder createVibratorControllerHolder() {
return new VibratorControllerHolder();
}
+
+ boolean isServiceDeclared(String name) {
+ return ServiceManager.isDeclared(name);
+ }
}
/**
@@ -1594,7 +1600,7 @@
IBinder.DeathRecipient {
public final ExternalVibration externalVibration;
- public int scale;
+ public ExternalVibrationScale scale = new ExternalVibrationScale();
private Vibration.Status mStatus;
@@ -1605,7 +1611,6 @@
// instead of using DEVICE_ID_INVALID here and relying on the UID checks.
Context.DEVICE_ID_INVALID, externalVibration.getPackage(), null));
this.externalVibration = externalVibration;
- this.scale = IExternalVibratorService.SCALE_NONE;
mStatus = Vibration.Status.RUNNING;
}
@@ -1658,7 +1663,7 @@
public Vibration.DebugInfo getDebugInfo() {
return new Vibration.DebugInfo(mStatus, stats, /* playedEffect= */ null,
- /* originalEffect= */ null, scale, callerInfo);
+ /* originalEffect= */ null, scale.scaleLevel, callerInfo);
}
public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) {
@@ -1988,11 +1993,17 @@
/** Implementation of {@link IExternalVibratorService} to be triggered on external control. */
@VisibleForTesting
final class ExternalVibratorService extends IExternalVibratorService.Stub {
+ private static final ExternalVibrationScale SCALE_MUTE = new ExternalVibrationScale();
+
+ static {
+ SCALE_MUTE.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ }
@Override
- public int onExternalVibrationStart(ExternalVibration vib) {
+ public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) {
+
if (!hasExternalControlCapability()) {
- return IExternalVibratorService.SCALE_MUTE;
+ return SCALE_MUTE;
}
if (ActivityManager.checkComponentPermission(android.Manifest.permission.VIBRATE,
vib.getUid(), -1 /*owningUid*/, true /*exported*/)
@@ -2000,7 +2011,7 @@
Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
+ " tried to play externally controlled vibration"
+ " without VIBRATE permission, ignoring.");
- return IExternalVibratorService.SCALE_MUTE;
+ return SCALE_MUTE;
}
// Create Vibration.Stats as close to the received request as possible, for tracking.
@@ -2033,7 +2044,7 @@
}
if (vibrationEndInfo != null) {
- vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
+ vibHolder.scale = SCALE_MUTE;
// Failed to start the vibration, end it and report metrics right away.
endVibrationAndWriteStatsLocked(vibHolder, vibrationEndInfo);
return vibHolder.scale;
@@ -2074,7 +2085,10 @@
}
mCurrentExternalVibration = vibHolder;
vibHolder.linkToDeath();
- vibHolder.scale = mVibrationScaler.getExternalVibrationScale(attrs.getUsage());
+ vibHolder.scale.scaleLevel = mVibrationScaler.getExternalVibrationScaleLevel(
+ attrs.getUsage());
+ vibHolder.scale.adaptiveHapticsScale = mVibrationScaler.getAdaptiveHapticsScale(
+ attrs.getUsage());
}
if (waitForCompletion) {
@@ -2086,7 +2100,7 @@
new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING),
/* continueExternalControl= */ false);
}
- return IExternalVibratorService.SCALE_MUTE;
+ return SCALE_MUTE;
}
}
if (!alreadyUnderExternalControl) {
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 3b2e69a..c6e8eb8 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
@@ -225,6 +226,21 @@
}
@Override
+ public void installExistingPackageForAllUsers(Context context, String packageName) {
+ UserManager userManager = context.getSystemService(UserManager.class);
+ for (UserInfo userInfo : userManager.getUsers()) {
+ installPackageForUser(packageName, userInfo.id);
+ }
+ }
+
+ private void installPackageForUser(String packageName, int userId) {
+ final Context context = AppGlobals.getInitialApplication();
+ final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0);
+ final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller();
+ installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null);
+ }
+
+ @Override
public boolean systemIsDebuggable() {
return Build.IS_DEBUGGABLE;
}
diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java
index 5ed2cfe..ad32f62 100644
--- a/services/core/java/com/android/server/webkit/SystemInterface.java
+++ b/services/core/java/com/android/server/webkit/SystemInterface.java
@@ -43,6 +43,7 @@
public void killPackageDependents(String packageName);
public void enablePackageForAllUsers(Context context, String packageName, boolean enable);
+ public void installExistingPackageForAllUsers(Context context, String packageName);
public boolean systemIsDebuggable();
public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 27c80c4..596de68 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -211,14 +211,16 @@
}
if (repairNeeded) {
- // We didn't find a valid WebView implementation. Try explicitly re-enabling the
- // default package for all users in case it was disabled, even if we already did the
- // one-time migration before. If this actually changes the state, we will see the
- // PackageManager broadcast shortly and try again.
+ // We didn't find a valid WebView implementation. Try explicitly re-installing and
+ // re-enabling the default package for all users in case it was disabled, even if we
+ // already did the one-time migration before. If this actually changes the state, we
+ // will see the PackageManager broadcast shortly and try again.
Slog.w(
TAG,
- "No provider available for all users, trying to enable "
+ "No provider available for all users, trying to install and enable "
+ mDefaultProvider.packageName);
+ mSystemInterface.installExistingPackageForAllUsers(
+ mContext, mDefaultProvider.packageName);
mSystemInterface.enablePackageForAllUsers(
mContext, mDefaultProvider.packageName, true);
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2ef34a8..bc6f93f 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3550,7 +3550,7 @@
IBinder callerToken = new Binder();
if (android.security.Flags.contentUriPermissionApis()) {
try {
- resultTo.computeCallerInfo(callerToken, intent, this.getUid(),
+ resultTo.computeCallerInfo(callerToken, resultData, this.getUid(),
mAtmService.getPackageManager().getNameForUid(this.getUid()),
/* isShareIdentityEnabled */ false);
// Result callers cannot share their identity via
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 24c8ebb..514a3d8 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -2505,6 +2505,11 @@
userId = handleIncomingUser(Binder.getCallingPid(), callingUid, userId, "getRecentTasks");
final boolean allowed = isGetTasksAllowed("getRecentTasks", Binder.getCallingPid(),
callingUid);
+ if (!mAmInternal.isUserRunning(userId, ActivityManager.FLAG_AND_UNLOCKED)) {
+ Slog.i(TAG, "User " + userId + " is locked. Cannot load recents");
+ return ParceledListSlice.emptyList();
+ }
+ mRecentTasks.loadRecentTasksIfNeeded(userId);
synchronized (mGlobalLock) {
return mRecentTasks.getRecentTasks(maxNum, flags, allowed, userId, callingUid);
}
@@ -7056,11 +7061,9 @@
@Override
public void loadRecentTasksForUser(int userId) {
- synchronized (mGlobalLock) {
- mRecentTasks.loadUserRecentsLocked(userId);
- // TODO renaming the methods(?)
- mPackageConfigPersister.loadUserPackages(userId);
- }
+ // This runs on android.fg thread when the user is unlocking.
+ mRecentTasks.loadRecentTasksIfNeeded(userId);
+ mPackageConfigPersister.loadUserPackages(userId);
}
@Override
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 939cf1a..1a63f14 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -137,7 +137,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.TransitionAnimation;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.DumpUtils.Dump;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -248,7 +247,7 @@
mHandler = new Handler(service.mH.getLooper());
mDisplayContent = displayContent;
mTransitionAnimation = new TransitionAnimation(
- context, ProtoLogImpl.isEnabled(WM_DEBUG_ANIM), TAG);
+ context, ProtoLog.isEnabled(WM_DEBUG_ANIM), TAG);
mGridLayoutRecentsEnabled = SystemProperties.getBoolean("ro.recents.grid", false);
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index e027eb6..dd14642 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-import static android.app.ActivityManager.FLAG_AND_UNLOCKED;
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
@@ -69,6 +68,7 @@
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -89,9 +89,9 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
-import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for managing the recent tasks list. The list is ordered by most recent (index 0) to the
@@ -167,8 +167,9 @@
/**
* Mapping of user id -> whether recent tasks have been loaded for that user.
+ * The AtomicBoolean per user will be locked when reading persisted task from storage.
*/
- private final SparseBooleanArray mUsersWithRecentsLoaded = new SparseBooleanArray(
+ private final SparseArray<AtomicBoolean> mUsersWithRecentsLoaded = new SparseArray<>(
DEFAULT_INITIAL_CAPACITY);
/**
@@ -481,29 +482,49 @@
/**
* Loads the persistent recentTasks for {@code userId} into this list from persistent storage.
- * Does nothing if they are already loaded.
- *
- * @param userId the user Id
+ * Does nothing if they are already loaded. This may perform IO operation, so the caller should
+ * not hold a lock.
*/
- void loadUserRecentsLocked(int userId) {
- if (mUsersWithRecentsLoaded.get(userId)) {
- // User already loaded, return early
- return;
- }
-
- // Load the task ids if not loaded.
- loadPersistedTaskIdsForUserLocked(userId);
-
- // Check if any tasks are added before recents is loaded
- final SparseBooleanArray preaddedTasks = new SparseBooleanArray();
- for (final Task task : mTasks) {
- if (task.mUserId == userId && shouldPersistTaskLocked(task)) {
- preaddedTasks.put(task.mTaskId, true);
+ void loadRecentTasksIfNeeded(int userId) {
+ AtomicBoolean userLoaded;
+ synchronized (mService.mGlobalLock) {
+ userLoaded = mUsersWithRecentsLoaded.get(userId);
+ if (userLoaded == null) {
+ mUsersWithRecentsLoaded.append(userId, userLoaded = new AtomicBoolean());
}
}
+ synchronized (userLoaded) {
+ if (userLoaded.get()) {
+ // The recent tasks of the user are already loaded.
+ return;
+ }
+ // Read task files from storage.
+ final SparseBooleanArray persistedTaskIds =
+ mTaskPersister.readPersistedTaskIdsFromFileForUser(userId);
+ final TaskPersister.RecentTaskFiles taskFiles = TaskPersister.loadTasksForUser(userId);
+ synchronized (mService.mGlobalLock) {
+ restoreRecentTasksLocked(userId, persistedTaskIds, taskFiles);
+ }
+ userLoaded.set(true);
+ }
+ }
- Slog.i(TAG, "Loading recents for user " + userId + " into memory.");
- List<Task> tasks = mTaskPersister.restoreTasksForUserLocked(userId, preaddedTasks);
+ /** Restores recent tasks from raw data (the files are already read into memory). */
+ private void restoreRecentTasksLocked(int userId, SparseBooleanArray persistedTaskIds,
+ TaskPersister.RecentTaskFiles taskFiles) {
+ mTaskPersister.setPersistedTaskIds(userId, persistedTaskIds);
+ mPersistedTaskIds.put(userId, persistedTaskIds.clone());
+ // Check if any tasks are added before recents is loaded.
+ final IntArray existedTaskIds = new IntArray();
+ for (int i = mTasks.size() - 1; i >= 0; i--) {
+ final Task task = mTasks.get(i);
+ if (task.mUserId == userId && shouldPersistTaskLocked(task)) {
+ existedTaskIds.add(task.mTaskId);
+ }
+ }
+ Slog.i(TAG, "Restoring recents for user " + userId);
+ final ArrayList<Task> tasks = mTaskPersister.restoreTasksForUserLocked(userId, taskFiles,
+ existedTaskIds);
// Tasks are ordered from most recent to least recent. Update the last active time to be
// in sync with task recency when device reboots, so the most recent task has the
@@ -516,37 +537,34 @@
mTasks.addAll(tasks);
cleanupLocked(userId);
- mUsersWithRecentsLoaded.put(userId, true);
// If we have tasks added before loading recents, we need to update persistent task IDs.
- if (preaddedTasks.size() > 0) {
+ if (existedTaskIds.size() > 0) {
syncPersistentTaskIdsLocked();
}
}
- private void loadPersistedTaskIdsForUserLocked(int userId) {
- // An empty instead of a null set here means that no persistent taskIds were present
- // on file when we loaded them.
- if (mPersistedTaskIds.get(userId) == null) {
- mPersistedTaskIds.put(userId, mTaskPersister.loadPersistedTaskIdsForUser(userId));
- Slog.i(TAG, "Loaded persisted task ids for user " + userId);
- }
+ private boolean isRecentTasksLoaded(int userId) {
+ final AtomicBoolean userLoaded = mUsersWithRecentsLoaded.get(userId);
+ return userLoaded != null && userLoaded.get();
}
/**
* @return whether the {@param taskId} is currently in use for the given user.
*/
boolean containsTaskId(int taskId, int userId) {
- loadPersistedTaskIdsForUserLocked(userId);
- return mPersistedTaskIds.get(userId).get(taskId);
+ final SparseBooleanArray taskIds = mPersistedTaskIds.get(userId);
+ return taskIds != null && taskIds.get(taskId);
}
- /**
- * @return all the task ids for the user with the given {@param userId}.
- */
- SparseBooleanArray getTaskIdsForUser(int userId) {
- loadPersistedTaskIdsForUserLocked(userId);
- return mPersistedTaskIds.get(userId);
+ /** Returns all the task ids for the user from {@link #usersWithRecentsLoadedLocked}. */
+ SparseBooleanArray getTaskIdsForLoadedUser(int loadedUserId) {
+ final SparseBooleanArray taskIds = mPersistedTaskIds.get(loadedUserId);
+ if (taskIds == null) {
+ Slog.wtf(TAG, "Loaded user without loaded tasks, userId=" + loadedUserId);
+ return new SparseBooleanArray();
+ }
+ return taskIds;
}
/**
@@ -565,7 +583,7 @@
private void syncPersistentTaskIdsLocked() {
for (int i = mPersistedTaskIds.size() - 1; i >= 0; i--) {
int userId = mPersistedTaskIds.keyAt(i);
- if (mUsersWithRecentsLoaded.get(userId)) {
+ if (isRecentTasksLoaded(userId)) {
// Recents are loaded only after task ids are loaded. Therefore, the set of taskids
// referenced here should not be null.
mPersistedTaskIds.valueAt(i).clear();
@@ -621,7 +639,7 @@
int len = 0;
for (int i = 0; i < usersWithRecentsLoaded.length; i++) {
int userId = mUsersWithRecentsLoaded.keyAt(i);
- if (mUsersWithRecentsLoaded.valueAt(i)) {
+ if (mUsersWithRecentsLoaded.valueAt(i).get()) {
usersWithRecentsLoaded[len++] = userId;
}
}
@@ -639,7 +657,7 @@
* @param userId the id of the user
*/
void unloadUserDataFromMemoryLocked(int userId) {
- if (mUsersWithRecentsLoaded.get(userId)) {
+ if (isRecentTasksLoaded(userId)) {
Slog.i(TAG, "Unloading recents for user " + userId + " from memory.");
mUsersWithRecentsLoaded.delete(userId);
removeTasksForUserLocked(userId);
@@ -922,11 +940,6 @@
return mService.mAmInternal.getCurrentProfileIds();
}
- @VisibleForTesting
- boolean isUserRunning(int userId, int flags) {
- return mService.mAmInternal.isUserRunning(userId, flags);
- }
-
/**
* @return the list of recent tasks for presentation.
*/
@@ -942,13 +955,6 @@
private ArrayList<ActivityManager.RecentTaskInfo> getRecentTasksImpl(int maxNum, int flags,
boolean getTasksAllowed, int userId, int callingUid) {
final boolean withExcluded = (flags & RECENT_WITH_EXCLUDED) != 0;
-
- if (!isUserRunning(userId, FLAG_AND_UNLOCKED)) {
- Slog.i(TAG, "user " + userId + " is still locked. Cannot load recents");
- return new ArrayList<>();
- }
- loadUserRecentsLocked(userId);
-
final Set<Integer> includedUsers = getProfileIds(userId);
includedUsers.add(Integer.valueOf(userId));
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index a98b9f7..3ef6eeb 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -44,7 +44,6 @@
import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FastPrintWriter;
import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -210,7 +209,7 @@
Slog.e(TAG, "Failed to start remote animation", e);
onAnimationFinished();
}
- if (ProtoLogImpl.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) {
+ if (ProtoLog.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) {
ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation(): Notify animation start:");
writeStartDebugStatement();
}
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index c3de4d5..d67684c 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -32,7 +32,6 @@
import android.view.SurfaceControl.Transaction;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import java.io.PrintWriter;
@@ -193,7 +192,7 @@
return;
}
mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
- if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+ if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
mAnimation.dump(pw, "");
diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java
index 5ec0119..d89dc0b 100644
--- a/services/core/java/com/android/server/wm/TaskPersister.java
+++ b/services/core/java/com/android/server/wm/TaskPersister.java
@@ -27,6 +27,7 @@
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.AtomicFile;
+import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -43,20 +44,20 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
-import java.util.List;
/**
* Persister that saves recent tasks into disk.
@@ -129,11 +130,9 @@
ImageWriteQueueItem.class);
}
+ /** Reads task ids from file. This should not be called in lock. */
@NonNull
- SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
- if (mTaskIdsInFile.get(userId) != null) {
- return mTaskIdsInFile.get(userId).clone();
- }
+ SparseBooleanArray readPersistedTaskIdsFromFileForUser(int userId) {
final SparseBooleanArray persistedTaskIds = new SparseBooleanArray();
synchronized (mIoLock) {
BufferedReader reader = null;
@@ -154,11 +153,10 @@
IoUtils.closeQuietly(reader);
}
}
- mTaskIdsInFile.put(userId, persistedTaskIds);
- return persistedTaskIds.clone();
+ Slog.i(TAG, "Loaded persisted task ids for user " + userId);
+ return persistedTaskIds;
}
-
@VisibleForTesting
void writePersistedTaskIdsForUser(@NonNull SparseBooleanArray taskIds, int userId) {
if (userId < 0) {
@@ -183,6 +181,10 @@
}
}
+ void setPersistedTaskIds(int userId, @NonNull SparseBooleanArray taskIds) {
+ mTaskIdsInFile.put(userId, taskIds);
+ }
+
void unloadUserDataFromMemory(int userId) {
mTaskIdsInFile.delete(userId);
}
@@ -241,7 +243,7 @@
return item != null ? item.mImage : null;
}
- private String fileToString(File file) {
+ private static String fileToString(File file) {
final String newline = System.lineSeparator();
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
@@ -272,44 +274,64 @@
return null;
}
- List<Task> restoreTasksForUserLocked(final int userId, SparseBooleanArray preaddedTasks) {
- final ArrayList<Task> tasks = new ArrayList<Task>();
- ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
-
- File userTasksDir = getUserTasksDir(userId);
-
- File[] recentFiles = userTasksDir.listFiles();
+ /** Loads task files from disk. This should not be called in lock. */
+ static RecentTaskFiles loadTasksForUser(int userId) {
+ final ArrayList<RecentTaskFile> taskFiles = new ArrayList<>();
+ final File userTasksDir = getUserTasksDir(userId);
+ final File[] recentFiles = userTasksDir.listFiles();
if (recentFiles == null) {
- Slog.e(TAG, "restoreTasksForUserLocked: Unable to list files from " + userTasksDir);
+ Slog.i(TAG, "loadTasksForUser: Unable to list files from " + userTasksDir
+ + " exists=" + userTasksDir.exists());
+ return new RecentTaskFiles(new File[0], taskFiles);
+ }
+ for (File taskFile : recentFiles) {
+ if (!taskFile.getName().endsWith(TASK_FILENAME_SUFFIX)) {
+ continue;
+ }
+ final int taskId;
+ try {
+ taskId = Integer.parseInt(taskFile.getName().substring(
+ 0 /* beginIndex */,
+ taskFile.getName().length() - TASK_FILENAME_SUFFIX.length()));
+ } catch (NumberFormatException e) {
+ Slog.w(TAG, "Unexpected task file name", e);
+ continue;
+ }
+ try {
+ taskFiles.add(new RecentTaskFile(taskId, taskFile));
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to read file: " + fileToString(taskFile), e);
+ taskFile.delete();
+ }
+ }
+ return new RecentTaskFiles(recentFiles, taskFiles);
+ }
+
+ /** Restores tasks from raw bytes (no read storage operation). */
+ ArrayList<Task> restoreTasksForUserLocked(int userId, RecentTaskFiles recentTaskFiles,
+ IntArray existedTaskIds) {
+ final ArrayList<Task> tasks = new ArrayList<>();
+ final ArrayList<RecentTaskFile> taskFiles = recentTaskFiles.mLoadedFiles;
+ if (taskFiles.isEmpty()) {
return tasks;
}
- for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
- File taskFile = recentFiles[taskNdx];
+ final ArraySet<Integer> recoveredTaskIds = new ArraySet<>();
+ for (int taskNdx = 0; taskNdx < taskFiles.size(); ++taskNdx) {
+ final RecentTaskFile recentTask = taskFiles.get(taskNdx);
+ if (existedTaskIds.contains(recentTask.mTaskId)) {
+ Slog.w(TAG, "Task #" + recentTask.mTaskId
+ + " has already been created, so skip restoring");
+ continue;
+ }
+ final File taskFile = recentTask.mFile;
if (DEBUG) {
Slog.d(TAG, "restoreTasksForUserLocked: userId=" + userId
+ ", taskFile=" + taskFile.getName());
}
- if (!taskFile.getName().endsWith(TASK_FILENAME_SUFFIX)) {
- continue;
- }
- try {
- final int taskId = Integer.parseInt(taskFile.getName().substring(
- 0 /* beginIndex */,
- taskFile.getName().length() - TASK_FILENAME_SUFFIX.length()));
- if (preaddedTasks.get(taskId, false)) {
- Slog.w(TAG, "Task #" + taskId +
- " has already been created so we don't restore again");
- continue;
- }
- } catch (NumberFormatException e) {
- Slog.w(TAG, "Unexpected task file name", e);
- continue;
- }
-
boolean deleteFile = false;
- try (InputStream is = new FileInputStream(taskFile)) {
+ try (InputStream is = recentTask.mXmlContent) {
final TypedXmlPullParser in = Xml.resolvePullParser(is);
int event;
@@ -345,7 +367,7 @@
} else if (userId != task.mUserId) {
// Should not happen.
Slog.wtf(TAG, "Task with userId " + task.mUserId + " found in "
- + userTasksDir.getAbsolutePath());
+ + taskFile.getAbsolutePath());
} else {
// Looks fine.
mTaskSupervisor.setNextTaskIdForUser(taskId, userId);
@@ -377,7 +399,7 @@
}
if (!DEBUG) {
- removeObsoleteFiles(recoveredTaskIds, userTasksDir.listFiles());
+ removeObsoleteFiles(recoveredTaskIds, recentTaskFiles.mUserTaskFiles);
}
// Fix up task affiliation from taskIds
@@ -456,7 +478,7 @@
SparseArray<SparseBooleanArray> changedTaskIdsPerUser = new SparseArray<>();
synchronized (mService.mGlobalLock) {
for (int userId : mRecentTasks.usersWithRecentsLoadedLocked()) {
- SparseBooleanArray taskIdsToSave = mRecentTasks.getTaskIdsForUser(userId);
+ SparseBooleanArray taskIdsToSave = mRecentTasks.getTaskIdsForLoadedUser(userId);
SparseBooleanArray persistedIdsInFile = mTaskIdsInFile.get(userId);
if (persistedIdsInFile != null && persistedIdsInFile.equals(taskIdsToSave)) {
continue;
@@ -512,6 +534,30 @@
return parentDir.isDirectory() || parentDir.mkdir();
}
+ private static class RecentTaskFile {
+ final int mTaskId;
+ final File mFile;
+ final ByteArrayInputStream mXmlContent;
+
+ RecentTaskFile(int taskId, File file) throws IOException {
+ mTaskId = taskId;
+ mFile = file;
+ mXmlContent = new ByteArrayInputStream(Files.readAllBytes(file.toPath()));
+ }
+ }
+
+ static class RecentTaskFiles {
+ /** All files under the user task directory. */
+ final File[] mUserTaskFiles;
+ /** The successfully loaded files. */
+ final ArrayList<RecentTaskFile> mLoadedFiles;
+
+ RecentTaskFiles(File[] userFiles, ArrayList<RecentTaskFile> loadedFiles) {
+ mUserTaskFiles = userFiles;
+ mLoadedFiles = loadedFiles;
+ }
+ }
+
private static class TaskWriteQueueItem implements PersisterQueue.WriteQueueItem {
private final ActivityTaskManagerService mService;
private final Task mTask;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 001f46d..594043d 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -58,7 +58,6 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
@@ -336,7 +335,7 @@
for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
final WallpaperWindowToken token = mWallpaperTokens.get(i);
token.setVisibility(false);
- if (ProtoLogImpl.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
+ if (ProtoLog.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
ProtoLog.d(WM_DEBUG_WALLPAPER,
"Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
token, winGoingAway, mWallpaperTarget, mPrevWallpaperTarget,
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 61fde5e..fd0289e 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -113,7 +113,6 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.wm.SurfaceAnimator.Animatable;
@@ -3410,7 +3409,7 @@
// ActivityOption#makeCustomAnimation or WindowManager#overridePendingTransition.
a.restrictDuration(MAX_APP_TRANSITION_DURATION);
}
- if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+ if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
ProtoLog.i(WM_DEBUG_ANIM, "Loaded animation %s for %s, duration: %d, stack=%s",
a, this, ((a != null) ? a.getDuration() : 0), Debug.getCallers(20));
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9b7bc43..08d43ae 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -327,8 +327,8 @@
import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.KeyInterceptionInfo;
+import com.android.internal.protolog.LegacyProtoLogImpl;
import com.android.internal.protolog.ProtoLogGroup;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastPrintWriter;
@@ -6701,7 +6701,11 @@
private void dumpLogStatus(PrintWriter pw) {
pw.println("WINDOW MANAGER LOGGING (dumpsys window logging)");
- pw.println(ProtoLogImpl.getSingleInstance().getStatus());
+ if (android.tracing.Flags.perfettoProtolog()) {
+ pw.println("Deprecated legacy command. Use Perfetto commands instead.");
+ return;
+ }
+ ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).getStatus();
}
private void dumpSessionsLocked(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 8fad950..0b29f96 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -48,7 +48,9 @@
import android.view.ViewDebug;
import com.android.internal.os.ByteTransferPipe;
-import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.server.IoThread;
import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
@@ -107,11 +109,19 @@
// trace files can be written.
return mInternal.mWindowTracing.onShellCommand(this);
case "logging":
- int result = ProtoLogImpl.getSingleInstance().onShellCommand(this);
- if (result != 0) {
- pw.println("Not handled, please use "
- + "`adb shell dumpsys activity service SystemUIService WMShell` "
- + "if you are looking for ProtoLog in WMShell");
+ IProtoLog instance = ProtoLog.getSingleInstance();
+ int result = 0;
+ if (instance instanceof LegacyProtoLogImpl) {
+ result = ((LegacyProtoLogImpl) instance).onShellCommand(this);
+ if (result != 0) {
+ pw.println("Not handled, please use "
+ + "`adb shell dumpsys activity service SystemUIService "
+ + "WMShell` if you are looking for ProtoLog in WMShell");
+ }
+ } else {
+ result = -1;
+ pw.println("Command not supported. "
+ + "Only supported when using legacy ProtoLog.");
}
return result;
case "user-rotation":
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 90f5b62..a7a28c2 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -245,7 +245,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.KeyInterceptionInfo;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.ToBooleanFunction;
@@ -4681,7 +4680,7 @@
}
void onExitAnimationDone() {
- if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+ if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
final AnimationAdapter animationAdapter = mSurfaceAnimator.getAnimation();
StringWriter sw = new StringWriter();
if (animationAdapter != null) {
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 6428591..7f7c249 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -60,7 +60,6 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
-import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.policy.WindowManagerPolicy;
@@ -584,7 +583,7 @@
mWin.mAttrs, attr, TRANSIT_OLD_NONE);
}
}
- if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+ if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
ProtoLog.v(WM_DEBUG_ANIM, "applyAnimation: win=%s"
+ " anim=%d attr=0x%x a=%s transit=%d type=%d isEntrance=%b Callers %s",
this, anim, attr, a, transit, mAttrType, isEntrance, Debug.getCallers(20));
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index 416d042..424d504 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -35,7 +35,9 @@
import android.util.proto.ProtoOutputStream;
import android.view.Choreographer;
-import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.TraceBuffer;
import java.io.File;
@@ -77,6 +79,8 @@
private volatile boolean mEnabledLockFree;
private boolean mScheduled;
+ private final IProtoLog mProtoLog;
+
static WindowTracing createDefaultAndStartLooper(WindowManagerService service,
Choreographer choreographer) {
File file = new File(TRACE_FILENAME);
@@ -96,6 +100,7 @@
mTraceFile = file;
mBuffer = new TraceBuffer(bufferCapacity);
setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */);
+ mProtoLog = ProtoLog.getSingleInstance();
}
void startTrace(@Nullable PrintWriter pw) {
@@ -104,7 +109,6 @@
return;
}
synchronized (mEnabledLock) {
- ProtoLogImpl.getSingleInstance().startProtoLog(pw);
logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
mBuffer.resetBuffer();
mEnabled = mEnabledLockFree = true;
@@ -132,7 +136,6 @@
writeTraceToFileLocked();
logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
}
- ProtoLogImpl.getSingleInstance().stopProtoLog(pw, true);
}
/**
@@ -152,11 +155,15 @@
logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
writeTraceToFileLocked();
logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
- ProtoLogImpl.getSingleInstance().stopProtoLog(pw, true);
+ if (!android.tracing.Flags.perfettoProtolog()) {
+ ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true);
+ }
logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
mBuffer.resetBuffer();
mEnabled = mEnabledLockFree = true;
- ProtoLogImpl.getSingleInstance().startProtoLog(pw);
+ if (!android.tracing.Flags.perfettoProtolog()) {
+ ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw);
+ }
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
index e7855bc..c4e2dc8 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
@@ -15,6 +15,10 @@
*/
package com.android.server.devicepolicy;
+import static android.app.admin.DevicePolicyManager.CONTENT_PROTECTION_DISABLED;
+import static android.app.admin.DevicePolicyManager.ContentProtectionPolicy;
+
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.admin.DevicePolicyCache;
import android.app.admin.DevicePolicyManager;
@@ -70,10 +74,14 @@
/** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}. */
private final AtomicBoolean mCanGrantSensorsPermissions = new AtomicBoolean(false);
+ @GuardedBy("mLock")
+ private final SparseIntArray mContentProtectionPolicy = new SparseIntArray();
+
public void onUserRemoved(int userHandle) {
synchronized (mLock) {
mPasswordQuality.delete(userHandle);
mPermissionPolicy.delete(userHandle);
+ mContentProtectionPolicy.delete(userHandle);
}
}
@@ -143,6 +151,24 @@
}
@Override
+ public @ContentProtectionPolicy int getContentProtectionPolicy(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mContentProtectionPolicy.get(userId, CONTENT_PROTECTION_DISABLED);
+ }
+ }
+
+ /** Update the content protection policy for the given user. */
+ public void setContentProtectionPolicy(@UserIdInt int userId, @Nullable Integer value) {
+ synchronized (mLock) {
+ if (value == null) {
+ mContentProtectionPolicy.delete(userId);
+ } else {
+ mContentProtectionPolicy.put(userId, value);
+ }
+ }
+ }
+
+ @Override
public boolean canAdminGrantSensorsPermissions() {
return mCanGrantSensorsPermissions.get();
}
@@ -178,6 +204,7 @@
pw.println("Screen capture disallowed users: " + mScreenCaptureDisallowedUsers);
pw.println("Password quality: " + mPasswordQuality);
pw.println("Permission policy: " + mPermissionPolicy);
+ pw.println("Content protection policy: " + mContentProtectionPolicy);
pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions.get());
pw.print("Shortcuts overrides: ");
pw.println(mLauncherShortcutOverrides);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 9c48f29..0f97f4a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3633,6 +3633,7 @@
userId == UserHandle.USER_SYSTEM ? UserHandle.USER_ALL : userId);
updatePermissionPolicyCache(userId);
updateAdminCanGrantSensorsPermissionCache(userId);
+ updateContentProtectionPolicyCache(userId);
final List<PreferentialNetworkServiceConfig> preferentialNetworkServiceConfigs;
synchronized (getLockObject()) {
@@ -23534,6 +23535,12 @@
}
}
+ private void updateContentProtectionPolicyCache(@UserIdInt int userId) {
+ mPolicyCache.setContentProtectionPolicy(
+ userId,
+ mDevicePolicyEngine.getResolvedPolicy(PolicyDefinition.CONTENT_PROTECTION, userId));
+ }
+
@Override
public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() {
synchronized (getLockObject()) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 1247f9002..71facab 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -359,7 +359,7 @@
new NoArgsPolicyKey(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY),
new MostRecent<>(),
POLICY_FLAG_LOCAL_ONLY_POLICY,
- (Integer value, Context context, Integer userId, PolicyKey policyKey) -> true,
+ PolicyEnforcerCallbacks::setContentProtectionPolicy,
new IntegerPolicySerializer());
private static final Map<String, PolicyDefinition<?>> POLICY_DEFINITIONS = new HashMap<>();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 54242ab..c108deaf 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyCache;
import android.app.admin.DevicePolicyManager;
@@ -282,6 +283,21 @@
return true;
}
+ static boolean setContentProtectionPolicy(
+ @Nullable Integer value,
+ @NonNull Context context,
+ @UserIdInt Integer userId,
+ @NonNull PolicyKey policyKey) {
+ Binder.withCleanCallingIdentity(
+ () -> {
+ DevicePolicyCache cache = DevicePolicyCache.getInstance();
+ if (cache instanceof DevicePolicyCacheImpl cacheImpl) {
+ cacheImpl.setContentProtectionPolicy(userId, value);
+ }
+ });
+ return true;
+ }
+
private static void updateScreenCaptureDisabled() {
BackgroundThread.getHandler().post(() -> {
try {
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index b7af58c..3bce9b5 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -69,7 +69,7 @@
}
android_ravenwood_test {
- name: "FrameworksInputMethodSystemServerTests_host",
+ name: "FrameworksInputMethodSystemServerTestsRavenwood",
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
diff --git a/services/tests/displayservicetests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java
index a8af98f..8a6c2440 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java
@@ -16,6 +16,8 @@
package com.android.server.display;
+import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+
import static com.android.internal.display.RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE;
import static org.junit.Assert.assertEquals;
@@ -50,6 +52,8 @@
private DisplayManager mDisplayManagerMock;
@Mock
private Display mDisplayMock;
+ @Mock
+ private Display mDisplayMock2;
@Before
public void setUp() {
@@ -65,25 +69,54 @@
new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600,
/* refreshRate= */ 90)
};
-
when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mDisplayMock);
when(mDisplayMock.getSupportedModes()).thenReturn(modes);
+
+ Display.Mode[] modes2 = new Display.Mode[]{
+ new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600,
+ /* refreshRate= */ 70),
+ new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600,
+ /* refreshRate= */ 130),
+ new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600,
+ /* refreshRate= */ 80)
+ };
+ when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY + 1)).thenReturn(mDisplayMock2);
+ when(mDisplayMock2.getSupportedModes()).thenReturn(modes2);
+
+ when(mDisplayManagerMock.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED))
+ .thenReturn(new Display[]{ mDisplayMock, mDisplayMock2 });
}
@Test
public void testFindHighestRefreshRateForDefaultDisplay() {
- when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mDisplayMock);
assertEquals(120,
RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext),
/* delta= */ 0);
}
@Test
- public void testFindHighestRefreshRate_DisplayIsNull() {
+ public void testFindHighestRefreshRateForDefaultDisplay_DisplayIsNull() {
when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(null);
assertEquals(DEFAULT_REFRESH_RATE,
RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext),
/* delta= */ 0);
}
+
+ @Test
+ public void testFindHighestRefreshRateAmongAllDisplays() {
+ assertEquals(130,
+ RefreshRateSettingsUtils.findHighestRefreshRateAmongAllDisplays(mContext),
+ /* delta= */ 0);
+ }
+
+ @Test
+ public void testFindHighestRefreshRateAmongAllDisplays_NoDisplays() {
+ when(mDisplayManagerMock.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED))
+ .thenReturn(new Display[0]);
+ assertEquals(DEFAULT_REFRESH_RATE,
+ RefreshRateSettingsUtils.findHighestRefreshRateAmongAllDisplays(mContext),
+ /* delta= */ 0);
+
+ }
}
diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
index 8f5d125..a918be2 100644
--- a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
+++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
@@ -16,6 +16,8 @@
package com.android.server.media;
+import static android.Manifest.permission.MODIFY_AUDIO_ROUTING;
+
import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA;
import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy;
@@ -31,6 +33,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Instrumentation;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
@@ -48,6 +51,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -85,6 +89,7 @@
/* name= */ null,
/* address= */ null);
+ private Instrumentation mInstrumentation;
private AudioDeviceInfo mSelectedAudioDeviceInfo;
private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos;
@Mock private AudioManager mMockAudioManager;
@@ -96,10 +101,11 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(MODIFY_AUDIO_ROUTING);
Resources mockResources = Mockito.mock(Resources.class);
when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME);
- Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
+ Context realContext = mInstrumentation.getContext();
Context mockContext = Mockito.mock(Context.class);
when(mockContext.getResources()).thenReturn(mockResources);
// The bluetooth stack needs the application info, but we cannot use a spy because the
@@ -135,6 +141,11 @@
clearInvocations(mOnDeviceRouteChangedListener);
}
+ @After
+ public void tearDown() {
+ mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
+ }
+
@Test
public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() {
assertThat(mControllerUnderTest.getSelectedRoute().getType())
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java b/services/tests/mockingservicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
index a8faa54..ad68de8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
@@ -196,6 +196,9 @@
when(mMockResources.getBoolean(
com.android.internal.R.bool.config_batterySaverStickyBehaviourDisabled))
.thenReturn(false);
+ when(mMockResources.getBoolean(
+ com.android.internal.R.bool.config_batterySaverTurnedOffNotificationEnabled))
+ .thenReturn(true);
when(mMockResources.getInteger(
com.android.internal.R.integer.config_dynamicPowerSavingsDefaultDisableThreshold))
.thenReturn(80);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 548fae7..a1101cd 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -578,45 +578,136 @@
// First wakelock, acquired once, not currently held
mMockClock.realtime = 1000;
- mBatteryStatsImpl.noteStartWakeLocked(10100, 100, null, "wakeLock1", null,
- BatteryStats.WAKE_TYPE_PARTIAL, false);
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10100, 100, null, "wakeLock1", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
mMockClock.realtime = 3000;
- mBatteryStatsImpl.noteStopWakeLocked(10100, 100, null, "wakeLock1", null,
- BatteryStats.WAKE_TYPE_PARTIAL);
+ mBatteryStatsImpl.noteStopWakeLocked(
+ 10100, 100, null, "wakeLock1", null, BatteryStats.WAKE_TYPE_PARTIAL);
// Second wakelock, acquired twice, still held
mMockClock.realtime = 4000;
- mBatteryStatsImpl.noteStartWakeLocked(10200, 101, null, "wakeLock2", null,
- BatteryStats.WAKE_TYPE_PARTIAL, false);
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10200, 101, null, "wakeLock2", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
mMockClock.realtime = 5000;
- mBatteryStatsImpl.noteStopWakeLocked(10200, 101, null, "wakeLock2", null,
- BatteryStats.WAKE_TYPE_PARTIAL);
+ mBatteryStatsImpl.noteStopWakeLocked(
+ 10200, 101, null, "wakeLock2", null, BatteryStats.WAKE_TYPE_PARTIAL);
mMockClock.realtime = 6000;
- mBatteryStatsImpl.noteStartWakeLocked(10200, 101, null, "wakeLock2", null,
- BatteryStats.WAKE_TYPE_PARTIAL, false);
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10200, 101, null, "wakeLock2", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
+
+ // Third and fourth wakelocks, overlapped with each other.
+ mMockClock.realtime = 7000;
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10300, 102, null, "wakeLock3", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
+
+ mMockClock.realtime = 8000;
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10400, 103, null, "wakeLock4", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
mMockClock.realtime = 9000;
+ mBatteryStatsImpl.noteStopWakeLocked(
+ 10400, 103, null, "wakeLock4", null, BatteryStats.WAKE_TYPE_PARTIAL);
- List<WakeLockStats.WakeLock> wakeLockStats =
- mBatteryStatsImpl.getWakeLockStats().getWakeLocks();
- assertThat(wakeLockStats).hasSize(2);
+ mMockClock.realtime = 10000;
+ mBatteryStatsImpl.noteStartWakeLocked(
+ 10400, 104, null, "wakeLock5", null, BatteryStats.WAKE_TYPE_PARTIAL, false);
- WakeLockStats.WakeLock wakeLock1 = wakeLockStats.stream()
- .filter(wl -> wl.uid == 10100 && wl.name.equals("wakeLock1")).findFirst().get();
+ mMockClock.realtime = 11000;
+ mBatteryStatsImpl.noteStopWakeLocked(
+ 10400, 104, null, "wakeLock5", null, BatteryStats.WAKE_TYPE_PARTIAL);
- assertThat(wakeLock1.timesAcquired).isEqualTo(1);
- assertThat(wakeLock1.timeHeldMs).isEqualTo(0); // Not currently held
- assertThat(wakeLock1.totalTimeHeldMs).isEqualTo(2000); // 3000-1000
+ mMockClock.realtime = 12000;
+ mBatteryStatsImpl.noteStopWakeLocked(
+ 10300, 102, null, "wakeLock3", null, BatteryStats.WAKE_TYPE_PARTIAL);
- WakeLockStats.WakeLock wakeLock2 = wakeLockStats.stream()
- .filter(wl -> wl.uid == 10200 && wl.name.equals("wakeLock2")).findFirst().get();
+ mMockClock.realtime = 13000;
- assertThat(wakeLock2.timesAcquired).isEqualTo(2);
- assertThat(wakeLock2.timeHeldMs).isEqualTo(3000); // 9000-6000
- assertThat(wakeLock2.totalTimeHeldMs).isEqualTo(4000); // (5000-4000) + (9000-6000)
+ // Verify un-aggregated wakelocks.
+ WakeLockStats wakeLockStats = mBatteryStatsImpl.getWakeLockStats();
+ List<WakeLockStats.WakeLock> wakeLockList = wakeLockStats.getWakeLocks();
+ assertThat(wakeLockList).hasSize(4);
+
+ WakeLockStats.WakeLock wakeLock1 = getWakeLockFromList(wakeLockList, 10100, "wakeLock1");
+ assertThat(wakeLock1.isAggregated).isFalse();
+ assertThat(wakeLock1.totalWakeLockData.timesAcquired).isEqualTo(1);
+ assertThat(wakeLock1.totalWakeLockData.timeHeldMs).isEqualTo(0); // Not currently held
+ assertThat(wakeLock1.totalWakeLockData.totalTimeHeldMs).isEqualTo(2000); // 3000-1000
+
+ WakeLockStats.WakeLock wakeLock3 = getWakeLockFromList(wakeLockList, 10300, "wakeLock3");
+ assertThat(wakeLock3.isAggregated).isFalse();
+ assertThat(wakeLock3.totalWakeLockData.timesAcquired).isEqualTo(1);
+ assertThat(wakeLock3.totalWakeLockData.timeHeldMs).isEqualTo(0); // Not currently held
+ // (8000-7000)/2 + (9000-8000)/3 + (10000-9000)/2 + (11000-10000)/3 + (12000-11000)/2
+ assertThat(wakeLock3.totalWakeLockData.totalTimeHeldMs).isEqualTo(2166);
+
+ WakeLockStats.WakeLock wakeLock4 = getWakeLockFromList(wakeLockList, 10400, "wakeLock4");
+ assertThat(wakeLock4.isAggregated).isFalse();
+ assertThat(wakeLock4.totalWakeLockData.timesAcquired).isEqualTo(1);
+ assertThat(wakeLock4.totalWakeLockData.timeHeldMs).isEqualTo(0); // Not currently held
+ assertThat(wakeLock4.totalWakeLockData.totalTimeHeldMs).isEqualTo(333); // (9000-8000)/3
+
+ WakeLockStats.WakeLock wakeLock5 = getWakeLockFromList(wakeLockList, 10400, "wakeLock5");
+ assertThat(wakeLock5.isAggregated).isFalse();
+ assertThat(wakeLock5.totalWakeLockData.timesAcquired).isEqualTo(1);
+ assertThat(wakeLock5.totalWakeLockData.timeHeldMs).isEqualTo(0); // Not currently held
+ assertThat(wakeLock5.totalWakeLockData.totalTimeHeldMs).isEqualTo(333); // (11000-10000)/3
+
+ // Verify aggregated wakelocks.
+ List<WakeLockStats.WakeLock> aggregatedWakeLockList =
+ wakeLockStats.getAggregatedWakeLocks();
+ assertThat(aggregatedWakeLockList).hasSize(4);
+
+ WakeLockStats.WakeLock aggregatedWakeLock1 =
+ getAggregatedWakeLockFromList(aggregatedWakeLockList, 10100);
+ assertThat(aggregatedWakeLock1.isAggregated).isTrue();
+ assertThat(aggregatedWakeLock1.totalWakeLockData.timesAcquired).isEqualTo(1);
+ // Not currently held
+ assertThat(aggregatedWakeLock1.totalWakeLockData.timeHeldMs).isEqualTo(0);
+ // 3000-1000
+ assertThat(aggregatedWakeLock1.totalWakeLockData.totalTimeHeldMs).isEqualTo(2000);
+
+ WakeLockStats.WakeLock aggregatedWakeLock2 =
+ getAggregatedWakeLockFromList(aggregatedWakeLockList, 10200);
+ assertThat(aggregatedWakeLock2.isAggregated).isTrue();
+ assertThat(aggregatedWakeLock2.totalWakeLockData.timesAcquired).isEqualTo(2);
+ assertThat(aggregatedWakeLock2.totalWakeLockData.timeHeldMs).isEqualTo(7000); // 13000-6000
+ // (5000-4000) + (13000-6000)
+ assertThat(aggregatedWakeLock2.totalWakeLockData.totalTimeHeldMs)
+ .isEqualTo(8000);
+
+ WakeLockStats.WakeLock aggregatedWakeLock3 =
+ getAggregatedWakeLockFromList(aggregatedWakeLockList, 10300);
+ assertThat(aggregatedWakeLock3.isAggregated).isTrue();
+ assertThat(aggregatedWakeLock3.totalWakeLockData.timesAcquired).isEqualTo(1);
+ // Not currently held
+ assertThat(aggregatedWakeLock3.totalWakeLockData.timeHeldMs).isEqualTo(0);
+ // 12000-7000
+ assertThat(aggregatedWakeLock3.totalWakeLockData.totalTimeHeldMs).isEqualTo(5000);
+
+ WakeLockStats.WakeLock aggregatedWakeLock4 =
+ getAggregatedWakeLockFromList(aggregatedWakeLockList, 10400);
+ assertThat(aggregatedWakeLock4.isAggregated).isTrue();
+ assertThat(aggregatedWakeLock4.totalWakeLockData.timesAcquired).isEqualTo(2);
+ // Not currently held
+ assertThat(aggregatedWakeLock4.totalWakeLockData.timeHeldMs).isEqualTo(0);
+ assertThat(aggregatedWakeLock4.totalWakeLockData.totalTimeHeldMs)
+ .isEqualTo(2000); // (9000-8000) + (11000-10000)
+ }
+
+ private WakeLockStats.WakeLock getAggregatedWakeLockFromList(
+ List<WakeLockStats.WakeLock> wakeLockList, final int uid) {
+ return getWakeLockFromList(wakeLockList, uid, WakeLockStats.WakeLock.NAME_AGGREGATED);
+ }
+
+ private WakeLockStats.WakeLock getWakeLockFromList(
+ List<WakeLockStats.WakeLock> wakeLockList, final int uid, final String name) {
+ return wakeLockList.stream()
+ .filter(wl -> wl.uid == uid && wl.name.equals(name))
+ .findFirst()
+ .get();
}
@Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index ae6984e..374426a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -55,7 +55,6 @@
import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
import java.util.List;
@SmallTest
@@ -70,11 +69,10 @@
private static final long MINUTE_IN_MS = 60 * 1000;
private static final double PRECISION = 0.00001;
- private File mHistoryDir;
-
@Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule =
- new BatteryUsageStatsRule(12345, mHistoryDir)
+ new BatteryUsageStatsRule(12345)
+ .createTempDirectory()
.setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0)
.setAveragePower(PowerProfile.POWER_AUDIO, 720.0);
@@ -83,9 +81,6 @@
@Before
public void setup() throws IOException {
- mHistoryDir = Files.createTempDirectory("BatteryUsageStatsProviderTest").toFile();
- clearDirectory(mHistoryDir);
-
if (RavenwoodRule.isUnderRavenwood()) {
mContext = mock(Context.class);
SensorManager sensorManager = mock(SensorManager.class);
@@ -95,17 +90,6 @@
}
}
- private void clearDirectory(File dir) {
- if (dir.exists()) {
- for (File child : dir.listFiles()) {
- if (child.isDirectory()) {
- clearDirectory(child);
- }
- child.delete();
- }
- }
- }
-
@Test
public void test_getBatteryUsageStats() {
BatteryStatsImpl batteryStats = prepareBatteryStats();
@@ -423,7 +407,7 @@
}
PowerStatsStore powerStatsStore = new PowerStatsStore(
- new File(mHistoryDir, "powerstatsstore"),
+ new File(mStatsRule.getHistoryDir(), "powerstatsstore"),
mStatsRule.getHandler(), null);
powerStatsStore.reset();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 7e8fa55..296ad0e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -35,7 +35,6 @@
import android.os.HandlerThread;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
-import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -50,6 +49,8 @@
import org.mockito.stubbing.Answer;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
import java.util.Arrays;
@SuppressWarnings("SynchronizeOnNonFinalField")
@@ -62,7 +63,9 @@
private final PowerProfile mPowerProfile;
private final MockClock mMockClock = new MockClock();
- private final File mHistoryDir;
+ private String mTestName;
+ private boolean mCreateTempDirectory;
+ private File mHistoryDir;
private MockBatteryStatsImpl mBatteryStats;
private Handler mHandler;
@@ -80,32 +83,30 @@
private Throwable mThrowable;
public BatteryUsageStatsRule() {
- this(0, null);
+ this(0);
}
public BatteryUsageStatsRule(long currentTime) {
- this(currentTime, null);
- }
-
- public BatteryUsageStatsRule(long currentTime, File historyDir) {
mHandler = mock(Handler.class);
mPowerProfile = spy(new PowerProfile());
mMockClock.currentTime = currentTime;
- mHistoryDir = historyDir;
-
- if (!RavenwoodRule.isUnderRavenwood()) {
- lateInitBatteryStats();
- }
-
mCpusByPolicy.put(0, new int[]{0, 1, 2, 3});
mCpusByPolicy.put(4, new int[]{4, 5, 6, 7});
mFreqsByPolicy.put(0, new int[]{300000, 1000000, 2000000});
mFreqsByPolicy.put(4, new int[]{300000, 1000000, 2500000, 3000000});
}
- private void lateInitBatteryStats() {
+ private void initBatteryStats() {
if (mBatteryStats != null) return;
+ if (mCreateTempDirectory) {
+ try {
+ mHistoryDir = Files.createTempDirectory(mTestName).toFile();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ clearDirectory();
+ }
mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler);
mBatteryStats.setPowerProfile(mPowerProfile);
mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
@@ -138,6 +139,15 @@
return mHandler;
}
+ public File getHistoryDir() {
+ return mHistoryDir;
+ }
+
+ public BatteryUsageStatsRule createTempDirectory() {
+ mCreateTempDirectory = true;
+ return this;
+ }
+
public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) {
mPowerProfile.forceInitForTesting(InstrumentationRegistry.getContext(), xmlId);
return this;
@@ -269,6 +279,7 @@
@Override
public Statement apply(Statement base, Description description) {
+ mTestName = description.getClassName() + "#" + description.getMethodName();
return new Statement() {
@Override
public void evaluate() throws Throwable {
@@ -280,7 +291,7 @@
}
private void before() {
- lateInitBatteryStats();
+ initBatteryStats();
HandlerThread bgThread = new HandlerThread("bg thread");
bgThread.setUncaughtExceptionHandler((thread, throwable)-> {
mThrowable = throwable;
@@ -324,6 +335,9 @@
}
public MockBatteryStatsImpl getBatteryStats() {
+ if (mBatteryStats == null) {
+ initBatteryStats();
+ }
return mBatteryStats;
}
@@ -397,4 +411,19 @@
}
return null;
}
+
+ public void clearDirectory() {
+ clearDirectory(mHistoryDir);
+ }
+
+ private void clearDirectory(File dir) {
+ if (dir.exists()) {
+ for (File child : dir.listFiles()) {
+ if (child.isDirectory()) {
+ clearDirectory(child);
+ }
+ child.delete();
+ }
+ }
+ }
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
index af5b462..2ea86a4 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
@@ -58,7 +58,7 @@
@Before
public void setup() throws ParseException {
- mHistory = new BatteryStatsHistory(32, 1024,
+ mHistory = new BatteryStatsHistory(1024,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null);
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 5c6f3c9..a529382 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -205,6 +205,7 @@
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
@@ -2143,12 +2144,14 @@
assertFalse(mService.isUidNetworkingBlocked(UID_E, false));
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testBackgroundChainEnabled() throws Exception {
verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true);
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testBackgroundChainOnProcStateChange() throws Exception {
@@ -2178,6 +2181,7 @@
assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testBackgroundChainOnAllowlistChange() throws Exception {
@@ -2216,6 +2220,7 @@
assertFalse(mService.isUidNetworkingBlocked(UID_B, false));
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testBackgroundChainOnTempAllowlistChange() throws Exception {
@@ -2245,6 +2250,7 @@
assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testUidObserverFiltersProcStateChanges() throws Exception {
@@ -2307,6 +2313,7 @@
waitForUidEventHandlerIdle();
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testUidObserverFiltersStaleChanges() throws Exception {
@@ -2327,6 +2334,7 @@
waitForUidEventHandlerIdle();
}
+ @Ignore("Temporarily disabled until the feature is enabled")
@Test
@RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
public void testUidObserverFiltersCapabilityChanges() throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
index f5c6795..771a765 100644
--- a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
+++ b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
@@ -92,7 +92,7 @@
public void testNonSearchable() {
// test basic array & hashmap
Searchables searchables = new Searchables(mContext, 0);
- searchables.updateSearchableList();
+ searchables.updateSearchableListIfNeeded();
// confirm that we return null for non-searchy activities
ComponentName nonActivity = new ComponentName("com.android.frameworks.servicestests",
@@ -121,7 +121,7 @@
doReturn(true).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt());
Searchables searchables = new Searchables(mContext, 0);
- searchables.updateSearchableList();
+ searchables.updateSearchableListIfNeeded();
// tests with "real" searchables (deprecate, this should be a unit test)
ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
int count = searchablesList.size();
@@ -139,7 +139,7 @@
doReturn(false).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt());
Searchables searchables = new Searchables(mContext, 0);
- searchables.updateSearchableList();
+ searchables.updateSearchableListIfNeeded();
ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
assertNotNull(searchablesList);
MoreAsserts.assertEmpty(searchablesList);
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 65662d6..2039f93 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -83,6 +83,13 @@
}
}
+ @Override
+ public void installExistingPackageForAllUsers(Context context, String packageName) {
+ for (int userId : mUsers) {
+ installPackageForUser(packageName, userId);
+ }
+ }
+
private void enablePackageForUser(String packageName, boolean enable, int userId) {
Map<Integer, PackageInfo> userPackages = mPackages.get(packageName);
if (userPackages == null) {
@@ -93,6 +100,17 @@
setPackageInfoForUser(userId, packageInfo);
}
+ private void installPackageForUser(String packageName, int userId) {
+ Map<Integer, PackageInfo> userPackages = mPackages.get(packageName);
+ if (userPackages == null) {
+ return;
+ }
+ PackageInfo packageInfo = userPackages.get(userId);
+ packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
+ packageInfo.applicationInfo.privateFlags &= (~ApplicationInfo.PRIVATE_FLAG_HIDDEN);
+ setPackageInfoForUser(userId, packageInfo);
+ }
+
@Override
public boolean systemIsDebuggable() { return mIsDebuggable; }
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index 5a06327..53c172a 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -1549,6 +1549,31 @@
Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
}
+ @Test
+ @RequiresFlagsEnabled("android.webkit.update_service_v2")
+ public void testDefaultWebViewPackageInstalling() {
+ String testPackage = "testDefault";
+ WebViewProviderInfo[] packages =
+ new WebViewProviderInfo[] {
+ new WebViewProviderInfo(
+ testPackage,
+ "",
+ true /* default available */,
+ false /* fallback */,
+ null)
+ };
+ setupWithPackages(packages);
+ mTestSystemImpl.setPackageInfo(
+ createPackageInfo(
+ testPackage, true /* enabled */, true /* valid */, false /* installed */));
+
+ // Check that the boot time logic tries to install the default package.
+ runWebViewBootPreparationOnMainSync();
+ Mockito.verify(mTestSystemImpl)
+ .installExistingPackageForAllUsers(
+ Matchers.anyObject(), Mockito.eq(testPackage));
+ }
+
private void testDefaultPackageChosen(PackageInfo packageInfo) {
WebViewProviderInfo[] packages =
new WebViewProviderInfo[] {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index cff7f46..715c9d4 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -12008,7 +12008,7 @@
// style + self managed call - bypasses block
when(mTelecomManager.isInSelfManagedCall(
- r.getSbn().getPackageName(), r.getUser(), true)).thenReturn(true);
+ r.getSbn().getPackageName(), true)).thenReturn(true);
assertThat(mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(),
r.getSbn().getId(), r.getSbn().getTag(), r, false, false)).isTrue();
@@ -12091,7 +12091,7 @@
// style + self managed call - bypasses block
mService.clearNotifications();
reset(mUsageStats);
- when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), r.getUser(), true))
+ when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), true))
.thenReturn(true);
mService.addEnqueuedNotification(r);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index b431888..3e59878 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -35,8 +35,8 @@
import android.content.ContentResolver;
import android.content.ContextWrapper;
import android.content.pm.PackageManagerInternal;
+import android.os.ExternalVibrationScale;
import android.os.Handler;
-import android.os.IExternalVibratorService;
import android.os.PowerManagerInternal;
import android.os.UserHandle;
import android.os.VibrationAttributes;
@@ -49,6 +49,7 @@
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -119,29 +120,65 @@
public void testGetExternalVibrationScale() {
setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
- assertEquals(IExternalVibratorService.SCALE_VERY_HIGH,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_VERY_HIGH,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
- assertEquals(IExternalVibratorService.SCALE_HIGH,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_HIGH,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW);
- assertEquals(IExternalVibratorService.SCALE_NONE,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_NONE,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM);
- assertEquals(IExternalVibratorService.SCALE_LOW,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_LOW,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
- assertEquals(IExternalVibratorService.SCALE_VERY_LOW,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
// Vibration setting being bypassed will use default setting and not scale.
- assertEquals(IExternalVibratorService.SCALE_NONE,
- mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_NONE,
+ mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() {
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
+
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
+
+ assertEquals(0.5f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH));
+ assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
+ assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION));
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+ // Vibration setting being bypassed will apply adaptive haptics scales.
+ assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void testAdaptiveHapticsScale_flagDisabled_adaptiveHapticScaleAlwaysNone() {
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
+
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
+
+ assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH));
+ assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
+ assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION));
}
@Test
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index 2823223..417fbd0 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -35,7 +35,6 @@
import android.content.ComponentName;
import android.content.pm.PackageManagerInternal;
import android.frameworks.vibrator.ScaleParam;
-import android.frameworks.vibrator.VibrationParam;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -55,8 +54,6 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -135,7 +132,7 @@
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
mVibratorControlService.onRequestVibrationParamsComplete(token,
- generateVibrationParams(vibrationScales));
+ VibrationParamGenerator.generateVibrationParams(vibrationScales));
verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f);
@@ -162,7 +159,7 @@
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
mVibratorControlService.onRequestVibrationParamsComplete(new Binder(),
- generateVibrationParams(vibrationScales));
+ VibrationParamGenerator.generateVibrationParams(vibrationScales));
verifyZeroInteractions(mMockVibrationScaler);
}
@@ -175,7 +172,8 @@
vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
- mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
mFakeVibratorController);
verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
@@ -193,7 +191,8 @@
vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
- mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
mFakeVibratorController);
verifyZeroInteractions(mMockVibrationScaler);
@@ -268,28 +267,6 @@
}
}
- private VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) {
- List<VibrationParam> vibrationParamList = new ArrayList<>();
- for (int i = 0; i < vibrationScales.size(); i++) {
- int type = vibrationScales.keyAt(i);
- float scale = vibrationScales.valueAt(i);
-
- vibrationParamList.add(generateVibrationParam(type, scale));
- }
-
- return vibrationParamList.toArray(new VibrationParam[0]);
- }
-
- private VibrationParam generateVibrationParam(int type, float scale) {
- ScaleParam scaleParam = new ScaleParam();
- scaleParam.typesMask = type;
- scaleParam.scale = scale;
- VibrationParam vibrationParam = new VibrationParam();
- vibrationParam.setScale(scaleParam);
-
- return vibrationParam;
- }
-
private int buildVibrationTypesMask(int... types) {
int typesMask = 0;
for (int type : types) {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index e7571ef..ed89ccf 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -50,6 +50,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
+import android.frameworks.vibrator.ScaleParam;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
@@ -59,16 +60,17 @@
import android.media.AudioManager;
import android.os.CombinedVibration;
import android.os.ExternalVibration;
+import android.os.ExternalVibrationScale;
import android.os.Handler;
import android.os.IBinder;
import android.os.IExternalVibrationController;
-import android.os.IExternalVibratorService;
import android.os.IVibratorStateListener;
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.Process;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.VibrationAttributes;
@@ -82,6 +84,10 @@
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.util.SparseArray;
@@ -153,6 +159,8 @@
public MockitoRule rule = MockitoJUnit.rule();
@Rule
public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -185,8 +193,10 @@
private Context mContextSpy;
private TestLooper mTestLooper;
private FakeVibrator mVibrator;
+ private FakeVibratorController mFakeVibratorController;
private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
private VibratorManagerService.ExternalVibratorService mExternalVibratorService;
+ private VibratorControlService mVibratorControlService;
private VibrationConfig mVibrationConfig;
private InputManagerGlobal.TestSession mInputManagerGlobalSession;
private InputManager mInputManager;
@@ -197,6 +207,7 @@
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mIInputManagerMock);
mVibrationConfig = new VibrationConfig(mContextSpy.getResources());
+ mFakeVibratorController = new FakeVibratorController();
ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
@@ -310,6 +321,8 @@
if (service instanceof VibratorManagerService.ExternalVibratorService) {
mExternalVibratorService =
(VibratorManagerService.ExternalVibratorService) service;
+ } else if (service instanceof VibratorControlService) {
+ mVibratorControlService = (VibratorControlService) service;
}
}
@@ -321,9 +334,13 @@
VibratorControllerHolder createVibratorControllerHolder() {
VibratorControllerHolder holder = new VibratorControllerHolder();
- holder.setVibratorController(new FakeVibratorController());
+ holder.setVibratorController(mFakeVibratorController);
return holder;
}
+
+ boolean isServiceDeclared(String name) {
+ return true;
+ }
});
return mService;
}
@@ -1108,12 +1125,13 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
controller, firstToken);
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
RINGTONE_ATTRS);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// The external vibration should have been cancelled
verify(controller).mute();
assertEquals(Arrays.asList(false, true, false),
@@ -1708,13 +1726,14 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
mock(IExternalVibrationController.class), binderToken);
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart(
+ externalVibration);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
when(mVirtualDeviceManagerInternalMock.isAppRunningOnAnyVirtualDevice(UID))
.thenReturn(true);
scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
}
@Test
@@ -1727,10 +1746,11 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
mock(IExternalVibrationController.class), binderToken);
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart(
+ externalVibration);
mExternalVibratorService.onExternalVibrationStop(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
assertEquals(Arrays.asList(false, true, false),
mVibratorProviders.get(1).getExternalControlStates());
@@ -1753,17 +1773,19 @@
ExternalVibration firstVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
firstController, firstToken);
- int firstScale = mExternalVibratorService.onExternalVibrationStart(firstVibration);
+ ExternalVibrationScale firstScale =
+ mExternalVibratorService.onExternalVibrationStart(firstVibration);
AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build();
ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME,
ringtoneAudioAttrs, secondController, secondToken);
- int secondScale = mExternalVibratorService.onExternalVibrationStart(secondVibration);
+ ExternalVibrationScale secondScale =
+ mExternalVibratorService.onExternalVibrationStart(secondVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, firstScale);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, secondScale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, firstScale.scaleLevel);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, secondScale.scaleLevel);
verify(firstController).mute();
verify(secondController, never()).mute();
// Set external control called only once.
@@ -1799,8 +1821,9 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
mock(IExternalVibrationController.class));
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is cancelled.
assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -1825,9 +1848,10 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS,
mock(IExternalVibrationController.class));
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
// External vibration is ignored.
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is not cancelled.
assertFalse(waitUntil(s -> !s.isVibrating(1), service, CLEANUP_TIMEOUT_MILLIS));
@@ -1852,8 +1876,9 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class));
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is cancelled.
assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
@@ -1879,9 +1904,10 @@
ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
AUDIO_NOTIFICATION_ATTRS,
mock(IExternalVibrationController.class));
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
// New vibration is ignored.
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
// Vibration is not cancelled.
assertFalse(waitUntil(s -> !s.isVibrating(1), service, CLEANUP_TIMEOUT_MILLIS));
@@ -1901,18 +1927,19 @@
setRingerMode(AudioManager.RINGER_MODE_SILENT);
createSystemReadyService();
- int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
setRingerMode(AudioManager.RINGER_MODE_NORMAL);
createSystemReadyService();
scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
setRingerMode(AudioManager.RINGER_MODE_VIBRATE);
createSystemReadyService();
scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
}
@Test
@@ -1935,14 +1962,14 @@
ExternalVibration vib = new ExternalVibration(UID, PACKAGE_NAME, audioAttrs,
mock(IExternalVibrationController.class));
- int scale = mExternalVibratorService.onExternalVibrationStart(vib);
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart(vib);
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
mExternalVibratorService.onExternalVibrationStop(vib);
scale = mExternalVibratorService.onExternalVibrationStart(
new ExternalVibration(UID, PACKAGE_NAME, flaggedAudioAttrs,
mock(IExternalVibrationController.class)));
- assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
}
@Test
@@ -1956,10 +1983,91 @@
.build();
createSystemReadyService();
- int scale = mExternalVibratorService.onExternalVibrationStart(
- new ExternalVibration(/* uid= */ 123, PACKAGE_NAME, flaggedAudioAttrs,
- mock(IExternalVibrationController.class)));
- assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(
+ new ExternalVibration(/* uid= */ 123, PACKAGE_NAME, flaggedAudioAttrs,
+ mock(IExternalVibrationController.class)));
+ assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void onExternalVibration_withAdaptiveHaptics_returnsCorrectAdaptiveScales()
+ throws RemoteException {
+ mockVibrators(1);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL,
+ IVibrator.CAP_AMPLITUDE_CONTROL);
+ createSystemReadyService();
+
+ SparseArray<Float> vibrationScales = new SparseArray<>();
+ vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+ vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
+ mFakeVibratorController);
+ ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ AUDIO_ALARM_ATTRS,
+ mock(IExternalVibrationController.class));
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ mExternalVibratorService.onExternalVibrationStop(externalVibration);
+
+ assertEquals(scale.adaptiveHapticsScale, 0.7f, 0);
+
+ externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ AUDIO_NOTIFICATION_ATTRS,
+ mock(IExternalVibrationController.class));
+ scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ mExternalVibratorService.onExternalVibrationStop(externalVibration);
+
+ assertEquals(scale.adaptiveHapticsScale, 0.4f, 0);
+
+ AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build();
+ externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ ringtoneAudioAttrs,
+ mock(IExternalVibrationController.class));
+ scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+
+ assertEquals(scale.adaptiveHapticsScale, 1f, 0);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void onExternalVibration_withAdaptiveHapticsFlagDisabled_alwaysReturnScaleNone()
+ throws RemoteException {
+ mockVibrators(1);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL,
+ IVibrator.CAP_AMPLITUDE_CONTROL);
+ createSystemReadyService();
+
+ SparseArray<Float> vibrationScales = new SparseArray<>();
+ vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+ vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
+ mFakeVibratorController);
+ ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ AUDIO_ALARM_ATTRS,
+ mock(IExternalVibrationController.class));
+ ExternalVibrationScale scale =
+ mExternalVibratorService.onExternalVibrationStart(externalVibration);
+ mExternalVibratorService.onExternalVibrationStop(externalVibration);
+
+ assertEquals(scale.adaptiveHapticsScale, 1f, 0);
+
+ mVibratorControlService.setVibrationParams(
+ VibrationParamGenerator.generateVibrationParams(vibrationScales),
+ mFakeVibratorController);
+ externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+ AUDIO_NOTIFICATION_ATTRS,
+ mock(IExternalVibrationController.class));
+ scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+
+ assertEquals(scale.adaptiveHapticsScale, 1f, 0);
}
@Test
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java b/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java
new file mode 100644
index 0000000..a606388
--- /dev/null
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.server.vibrator;
+
+import android.frameworks.vibrator.ScaleParam;
+import android.frameworks.vibrator.VibrationParam;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class that can be used to generate arrays of {@link VibrationParam}.
+ */
+public final class VibrationParamGenerator {
+ /**
+ * Generates an array of {@link VibrationParam}.
+ */
+ public static VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) {
+ List<VibrationParam> vibrationParamList = new ArrayList<>();
+ for (int i = 0; i < vibrationScales.size(); i++) {
+ int type = vibrationScales.keyAt(i);
+ float scale = vibrationScales.valueAt(i);
+
+ vibrationParamList.add(generateVibrationParam(type, scale));
+ }
+
+ return vibrationParamList.toArray(new VibrationParam[0]);
+ }
+
+ private static VibrationParam generateVibrationParam(int type, float scale) {
+ ScaleParam scaleParam = new ScaleParam();
+ scaleParam.typesMask = type;
+ scaleParam.scale = scale;
+ VibrationParam vibrationParam = new VibrationParam();
+ vibrationParam.setScale(scaleParam);
+
+ return vibrationParam;
+ }
+}
diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp
index e83f03d..b292294 100644
--- a/services/tests/wmtests/Android.bp
+++ b/services/tests/wmtests/Android.bp
@@ -22,16 +22,21 @@
genrule {
name: "wmtests.protologsrc",
srcs: [
+ ":protolog-impl",
":protolog-groups",
":wmtests-sources",
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
- "--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
- "--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
+ // Used for the ProtoLogIntegrationTest, where don't test decoding or writing to file
+ // so the parameters below are irrelevant.
+ "--viewer-config-file-path /some/unused/file/path.pb " +
+ "--legacy-viewer-config-file-path /some/unused/file/path.json.gz " +
+ "--legacy-output-file-path /some/unused/file/path.winscope " +
+ // END of irrelevant params.
"--output-srcjar $(out) " +
"$(locations :wmtests-sources)",
out: ["wmtests.protolog.srcjar"],
@@ -42,7 +47,7 @@
// We only want this apk build for tests.
srcs: [
- ":wmtests.protologsrc",
+ ":wmtests-sources",
"src/**/*.aidl",
],
diff --git a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
index af0d32c..c5bf78b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
@@ -27,6 +27,8 @@
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.LogLevel;
import com.android.internal.protolog.common.ProtoLog;
import org.junit.After;
@@ -47,9 +49,9 @@
@Ignore("b/163095037")
@Test
public void testProtoLogToolIntegration() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
runWith(mockedProtoLog, this::testProtoLog);
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP),
+ verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP),
anyInt(), eq(0b0010010111),
eq(com.android.internal.protolog.ProtoLogGroup.TEST_GROUP.isLogToLogcat()
? "Test completed successfully: %b %d %x %f %% %s"
@@ -66,18 +68,13 @@
/**
* Starts protolog for the duration of {@code runnable}, with a ProtoLogImpl instance installed.
*/
- private void runWith(ProtoLogImpl mockInstance, Runnable runnable) {
- ProtoLogImpl original = ProtoLogImpl.getSingleInstance();
- original.startProtoLog(null);
+ private void runWith(IProtoLog mockInstance, Runnable runnable) {
+ IProtoLog original = ProtoLog.getSingleInstance();
+ ProtoLogImpl.setSingleInstance(mockInstance);
try {
- ProtoLogImpl.setSingleInstance(mockInstance);
- try {
- runnable.run();
- } finally {
- ProtoLogImpl.setSingleInstance(original);
- }
+ runnable.run();
} finally {
- original.stopProtoLog(null, false);
+ ProtoLogImpl.setSingleInstance(original);
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 9930c88..f7c253d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -46,7 +46,6 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
@@ -73,6 +72,7 @@
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.util.ArraySet;
+import android.util.IntArray;
import android.util.SparseBooleanArray;
import android.view.Surface;
import android.window.TaskSnapshot;
@@ -527,20 +527,20 @@
mTaskPersister.mUserTaskIdsOverride.put(1, true);
mTaskPersister.mUserTaskIdsOverride.put(2, true);
mTaskPersister.mUserTasksOverride = new ArrayList<>();
- mTaskPersister.mUserTasksOverride.add(createTaskBuilder(".UserTask1").build());
- mTaskPersister.mUserTasksOverride.add(createTaskBuilder(".UserTask2").build());
+ mTaskPersister.mUserTasksOverride.add(mTasks.get(0));
+ mTaskPersister.mUserTasksOverride.add(mTasks.get(1));
// Assert no user tasks are initially loaded
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).hasLength(0);
// Load user 0 tasks
- mRecentTasks.loadUserRecentsLocked(TEST_USER_0_ID);
+ mRecentTasks.loadRecentTasksIfNeeded(TEST_USER_0_ID);
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).asList().contains(TEST_USER_0_ID);
assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
// Load user 1 tasks
- mRecentTasks.loadUserRecentsLocked(TEST_USER_1_ID);
+ mRecentTasks.loadRecentTasksIfNeeded(TEST_USER_1_ID);
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).asList().contains(TEST_USER_0_ID);
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).asList().contains(TEST_USER_1_ID);
assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
@@ -575,15 +575,15 @@
mTaskPersister.mUserTaskIdsOverride.put(2, true);
mTaskPersister.mUserTaskIdsOverride.put(3, true);
mTaskPersister.mUserTasksOverride = new ArrayList<>();
- mTaskPersister.mUserTasksOverride.add(createTaskBuilder(".UserTask1").build());
- mTaskPersister.mUserTasksOverride.add(createTaskBuilder(".UserTask2").build());
- mTaskPersister.mUserTasksOverride.add(createTaskBuilder(".UserTask3").build());
+ mTaskPersister.mUserTasksOverride.add(mTasks.get(0));
+ mTaskPersister.mUserTasksOverride.add(mTasks.get(1));
+ mTaskPersister.mUserTasksOverride.add(mTasks.get(2));
// Assert no user tasks are initially loaded
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).hasLength(0);
// Load tasks
- mRecentTasks.loadUserRecentsLocked(TEST_USER_0_ID);
+ mRecentTasks.loadRecentTasksIfNeeded(TEST_USER_0_ID);
assertThat(mRecentTasks.usersWithRecentsLoadedLocked()).asList().contains(TEST_USER_0_ID);
// Sort the time descendingly so the order should be in-sync with task recency (most
@@ -1419,8 +1419,6 @@
}
private List<RecentTaskInfo> getRecentTasks(int flags) {
- doNothing().when(mRecentTasks).loadUserRecentsLocked(anyInt());
- doReturn(true).when(mRecentTasks).isUserRunning(anyInt(), anyInt());
return mRecentTasks.getRecentTasks(MAX_VALUE, flags, true /* getTasksAllowed */,
TEST_USER_0_ID, 0 /* callingUid */).getList();
}
@@ -1590,19 +1588,20 @@
}
@Override
- SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
+ SparseBooleanArray readPersistedTaskIdsFromFileForUser(int userId) {
if (mUserTaskIdsOverride != null) {
return mUserTaskIdsOverride;
}
- return super.loadPersistedTaskIdsForUser(userId);
+ return super.readPersistedTaskIdsFromFileForUser(userId);
}
@Override
- List<Task> restoreTasksForUserLocked(int userId, SparseBooleanArray preaddedTasks) {
+ ArrayList<Task> restoreTasksForUserLocked(int userId, RecentTaskFiles recentTaskFiles,
+ IntArray existedTaskIds) {
if (mUserTasksOverride != null) {
return mUserTasksOverride;
}
- return super.restoreTasksForUserLocked(userId, preaddedTasks);
+ return super.restoreTasksForUserLocked(userId, recentTaskFiles, existedTaskIds);
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskPersisterTest.java
index 12ed3c2..319be89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskPersisterTest.java
@@ -29,8 +29,6 @@
import android.platform.test.annotations.Presubmit;
import android.util.SparseBooleanArray;
-import androidx.test.filters.FlakyTest;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -81,7 +79,7 @@
}
mTaskPersister.writePersistedTaskIdsForUser(taskIdsOnFile, mTestUserId);
SparseBooleanArray newTaskIdsOnFile = mTaskPersister
- .loadPersistedTaskIdsForUser(mTestUserId);
+ .readPersistedTaskIdsFromFileForUser(mTestUserId);
assertEquals("TaskIds written differ from TaskIds read back from file",
taskIdsOnFile, newTaskIdsOnFile);
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 889f842..c217780 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -2582,7 +2582,6 @@
if (anyPackagesAppearing()) {
initRecognizer(userHandle);
}
- return;
}
if (curInteractor != null) {
@@ -2631,15 +2630,16 @@
}
}
- // There is no interactor, so just deal with a simple recognizer.
- int change = isPackageDisappearing(curRecognizer.getPackageName());
- if (change == PACKAGE_PERMANENT_CHANGE
- || change == PACKAGE_TEMPORARY_CHANGE) {
- setCurRecognizer(findAvailRecognizer(null, userHandle), userHandle);
+ if (curRecognizer != null) {
+ int change = isPackageDisappearing(curRecognizer.getPackageName());
+ if (change == PACKAGE_PERMANENT_CHANGE
+ || change == PACKAGE_TEMPORARY_CHANGE) {
+ setCurRecognizer(findAvailRecognizer(null, userHandle), userHandle);
- } else if (isPackageModified(curRecognizer.getPackageName())) {
- setCurRecognizer(findAvailRecognizer(curRecognizer.getPackageName(),
- userHandle), userHandle);
+ } else if (isPackageModified(curRecognizer.getPackageName())) {
+ setCurRecognizer(findAvailRecognizer(curRecognizer.getPackageName(),
+ userHandle), userHandle);
+ }
}
}
}
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 08c76af..9792cdd 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -2797,13 +2797,10 @@
* calls for a given {@code packageName} and {@code userHandle}.
*
* @param packageName the package name of the app to check calls for.
- * @param userHandle the user handle on which to check for calls.
- * @param detectForAllUsers indicates if calls should be detected across all users. If it is
- * set to {@code true}, and the caller has the ability to interact
- * across users, the userHandle parameter is disregarded.
+ * @param userHandle the user handle to check calls for.
* @return {@code true} if there are ongoing calls, {@code false} otherwise.
- * @throws SecurityException if detectForAllUsers is true or userHandle is not the calling user
- * and the caller does not grant the ability to interact across users.
+ * @throws SecurityException if the userHandle is not the calling user and the caller does not
+ * grant the ability to interact across users.
* @hide
*/
@SystemApi
@@ -2811,11 +2808,45 @@
@RequiresPermission(allOf = {Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
public boolean isInSelfManagedCall(@NonNull String packageName,
- @NonNull UserHandle userHandle, boolean detectForAllUsers) {
+ @NonNull UserHandle userHandle) {
ITelecomService service = getTelecomService();
if (service != null) {
try {
return service.isInSelfManagedCall(packageName, userHandle,
+ mContext.getOpPackageName(), false);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException isInSelfManagedCall: " + e);
+ e.rethrowFromSystemServer();
+ return false;
+ }
+ } else {
+ throw new IllegalStateException("Telecom service is not present");
+ }
+ }
+
+ /**
+ * Determines whether there are any ongoing {@link PhoneAccount#CAPABILITY_SELF_MANAGED}
+ * calls for a given {@code packageName} amongst all users, given that detectForAllUsers is true
+ * and the caller has the ability to interact across users. If detectForAllUsers isn't enabled,
+ * the calls will be checked against the caller.
+ *
+ * @param packageName the package name of the app to check calls for.
+ * @param detectForAllUsers indicates if calls should be detected across all users.
+ * @return {@code true} if there are ongoing calls, {@code false} otherwise.
+ * @throws SecurityException if detectForAllUsers is true and the caller does not grant the
+ * ability to interact across users.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ @RequiresPermission(allOf = {Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ public boolean isInSelfManagedCall(@NonNull String packageName,
+ boolean detectForAllUsers) {
+ ITelecomService service = getTelecomService();
+ if (service != null) {
+ try {
+ return service.isInSelfManagedCall(packageName, null,
mContext.getOpPackageName(), detectForAllUsers);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException isInSelfManagedCall: " + e);
diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java
index 5af2c34..5524541 100644
--- a/telephony/java/android/service/euicc/EuiccService.java
+++ b/telephony/java/android/service/euicc/EuiccService.java
@@ -856,10 +856,22 @@
int slotId, IGetAvailableMemoryInBytesCallback callback) {
mExecutor.execute(
() -> {
- long availableMemoryInBytes =
- EuiccService.this.onGetAvailableMemoryInBytes(slotId);
+ long availableMemoryInBytes = EuiccManager.EUICC_MEMORY_FIELD_UNAVAILABLE;
+ String unsupportedOperationMessage = "";
try {
- callback.onSuccess(availableMemoryInBytes);
+ availableMemoryInBytes =
+ EuiccService.this.onGetAvailableMemoryInBytes(slotId);
+ } catch (UnsupportedOperationException e) {
+ unsupportedOperationMessage = e.getMessage();
+ }
+
+ try {
+ if (!unsupportedOperationMessage.isEmpty()) {
+ callback.onUnsupportedOperationException(
+ unsupportedOperationMessage);
+ } else {
+ callback.onSuccess(availableMemoryInBytes);
+ }
} catch (RemoteException e) {
// Can't communicate with the phone process; ignore.
}
diff --git a/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
index bd6d19b..e550e77 100644
--- a/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
+++ b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
@@ -19,4 +19,5 @@
/** @hide */
oneway interface IGetAvailableMemoryInBytesCallback {
void onSuccess(long availableMemoryInBytes);
+ void onUnsupportedOperationException(String message);
}
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index a487799..827ff4f 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -21,6 +21,9 @@
"mockito-target-minus-junit4",
"truth",
"platform-test-annotations",
+ "flickerlib-parsers",
+ "perfetto_trace_java_protos",
+ "flickerlib-trace_processor_shell",
],
java_resource_dirs: ["res"],
certificate: "platform",
diff --git a/tests/Internal/AndroidManifest.xml b/tests/Internal/AndroidManifest.xml
index dbba245..9a3fe61 100644
--- a/tests/Internal/AndroidManifest.xml
+++ b/tests/Internal/AndroidManifest.xml
@@ -19,7 +19,11 @@
package="com.android.internal.tests">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BIND_WALLPAPER"/>
- <application>
+ <!-- Allow the test to connect to perfetto trace processor -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <application
+ android:requestLegacyExternalStorage="true"
+ android:networkSecurityConfig="@xml/network_security_config">
<uses-library android:name="android.test.runner"/>
<service android:name="stub.DummyWallpaperService"
diff --git a/tests/Internal/res/xml/network_security_config.xml b/tests/Internal/res/xml/network_security_config.xml
new file mode 100644
index 0000000..fdf1dbb
--- /dev/null
+++ b/tests/Internal/res/xml/network_security_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">localhost</domain>
+ </domain-config>
+</network-security-config>
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
new file mode 100644
index 0000000..a64996c
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2019 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.protolog;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.internal.protolog.LegacyProtoLogImpl.PROTOLOG_VERSION;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.util.proto.ProtoInputStream;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.LinkedList;
+
+/**
+ * Test class for {@link ProtoLogImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class LegacyProtoLogImplTest {
+
+ private static final byte[] MAGIC_HEADER = new byte[]{
+ 0x9, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47
+ };
+
+ private LegacyProtoLogImpl mProtoLog;
+ private File mFile;
+
+ @Mock
+ private LegacyProtoLogViewerConfigReader mReader;
+
+ private final String mViewerConfigFilename = "unused/file/path";
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ final Context testContext = getInstrumentation().getContext();
+ mFile = testContext.getFileStreamPath("tracing_test.dat");
+ //noinspection ResultOfMethodCallIgnored
+ mFile.delete();
+ mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename,
+ 1024 * 1024, mReader, 1024);
+ }
+
+ @After
+ public void tearDown() {
+ if (mFile != null) {
+ //noinspection ResultOfMethodCallIgnored
+ mFile.delete();
+ }
+ ProtoLogImpl.setSingleInstance(null);
+ }
+
+ @Test
+ public void isEnabled_returnsFalseByDefault() {
+ assertFalse(mProtoLog.isProtoEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsTrueAfterStart() {
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ assertTrue(mProtoLog.isProtoEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsFalseAfterStop() {
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+ assertFalse(mProtoLog.isProtoEnabled());
+ }
+
+ @Test
+ public void logFile_startsWithMagicHeader() throws Exception {
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+
+ assertTrue("Log file should exist", mFile.exists());
+
+ byte[] header = new byte[MAGIC_HEADER.length];
+ try (InputStream is = new FileInputStream(mFile)) {
+ assertEquals(MAGIC_HEADER.length, is.read(header));
+ assertArrayEquals(MAGIC_HEADER, header);
+ }
+ }
+
+ @Test
+ public void log_logcatEnabledExternalMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f");
+ LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{true, 10000, 30000, "test", 0.000003});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO),
+ eq("test true 10000 % 0x7530 test 3.0E-6"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatEnabledInvalidMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f");
+ LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{true, 10000, 0.0001, 0.00002, "test"});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO),
+ eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatEnabledInlineMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+ LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+ new Object[]{5});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO), eq("test 5"));
+ verify(mReader, never()).getViewerString(anyLong());
+ }
+
+ @Test
+ public void log_logcatEnabledNoMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn(null);
+ LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{5});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatDisabled() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+ LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+ new Object[]{5});
+
+ verify(implSpy, never()).passToLogcat(any(), any(), any());
+ verify(mReader, never()).getViewerString(anyLong());
+ }
+
+ private static class ProtoLogData {
+ Long mMessageHash = null;
+ Long mElapsedTime = null;
+ LinkedList<String> mStrParams = new LinkedList<>();
+ LinkedList<Long> mSint64Params = new LinkedList<>();
+ LinkedList<Double> mDoubleParams = new LinkedList<>();
+ LinkedList<Boolean> mBooleanParams = new LinkedList<>();
+ }
+
+ private ProtoLogData readProtoLogSingle(ProtoInputStream ip) throws IOException {
+ while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (ip.getFieldNumber() == (int) ProtoLogFileProto.VERSION) {
+ assertEquals(PROTOLOG_VERSION, ip.readString(ProtoLogFileProto.VERSION));
+ continue;
+ }
+ if (ip.getFieldNumber() != (int) ProtoLogFileProto.LOG) {
+ continue;
+ }
+ long token = ip.start(ProtoLogFileProto.LOG);
+ ProtoLogData data = new ProtoLogData();
+ while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (ip.getFieldNumber()) {
+ case (int) ProtoLogMessage.MESSAGE_HASH: {
+ data.mMessageHash = ip.readLong(ProtoLogMessage.MESSAGE_HASH);
+ break;
+ }
+ case (int) ProtoLogMessage.ELAPSED_REALTIME_NANOS: {
+ data.mElapsedTime = ip.readLong(ProtoLogMessage.ELAPSED_REALTIME_NANOS);
+ break;
+ }
+ case (int) ProtoLogMessage.STR_PARAMS: {
+ data.mStrParams.add(ip.readString(ProtoLogMessage.STR_PARAMS));
+ break;
+ }
+ case (int) ProtoLogMessage.SINT64_PARAMS: {
+ data.mSint64Params.add(ip.readLong(ProtoLogMessage.SINT64_PARAMS));
+ break;
+ }
+ case (int) ProtoLogMessage.DOUBLE_PARAMS: {
+ data.mDoubleParams.add(ip.readDouble(ProtoLogMessage.DOUBLE_PARAMS));
+ break;
+ }
+ case (int) ProtoLogMessage.BOOLEAN_PARAMS: {
+ data.mBooleanParams.add(ip.readBoolean(ProtoLogMessage.BOOLEAN_PARAMS));
+ break;
+ }
+ }
+ }
+ ip.end(token);
+ return data;
+ }
+ return null;
+ }
+
+ @Test
+ public void log_protoEnabled() throws Exception {
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ long before = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+ 0b1110101001010100, null,
+ new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
+ long after = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+ try (InputStream is = new FileInputStream(mFile)) {
+ ProtoInputStream ip = new ProtoInputStream(is);
+ ProtoLogData data = readProtoLogSingle(ip);
+ assertNotNull(data);
+ assertEquals(1234, data.mMessageHash.longValue());
+ assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
+ assertArrayEquals(new String[]{"test"}, data.mStrParams.toArray());
+ assertArrayEquals(new Long[]{1L, 2L, 3L}, data.mSint64Params.toArray());
+ assertArrayEquals(new Double[]{0.4, 0.5, 0.6}, data.mDoubleParams.toArray());
+ assertArrayEquals(new Boolean[]{true}, data.mBooleanParams.toArray());
+ }
+ }
+
+ @Test
+ public void log_invalidParamsMask() throws Exception {
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ long before = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+ 0b01100100, null,
+ new Object[]{"test", 1, 0.1, true});
+ long after = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+ try (InputStream is = new FileInputStream(mFile)) {
+ ProtoInputStream ip = new ProtoInputStream(is);
+ ProtoLogData data = readProtoLogSingle(ip);
+ assertNotNull(data);
+ assertEquals(1234, data.mMessageHash.longValue());
+ assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
+ assertArrayEquals(new String[]{"test", "(INVALID PARAMS_MASK) true"},
+ data.mStrParams.toArray());
+ assertArrayEquals(new Long[]{1L}, data.mSint64Params.toArray());
+ assertArrayEquals(new Double[]{0.1}, data.mDoubleParams.toArray());
+ assertArrayEquals(new Boolean[]{}, data.mBooleanParams.toArray());
+ }
+ }
+
+ @Test
+ public void log_protoDisabled() throws Exception {
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+ mProtoLog.startProtoLog(mock(PrintWriter.class));
+ mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+ 0b11, null, new Object[]{true});
+ mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+ try (InputStream is = new FileInputStream(mFile)) {
+ ProtoInputStream ip = new ProtoInputStream(is);
+ ProtoLogData data = readProtoLogSingle(ip);
+ assertNull(data);
+ }
+ }
+
+ private enum TestProtoLogGroup implements IProtoLogGroup {
+ TEST_GROUP(true, true, false, "WindowManagetProtoLogTest");
+
+ private final boolean mEnabled;
+ private volatile boolean mLogToProto;
+ private volatile boolean mLogToLogcat;
+ private final String mTag;
+
+ /**
+ * @param enabled set to false to exclude all log statements for this group from
+ * compilation,
+ * they will not be available in runtime.
+ * @param logToProto enable binary logging for the group
+ * @param logToLogcat enable text logging for the group
+ * @param tag name of the source of the logged message
+ */
+ TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+ this.mEnabled = enabled;
+ this.mLogToProto = logToProto;
+ this.mLogToLogcat = logToLogcat;
+ this.mTag = tag;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ public boolean isLogToAny() {
+ return mLogToLogcat || mLogToProto;
+ }
+
+ @Override
+ public String getTag() {
+ return mTag;
+ }
+
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ this.mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ this.mLogToLogcat = logToLogcat;
+ }
+
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java
new file mode 100644
index 0000000..b9f1738
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 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.protolog;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import android.tracing.perfetto.CreateTlsStateArgs;
+import android.util.proto.ProtoInputStream;
+
+import com.android.internal.protolog.common.LogLevel;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import perfetto.protos.DataSourceConfigOuterClass;
+import perfetto.protos.ProtologCommon;
+import perfetto.protos.ProtologConfig;
+
+public class PerfettoDataSourceTest {
+ @Before
+ public void before() {
+ assumeTrue(android.tracing.Flags.perfettoProtolog());
+ }
+
+ @Test
+ public void noConfig() {
+ final ProtoLogDataSource.TlsState tlsState = createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig.newBuilder().build());
+
+ Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.WTF);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+ }
+
+ @Test
+ public void defaultTraceMode() {
+ final ProtoLogDataSource.TlsState tlsState = createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig.newBuilder()
+ .setProtologConfig(
+ ProtologConfig.ProtoLogConfig.newBuilder()
+ .setTracingMode(
+ ProtologConfig.ProtoLogConfig.TracingMode
+ .ENABLE_ALL)
+ .build()
+ ).build());
+
+ Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+ }
+
+ @Test
+ public void allEnabledTraceMode() {
+ final ProtoLogDataSource ds = new ProtoLogDataSource(() -> {}, () -> {}, () -> {});
+
+ final ProtoLogDataSource.TlsState tlsState = createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+ ProtologConfig.ProtoLogConfig.newBuilder()
+ .setTracingMode(
+ ProtologConfig.ProtoLogConfig.TracingMode.ENABLE_ALL)
+ .build()
+ ).build()
+ );
+
+ Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+ }
+
+ @Test
+ public void requireGroupTagInOverrides() {
+ Exception exception = assertThrows(RuntimeException.class, () -> {
+ createTlsState(DataSourceConfigOuterClass.DataSourceConfig.newBuilder()
+ .setProtologConfig(
+ ProtologConfig.ProtoLogConfig.newBuilder()
+ .addGroupOverrides(
+ ProtologConfig.ProtoLogGroup.newBuilder()
+ .setLogFrom(
+ ProtologCommon.ProtoLogLevel
+ .PROTOLOG_LEVEL_WARN)
+ .setCollectStacktrace(true)
+ )
+ .build()
+ ).build());
+ });
+
+ Truth.assertThat(exception).hasMessageThat().contains("group override without a group tag");
+ }
+
+ @Test
+ public void stackTraceCollection() {
+ final ProtoLogDataSource.TlsState tlsState = createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+ ProtologConfig.ProtoLogConfig.newBuilder()
+ .addGroupOverrides(
+ ProtologConfig.ProtoLogGroup.newBuilder()
+ .setGroupName("SOME_TAG")
+ .setCollectStacktrace(true)
+ )
+ .build()
+ ).build());
+
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isTrue();
+ }
+
+ @Test
+ public void groupLogFromOverrides() {
+ final ProtoLogDataSource.TlsState tlsState = createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+ ProtologConfig.ProtoLogConfig.newBuilder()
+ .addGroupOverrides(
+ ProtologConfig.ProtoLogGroup.newBuilder()
+ .setGroupName("SOME_TAG")
+ .setLogFrom(
+ ProtologCommon.ProtoLogLevel
+ .PROTOLOG_LEVEL_DEBUG)
+ .setCollectStacktrace(true)
+ )
+ .addGroupOverrides(
+ ProtologConfig.ProtoLogGroup.newBuilder()
+ .setGroupName("SOME_OTHER_TAG")
+ .setLogFrom(
+ ProtologCommon.ProtoLogLevel
+ .PROTOLOG_LEVEL_WARN)
+ )
+ .build()
+ ).build());
+
+ Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isTrue();
+
+ Truth.assertThat(tlsState.getLogFromLevel("SOME_OTHER_TAG")).isEqualTo(LogLevel.WARN);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_OTHER_TAG")).isFalse();
+
+ Truth.assertThat(tlsState.getLogFromLevel("UNKNOWN_TAG")).isEqualTo(LogLevel.WTF);
+ Truth.assertThat(tlsState.getShouldCollectStacktrace("UNKNOWN_TAG")).isFalse();
+ }
+
+ private ProtoLogDataSource.TlsState createTlsState(
+ DataSourceConfigOuterClass.DataSourceConfig config) {
+ final ProtoLogDataSource ds =
+ Mockito.spy(new ProtoLogDataSource(() -> {}, () -> {}, () -> {}));
+
+ ProtoInputStream configStream = new ProtoInputStream(config.toByteArray());
+ final ProtoLogDataSource.Instance dsInstance = Mockito.spy(
+ ds.createInstance(configStream, 8));
+ Mockito.doNothing().when(dsInstance).release();
+ final CreateTlsStateArgs mockCreateTlsStateArgs = Mockito.mock(CreateTlsStateArgs.class);
+ Mockito.when(mockCreateTlsStateArgs.getDataSourceInstanceLocked()).thenReturn(dsInstance);
+ return ds.createTlsState(mockCreateTlsStateArgs);
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
new file mode 100644
index 0000000..4c31105
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2024 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.protolog;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.io.File.createTempFile;
+import static java.nio.file.Files.createTempDirectory;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.tools.common.ScenarioBuilder;
+import android.tools.common.traces.protolog.ProtoLogTrace;
+import android.tools.device.traces.TraceConfig;
+import android.tools.device.traces.TraceConfigs;
+import android.tools.device.traces.io.ResultReader;
+import android.tools.device.traces.io.ResultWriter;
+import android.tools.device.traces.monitors.PerfettoTraceMonitor;
+import android.tracing.perfetto.DataSource;
+import android.util.proto.ProtoInputStream;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
+
+import com.google.common.truth.Truth;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Random;
+
+import perfetto.protos.Protolog;
+import perfetto.protos.ProtologCommon;
+
+/**
+ * Test class for {@link ProtoLogImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PerfettoProtoLogImplTest {
+ private final File mTracingDirectory = createTempDirectory("temp").toFile();
+
+ private final ResultWriter mWriter = new ResultWriter()
+ .forScenario(new ScenarioBuilder()
+ .forClass(createTempFile("temp", "").getName()).build())
+ .withOutputDir(mTracingDirectory)
+ .setRunComplete();
+
+ private final TraceConfigs mTraceConfig = new TraceConfigs(
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false)
+ );
+
+ private PerfettoProtoLogImpl mProtoLog;
+ private Protolog.ProtoLogViewerConfig.Builder mViewerConfigBuilder;
+ private File mFile;
+
+ private ProtoLogViewerConfigReader mReader;
+
+ public PerfettoProtoLogImplTest() throws IOException {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ final Context testContext = getInstrumentation().getContext();
+ mFile = testContext.getFileStreamPath("tracing_test.dat");
+ //noinspection ResultOfMethodCallIgnored
+ mFile.delete();
+
+ mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder()
+ .addGroups(
+ Protolog.ProtoLogViewerConfig.Group.newBuilder()
+ .setId(1)
+ .setName(TestProtoLogGroup.TEST_GROUP.toString())
+ .setTag(TestProtoLogGroup.TEST_GROUP.getTag())
+ ).addMessages(
+ Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(1)
+ .setMessage("My Test Debug Log Message %b")
+ .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG)
+ .setGroupId(1)
+ ).addMessages(
+ Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(2)
+ .setMessage("My Test Verbose Log Message %b")
+ .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE)
+ .setGroupId(1)
+ ).addMessages(
+ Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(3)
+ .setMessage("My Test Warn Log Message %b")
+ .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN)
+ .setGroupId(1)
+ ).addMessages(
+ Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(4)
+ .setMessage("My Test Error Log Message %b")
+ .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR)
+ .setGroupId(1)
+ ).addMessages(
+ Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(5)
+ .setMessage("My Test WTF Log Message %b")
+ .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF)
+ .setGroupId(1)
+ );
+
+ ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock(
+ ViewerConfigInputStreamProvider.class);
+ Mockito.when(viewerConfigInputStreamProvider.getInputStream())
+ .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray()));
+
+ mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+ mProtoLog = new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader);
+ }
+
+ @After
+ public void tearDown() {
+ if (mFile != null) {
+ //noinspection ResultOfMethodCallIgnored
+ mFile.delete();
+ }
+ ProtoLogImpl.setSingleInstance(null);
+ }
+
+ @Test
+ public void isEnabled_returnsFalseByDefault() {
+ assertFalse(mProtoLog.isProtoEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsTrueAfterStart() {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+ try {
+ traceMonitor.start();
+ assertTrue(mProtoLog.isProtoEnabled());
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+ }
+
+ @Test
+ public void isEnabled_returnsFalseAfterStop() {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+ try {
+ traceMonitor.start();
+ assertTrue(mProtoLog.isProtoEnabled());
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ assertFalse(mProtoLog.isProtoEnabled());
+ }
+
+ @Test
+ public void defaultMode() throws IOException {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build();
+ try {
+ traceMonitor.start();
+ // Shouldn't be logging anything except WTF unless explicitly requested in the group
+ // override.
+ mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(1);
+ Truth.assertThat(protolog.messages.getFirst().getLevel()).isEqualTo(LogLevel.WTF);
+ }
+
+ @Test
+ public void respectsOverrideConfigs_defaultMode() throws IOException {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+ List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+ TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true)))
+ .build();
+ try {
+ traceMonitor.start();
+ mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(5);
+ Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.DEBUG);
+ Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.VERBOSE);
+ Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WARN);
+ Truth.assertThat(protolog.messages.get(3).getLevel()).isEqualTo(LogLevel.ERROR);
+ Truth.assertThat(protolog.messages.get(4).getLevel()).isEqualTo(LogLevel.WTF);
+ }
+
+ @Test
+ public void respectsOverrideConfigs_allEnabledMode() throws IOException {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+ List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+ TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, false)))
+ .build();
+ try {
+ traceMonitor.start();
+ mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(3);
+ Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.WARN);
+ Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.ERROR);
+ Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WTF);
+ }
+
+ @Test
+ public void respectsAllEnabledMode() throws IOException {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of())
+ .build();
+ try {
+ traceMonitor.start();
+ mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+ LogDataType.BOOLEAN, null, new Object[]{true});
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(5);
+ Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.DEBUG);
+ Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.VERBOSE);
+ Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WARN);
+ Truth.assertThat(protolog.messages.get(3).getLevel()).isEqualTo(LogLevel.ERROR);
+ Truth.assertThat(protolog.messages.get(4).getLevel()).isEqualTo(LogLevel.WTF);
+ }
+
+ @Test
+ public void log_logcatEnabledExternalMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f");
+ PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{true, 10000, 30000, "test", 0.000003});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO),
+ eq("test true 10000 % 0x7530 test 3.0E-6"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatEnabledInvalidMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f");
+ PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{true, 10000, 0.0001, 0.00002, "test"});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO),
+ eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatEnabledInlineMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+ PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+ new Object[]{5});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO), eq("test 5"));
+ verify(mReader, never()).getViewerString(anyLong());
+ }
+
+ @Test
+ public void log_logcatEnabledNoMessage() {
+ when(mReader.getViewerString(anyLong())).thenReturn(null);
+ PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+ new Object[]{5});
+
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
+ verify(mReader).getViewerString(eq(1234L));
+ }
+
+ @Test
+ public void log_logcatDisabled() {
+ when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+ PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+
+ implSpy.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+ new Object[]{5});
+
+ verify(implSpy, never()).passToLogcat(any(), any(), any());
+ verify(mReader, never()).getViewerString(anyLong());
+ }
+
+ @Test
+ public void log_protoEnabled() throws Exception {
+ final long messageHash = addMessageToConfig(
+ ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO,
+ "My test message :: %s, %d, %o, %x, %f, %e, %g, %b");
+
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+ long before;
+ long after;
+ try {
+ traceMonitor.start();
+ assertTrue(mProtoLog.isProtoEnabled());
+
+ before = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
+ 0b1110101001010100, null,
+ new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
+ after = SystemClock.elapsedRealtimeNanos();
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(1);
+ Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
+ .isAtLeast(before);
+ Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
+ .isAtMost(after);
+ Truth.assertThat(protolog.messages.getFirst().getMessage())
+ .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true");
+ }
+
+ private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) {
+ final long messageId = new Random().nextLong();
+ mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(messageId)
+ .setMessage(message)
+ .setLevel(logLevel)
+ .setGroupId(1)
+ );
+
+ return messageId;
+ }
+
+ @Test
+ public void log_invalidParamsMask() {
+ final long messageHash = addMessageToConfig(
+ ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO,
+ "My test message :: %s, %d, %f, %b");
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+ long before;
+ long after;
+ try {
+ traceMonitor.start();
+ before = SystemClock.elapsedRealtimeNanos();
+ mProtoLog.log(
+ LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
+ 0b01100100, null,
+ new Object[]{"test", 1, 0.1, true});
+ after = SystemClock.elapsedRealtimeNanos();
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ assertThrows(IllegalStateException.class, reader::readProtoLogTrace);
+ }
+
+ @Test
+ public void log_protoDisabled() throws Exception {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build();
+ try {
+ traceMonitor.start();
+ mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+ 0b11, null, new Object[]{true});
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).isEmpty();
+ }
+
+ @Test
+ public void stackTraceTrimmed() throws IOException {
+ PerfettoTraceMonitor traceMonitor =
+ PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+ List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+ TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true)))
+ .build();
+ try {
+ traceMonitor.start();
+
+ ProtoLogImpl.setSingleInstance(mProtoLog);
+ ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1,
+ 0b11, null, true);
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+ Truth.assertThat(protolog.messages).hasSize(1);
+ String stacktrace = protolog.messages.getFirst().getStacktrace();
+ Truth.assertThat(stacktrace)
+ .doesNotContain(PerfettoProtoLogImpl.class.getSimpleName() + ".java");
+ Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java");
+ Truth.assertThat(stacktrace)
+ .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java");
+ Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName());
+ Truth.assertThat(stacktrace).contains("stackTraceTrimmed");
+ }
+
+ private enum TestProtoLogGroup implements IProtoLogGroup {
+ TEST_GROUP(true, true, false, "TEST_TAG");
+
+ private final boolean mEnabled;
+ private volatile boolean mLogToProto;
+ private volatile boolean mLogToLogcat;
+ private final String mTag;
+
+ /**
+ * @param enabled set to false to exclude all log statements for this group from
+ * compilation,
+ * they will not be available in runtime.
+ * @param logToProto enable binary logging for the group
+ * @param logToLogcat enable text logging for the group
+ * @param tag name of the source of the logged message
+ */
+ TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+ this.mEnabled = enabled;
+ this.mLogToProto = logToProto;
+ this.mLogToLogcat = logToLogcat;
+ this.mTag = tag;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ public boolean isLogToAny() {
+ return mLogToLogcat || mLogToProto;
+ }
+
+ @Override
+ public String getTag() {
+ return mTag;
+ }
+
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ this.mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ this.mLogToLogcat = logToLogcat;
+ }
+
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
index 7deb8c7..4267c2c 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
@@ -16,49 +16,23 @@
package com.android.internal.protolog;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.internal.protolog.ProtoLogImpl.PROTOLOG_VERSION;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import android.content.Context;
-import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
-import android.util.proto.ProtoInputStream;
import androidx.test.filters.SmallTest;
+import com.android.internal.protolog.common.IProtoLog;
import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
import org.junit.After;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.util.LinkedList;
/**
* Test class for {@link ProtoLogImpl}.
@@ -68,336 +42,78 @@
@Presubmit
@RunWith(JUnit4.class)
public class ProtoLogImplTest {
-
- private static final byte[] MAGIC_HEADER = new byte[]{
- 0x9, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47
- };
-
- private ProtoLogImpl mProtoLog;
- private File mFile;
-
- @Mock
- private ProtoLogViewerConfigReader mReader;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- final Context testContext = getInstrumentation().getContext();
- mFile = testContext.getFileStreamPath("tracing_test.dat");
- //noinspection ResultOfMethodCallIgnored
- mFile.delete();
- mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader, 1024);
- }
-
@After
public void tearDown() {
- if (mFile != null) {
- //noinspection ResultOfMethodCallIgnored
- mFile.delete();
- }
ProtoLogImpl.setSingleInstance(null);
}
@Test
- public void isEnabled_returnsFalseByDefault() {
- assertFalse(mProtoLog.isProtoEnabled());
- }
-
- @Test
- public void isEnabled_returnsTrueAfterStart() {
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- assertTrue(mProtoLog.isProtoEnabled());
- }
-
- @Test
- public void isEnabled_returnsFalseAfterStop() {
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
- assertFalse(mProtoLog.isProtoEnabled());
- }
-
- @Test
- public void logFile_startsWithMagicHeader() throws Exception {
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-
- assertTrue("Log file should exist", mFile.exists());
-
- byte[] header = new byte[MAGIC_HEADER.length];
- try (InputStream is = new FileInputStream(mFile)) {
- assertEquals(MAGIC_HEADER.length, is.read(header));
- assertArrayEquals(MAGIC_HEADER, header);
- }
- }
-
- @Test
public void getSingleInstance() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
assertSame(mockedProtoLog, ProtoLogImpl.getSingleInstance());
}
@Test
public void d_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.DEBUG), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
@Test
public void v_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.VERBOSE), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
@Test
public void i_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.INFO), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.INFO), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
@Test
public void w_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234,
4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.WARN), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.WARN), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
@Test
public void e_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.ERROR), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
@Test
public void wtf_logCalled() {
- ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+ IProtoLog mockedProtoLog = mock(IProtoLog.class);
ProtoLogImpl.setSingleInstance(mockedProtoLog);
ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP,
1234, 4321, "test %d");
- verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.WTF), eq(
+ verify(mockedProtoLog).log(eq(LogLevel.WTF), eq(
TestProtoLogGroup.TEST_GROUP),
- eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
- }
-
- @Test
- public void log_logcatEnabledExternalMessage() {
- when(mReader.getViewerString(anyInt())).thenReturn("test %b %d %% 0x%x %s %f");
- ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
- implSpy.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
- new Object[]{true, 10000, 30000, "test", 0.000003});
-
- verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
- ProtoLogImpl.LogLevel.INFO),
- eq("test true 10000 % 0x7530 test 3.0E-6"));
- verify(mReader).getViewerString(eq(1234));
- }
-
- @Test
- public void log_logcatEnabledInvalidMessage() {
- when(mReader.getViewerString(anyInt())).thenReturn("test %b %d %% %x %s %f");
- ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
- implSpy.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
- new Object[]{true, 10000, 0.0001, 0.00002, "test"});
-
- verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
- ProtoLogImpl.LogLevel.INFO),
- eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
- verify(mReader).getViewerString(eq(1234));
- }
-
- @Test
- public void log_logcatEnabledInlineMessage() {
- when(mReader.getViewerString(anyInt())).thenReturn("test %d");
- ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
- implSpy.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
- new Object[]{5});
-
- verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
- ProtoLogImpl.LogLevel.INFO), eq("test 5"));
- verify(mReader, never()).getViewerString(anyInt());
- }
-
- @Test
- public void log_logcatEnabledNoMessage() {
- when(mReader.getViewerString(anyInt())).thenReturn(null);
- ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
- implSpy.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
- new Object[]{5});
-
- verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
- ProtoLogImpl.LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
- verify(mReader).getViewerString(eq(1234));
- }
-
- @Test
- public void log_logcatDisabled() {
- when(mReader.getViewerString(anyInt())).thenReturn("test %d");
- ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
- implSpy.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
- new Object[]{5});
-
- verify(implSpy, never()).passToLogcat(any(), any(), any());
- verify(mReader, never()).getViewerString(anyInt());
- }
-
- private static class ProtoLogData {
- Integer mMessageHash = null;
- Long mElapsedTime = null;
- LinkedList<String> mStrParams = new LinkedList<>();
- LinkedList<Long> mSint64Params = new LinkedList<>();
- LinkedList<Double> mDoubleParams = new LinkedList<>();
- LinkedList<Boolean> mBooleanParams = new LinkedList<>();
- }
-
- private ProtoLogData readProtoLogSingle(ProtoInputStream ip) throws IOException {
- while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
- if (ip.getFieldNumber() == (int) ProtoLogFileProto.VERSION) {
- assertEquals(PROTOLOG_VERSION, ip.readString(ProtoLogFileProto.VERSION));
- continue;
- }
- if (ip.getFieldNumber() != (int) ProtoLogFileProto.LOG) {
- continue;
- }
- long token = ip.start(ProtoLogFileProto.LOG);
- ProtoLogData data = new ProtoLogData();
- while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
- switch (ip.getFieldNumber()) {
- case (int) ProtoLogMessage.MESSAGE_HASH: {
- data.mMessageHash = ip.readInt(ProtoLogMessage.MESSAGE_HASH);
- break;
- }
- case (int) ProtoLogMessage.ELAPSED_REALTIME_NANOS: {
- data.mElapsedTime = ip.readLong(ProtoLogMessage.ELAPSED_REALTIME_NANOS);
- break;
- }
- case (int) ProtoLogMessage.STR_PARAMS: {
- data.mStrParams.add(ip.readString(ProtoLogMessage.STR_PARAMS));
- break;
- }
- case (int) ProtoLogMessage.SINT64_PARAMS: {
- data.mSint64Params.add(ip.readLong(ProtoLogMessage.SINT64_PARAMS));
- break;
- }
- case (int) ProtoLogMessage.DOUBLE_PARAMS: {
- data.mDoubleParams.add(ip.readDouble(ProtoLogMessage.DOUBLE_PARAMS));
- break;
- }
- case (int) ProtoLogMessage.BOOLEAN_PARAMS: {
- data.mBooleanParams.add(ip.readBoolean(ProtoLogMessage.BOOLEAN_PARAMS));
- break;
- }
- }
- }
- ip.end(token);
- return data;
- }
- return null;
- }
-
- @Test
- public void log_protoEnabled() throws Exception {
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- long before = SystemClock.elapsedRealtimeNanos();
- mProtoLog.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
- 0b1110101001010100, null,
- new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
- long after = SystemClock.elapsedRealtimeNanos();
- mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
- try (InputStream is = new FileInputStream(mFile)) {
- ProtoInputStream ip = new ProtoInputStream(is);
- ProtoLogData data = readProtoLogSingle(ip);
- assertNotNull(data);
- assertEquals(1234, data.mMessageHash.longValue());
- assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
- assertArrayEquals(new String[]{"test"}, data.mStrParams.toArray());
- assertArrayEquals(new Long[]{1L, 2L, 3L}, data.mSint64Params.toArray());
- assertArrayEquals(new Double[]{0.4, 0.5, 0.6}, data.mDoubleParams.toArray());
- assertArrayEquals(new Boolean[]{true}, data.mBooleanParams.toArray());
- }
- }
-
- @Test
- public void log_invalidParamsMask() throws Exception {
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- long before = SystemClock.elapsedRealtimeNanos();
- mProtoLog.log(
- ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
- 0b01100100, null,
- new Object[]{"test", 1, 0.1, true});
- long after = SystemClock.elapsedRealtimeNanos();
- mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
- try (InputStream is = new FileInputStream(mFile)) {
- ProtoInputStream ip = new ProtoInputStream(is);
- ProtoLogData data = readProtoLogSingle(ip);
- assertNotNull(data);
- assertEquals(1234, data.mMessageHash.longValue());
- assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
- assertArrayEquals(new String[]{"test", "(INVALID PARAMS_MASK) true"},
- data.mStrParams.toArray());
- assertArrayEquals(new Long[]{1L}, data.mSint64Params.toArray());
- assertArrayEquals(new Double[]{0.1}, data.mDoubleParams.toArray());
- assertArrayEquals(new Boolean[]{}, data.mBooleanParams.toArray());
- }
- }
-
- @Test
- public void log_protoDisabled() throws Exception {
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
- TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
- mProtoLog.startProtoLog(mock(PrintWriter.class));
- mProtoLog.log(ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
- 0b11, null, new Object[]{true});
- mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
- try (InputStream is = new FileInputStream(mFile)) {
- ProtoInputStream ip = new ProtoInputStream(is);
- ProtoLogData data = readProtoLogSingle(ip);
- assertNull(data);
- }
+ eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
}
private enum TestProtoLogGroup implements IProtoLogGroup {
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index ae50216..dbd85d3 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -72,8 +72,8 @@
+ "}\n";
- private ProtoLogViewerConfigReader
- mConfig = new ProtoLogViewerConfigReader();
+ private LegacyProtoLogViewerConfigReader
+ mConfig = new LegacyProtoLogViewerConfigReader();
private File mTestViewerConfig;
@Before
@@ -98,7 +98,7 @@
@Test
public void loadViewerConfig() {
- mConfig.loadViewerConfig(null, mTestViewerConfig.getAbsolutePath());
+ mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath());
assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285));
assertEquals("Test 2", mConfig.getViewerString(1352021864));
assertEquals("Window %s is already added", mConfig.getViewerString(409412266));
@@ -107,7 +107,7 @@
@Test
public void loadViewerConfig_invalidFile() {
- mConfig.loadViewerConfig(null, "/tmp/unknown/file/does/not/exist");
+ mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist");
// No exception is thrown.
assertNull(mConfig.getViewerString(1));
}
diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp
index 46745e9..8fbc3e8 100644
--- a/tools/protologtool/Android.bp
+++ b/tools/protologtool/Android.bp
@@ -11,12 +11,13 @@
name: "protologtool-lib",
srcs: [
"src/com/android/protolog/tool/**/*.kt",
- ":protolog-common-no-android-src",
+ ":protolog-common-src",
],
static_libs: [
"javaparser",
"platformprotos",
"jsonlib",
+ "perfetto_trace-full",
],
}
@@ -42,5 +43,6 @@
"junit",
"mockito",
"objenesis",
+ "truth",
],
}
diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md
index ba63957..24a4861 100644
--- a/tools/protologtool/README.md
+++ b/tools/protologtool/README.md
@@ -8,11 +8,13 @@
### Code transformation
-Command: `protologtool transform-protolog-calls
- --protolog-class <protolog class name>
- --protolog-impl-class <protolog implementation class name>
+Command: `protologtool transform-protolog-calls
+ --protolog-class <protolog class name>
--loggroups-class <protolog groups class name>
--loggroups-jar <config jar path>
+ --viewer-config-file-path <protobuf viewer config file path>
+ --legacy-viewer-config-file-path <legacy json.gz viewer config file path>
+ --legacy-output-file-path <.winscope file path to write the legacy trace to>
--output-srcjar <output.srcjar>
[<input.java>]`
@@ -44,10 +46,11 @@
### Viewer config generation
Command: `generate-viewer-config
- --protolog-class <protolog class name>
+ --protolog-class <protolog class name>
--loggroups-class <protolog groups class name>
--loggroups-jar <config jar path>
- --viewer-conf <viewer.json>
+ --viewer-config-type <proto|json>
+ --viewer-config <viewer.json>
[<input.java>]`
This command is similar in it's syntax to the previous one, only instead of creating a processed source jar
@@ -74,7 +77,7 @@
### Binary log viewing
-Command: `read-log --viewer-conf <viewer.json> <wm_log.pb>`
+Command: `read-log --viewer-config <viewer.json> <wm_log.pb>`
Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log.
diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
index 07c6fd3..3d1dec2 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
@@ -22,15 +22,21 @@
import com.github.javaparser.ast.expr.BinaryExpr
import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.StringLiteralExpr
+import java.util.UUID
object CodeUtils {
/**
* Returns a stable hash of a string.
* We reimplement String::hashCode() for readability reasons.
*/
- fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int {
- return (position + messageString + logLevel.name + logGroup.name)
- .map { c -> c.code }.reduce { h, c -> h * 31 + c }
+ fun hash(
+ position: String,
+ messageString: String,
+ logLevel: LogLevel,
+ logGroup: LogGroup
+ ): Long {
+ val fullStringIdentifier = position + messageString + logLevel.name + logGroup.name
+ return UUID.nameUUIDFromBytes(fullStringIdentifier.toByteArray()).mostSignificantBits
}
fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) {
diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
index bfbbf7a..a359155 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
@@ -26,32 +26,35 @@
private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
- private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class"
- private const val PROTOLOGCACHE_CLASS_PARAM = "--protolog-cache-class"
private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
- private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf"
+ private const val VIEWER_CONFIG_PARAM = "--viewer-config"
+ private const val VIEWER_CONFIG_TYPE_PARAM = "--viewer-config-type"
private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar"
- private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM,
- PROTOLOGCACHE_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM,
- VIEWER_CONFIG_JSON_PARAM, OUTPUT_SOURCE_JAR_PARAM)
+ private const val VIEWER_CONFIG_FILE_PATH_PARAM = "--viewer-config-file-path"
+ // TODO(b/324128613): Remove these legacy options once we fully flip the Perfetto protolog flag
+ private const val LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM = "--legacy-viewer-config-file-path"
+ private const val LEGACY_OUTPUT_FILE_PATH = "--legacy-output-file-path"
+ private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM,
+ PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_PARAM, VIEWER_CONFIG_TYPE_PARAM,
+ OUTPUT_SOURCE_JAR_PARAM, VIEWER_CONFIG_FILE_PATH_PARAM,
+ LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, LEGACY_OUTPUT_FILE_PATH)
val USAGE = """
Usage: ${Constants.NAME} <command> [<args>]
Available commands:
- $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM
- <class name> $PROTOLOGCACHE_CLASS_PARAM
- <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM
- <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
+ $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name>
+ $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
+ $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
- processes java files replacing stub calls with logging code.
- $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM
- <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM
- <viewer.json> [<input.java>]
+ $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name>
+ $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
+ $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> [<input.java>]
- creates viewer config file from given java files.
- $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb>
+ $READ_LOG_CMD $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> <wm_log.pb>
- translates a binary log to a readable format.
""".trimIndent()
@@ -69,6 +72,13 @@
return params.getValue(paramName)
}
+ private fun getOptionalParam(paramName: String, params: Map<String, String>): String? {
+ if (!params.containsKey(paramName)) {
+ return null
+ }
+ return params.getValue(paramName)
+ }
+
private fun validateNotSpecified(paramName: String, params: Map<String, String>): String {
if (params.containsKey(paramName)) {
throw InvalidCommandException("Unsupported param $paramName")
@@ -90,9 +100,43 @@
return name
}
- private fun validateJSONName(name: String): String {
- if (!name.endsWith(".json")) {
- throw InvalidCommandException("Json file required, got $name instead")
+ private fun validateViewerConfigFilePath(name: String): String {
+ if (!name.endsWith(".pb")) {
+ throw InvalidCommandException("Proto file (ending with .pb) required, " +
+ "got $name instead")
+ }
+ return name
+ }
+
+ private fun validateLegacyViewerConfigFilePath(name: String): String {
+ if (!name.endsWith(".json.gz")) {
+ throw InvalidCommandException("GZiped Json file (ending with .json.gz) required, " +
+ "got $name instead")
+ }
+ return name
+ }
+
+ private fun validateOutputFilePath(name: String): String {
+ if (!name.endsWith(".winscope")) {
+ throw InvalidCommandException("Winscope file (ending with .winscope) required, " +
+ "got $name instead")
+ }
+ return name
+ }
+
+ private fun validateConfigFileName(name: String): String {
+ if (!name.endsWith(".json") && !name.endsWith(".pb")) {
+ throw InvalidCommandException("Json file (ending with .json) or proto file " +
+ "(ending with .pb) required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateConfigType(name: String): String {
+ val validType = listOf("json", "proto")
+ if (!validType.contains(name)) {
+ throw InvalidCommandException("Unexpected config file type. " +
+ "Expected on of [${validType.joinToString()}], but got $name")
}
return name
}
@@ -102,8 +146,8 @@
throw InvalidCommandException("No java source input files")
}
list.forEach { name ->
- if (!name.endsWith(".java")) {
- throw InvalidCommandException("Not a java source file $name")
+ if (!name.endsWith(".java") && !name.endsWith(".kt")) {
+ throw InvalidCommandException("Not a java or kotlin source file $name")
}
}
return list
@@ -122,12 +166,14 @@
val protoLogClassNameArg: String
val protoLogGroupsClassNameArg: String
- val protoLogImplClassNameArg: String
- val protoLogCacheClassNameArg: String
val protoLogGroupsJarArg: String
- val viewerConfigJsonArg: String
+ val viewerConfigFileNameArg: String
+ val viewerConfigTypeArg: String
val outputSourceJarArg: String
val logProtofileArg: String
+ val viewerConfigFilePathArg: String
+ val legacyViewerConfigFilePathArg: String?
+ val legacyOutputFilePath: String?
val javaSourceArgs: List<String>
val command: String
@@ -169,38 +215,55 @@
when (command) {
TRANSFORM_CALLS_CMD -> {
protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
- protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
- params))
- protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM,
- params))
- protoLogCacheClassNameArg = validateClassName(getParam(PROTOLOGCACHE_CLASS_PARAM,
- params))
+ protoLogGroupsClassNameArg =
+ validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
- viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params)
+ viewerConfigFileNameArg = validateNotSpecified(VIEWER_CONFIG_PARAM, params)
+ viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params))
+ viewerConfigFilePathArg = validateViewerConfigFilePath(
+ getParam(VIEWER_CONFIG_FILE_PATH_PARAM, params))
+ legacyViewerConfigFilePathArg =
+ getOptionalParam(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)?.let {
+ validateLegacyViewerConfigFilePath(it)
+ }
+ legacyOutputFilePath =
+ getOptionalParam(LEGACY_OUTPUT_FILE_PATH, params)?.let {
+ validateOutputFilePath(it)
+ }
javaSourceArgs = validateJavaInputList(inputFiles)
logProtofileArg = ""
}
GENERATE_CONFIG_CMD -> {
protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
- protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
- params))
- protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
- protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params)
+ protoLogGroupsClassNameArg =
+ validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
- viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ viewerConfigFileNameArg =
+ validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
+ viewerConfigTypeArg = validateConfigType(getParam(VIEWER_CONFIG_TYPE_PARAM, params))
outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ viewerConfigFilePathArg =
+ validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
+ legacyViewerConfigFilePathArg =
+ validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
+ legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
javaSourceArgs = validateJavaInputList(inputFiles)
logProtofileArg = ""
}
READ_LOG_CMD -> {
protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params)
protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params)
- protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
- protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params)
protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params)
- viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ viewerConfigFileNameArg =
+ validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
+ viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ viewerConfigFilePathArg =
+ validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
+ legacyViewerConfigFilePathArg =
+ validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
+ legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
javaSourceArgs = listOf()
logProtofileArg = validateLogInputList(inputFiles)
}
diff --git a/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt
new file mode 100644
index 0000000..fda6351
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.protolog.tool
+
+import com.github.javaparser.ast.expr.MethodCallExpr
+
+interface MethodCallVisitor {
+ fun processCall(call: MethodCallExpr)
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
index 9a76a6f..47724b7 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -16,115 +16,13 @@
package com.android.protolog.tool
-import com.android.internal.protolog.common.LogLevel
import com.github.javaparser.ast.CompilationUnit
-import com.github.javaparser.ast.Node
-import com.github.javaparser.ast.expr.Expression
-import com.github.javaparser.ast.expr.FieldAccessExpr
-import com.github.javaparser.ast.expr.MethodCallExpr
-import com.github.javaparser.ast.expr.NameExpr
-/**
- * Helper class for visiting all ProtoLog calls.
- * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback
- * is executed.
- */
-open class ProtoLogCallProcessor(
- private val protoLogClassName: String,
- private val protoLogGroupClassName: String,
- private val groupMap: Map<String, LogGroup>
-) {
- private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
- private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.')
-
- private fun getLogGroupName(
- expr: Expression,
- isClassImported: Boolean,
- staticImports: Set<String>,
+interface ProtoLogCallProcessor {
+ fun process(
+ code: CompilationUnit,
+ logCallVisitor: ProtoLogCallVisitor?,
+ otherCallVisitor: MethodCallVisitor?,
fileName: String
- ): String {
- val context = ParsingContext(fileName, expr)
- return when (expr) {
- is NameExpr -> when {
- expr.nameAsString in staticImports -> expr.nameAsString
- else ->
- throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
- context)
- }
- is FieldAccessExpr -> when {
- expr.scope.toString() == protoLogGroupClassName
- || isClassImported &&
- expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString
- else ->
- throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
- context)
- }
- else -> throw InvalidProtoLogCallException("Invalid group argument " +
- "- must be ProtoLogGroup enum member reference: $expr", context)
- }
- }
-
- private fun isProtoCall(
- call: MethodCallExpr,
- isLogClassImported: Boolean,
- staticLogImports: Collection<String>
- ): Boolean {
- return call.scope.isPresent && call.scope.get().toString() == protoLogClassName ||
- isLogClassImported && call.scope.isPresent &&
- call.scope.get().toString() == protoLogSimpleClassName ||
- !call.scope.isPresent && staticLogImports.contains(call.name.toString())
- }
-
- open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?, fileName: String):
- CompilationUnit {
- CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName)
- CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName)
-
- val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName)
- val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName)
- val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code,
- protoLogGroupClassName)
- val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName)
-
- code.findAll(MethodCallExpr::class.java)
- .filter { call ->
- isProtoCall(call, isLogClassImported, staticLogImports)
- }.forEach { call ->
- val context = ParsingContext(fileName, call)
- if (call.arguments.size < 2) {
- throw InvalidProtoLogCallException("Method signature does not match " +
- "any ProtoLog method: $call", context)
- }
-
- val messageString = CodeUtils.concatMultilineString(call.getArgument(1),
- context)
- val groupNameArg = call.getArgument(0)
- val groupName =
- getLogGroupName(groupNameArg, isGroupClassImported,
- staticGroupImports, fileName)
- if (groupName !in groupMap) {
- throw InvalidProtoLogCallException("Unknown group argument " +
- "- not a ProtoLogGroup enum member: $call", context)
- }
-
- callVisitor?.processCall(call, messageString, getLevelForMethodName(
- call.name.toString(), call, context), groupMap.getValue(groupName))
- }
- return code
- }
-
- companion object {
- fun getLevelForMethodName(name: String, node: Node, context: ParsingContext): LogLevel {
- return when (name) {
- "d" -> LogLevel.DEBUG
- "v" -> LogLevel.VERBOSE
- "i" -> LogLevel.INFO
- "w" -> LogLevel.WARN
- "e" -> LogLevel.ERROR
- "wtf" -> LogLevel.WTF
- else ->
- throw InvalidProtoLogCallException("Unknown log level $name in $node", context)
- }
- }
- }
+ ): CompilationUnit
}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
new file mode 100644
index 0000000..1087ae6
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 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.protolog.tool
+
+import com.android.internal.protolog.common.LogLevel
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+
+/**
+ * Helper class for visiting all ProtoLog calls.
+ * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback
+ * is executed.
+ */
+class ProtoLogCallProcessorImpl(
+ private val protoLogClassName: String,
+ private val protoLogGroupClassName: String,
+ private val groupMap: Map<String, LogGroup>
+) : ProtoLogCallProcessor {
+ private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
+ private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.')
+
+ private fun getLogGroupName(
+ expr: Expression,
+ isClassImported: Boolean,
+ staticImports: Set<String>,
+ fileName: String
+ ): String {
+ val context = ParsingContext(fileName, expr)
+ return when (expr) {
+ is NameExpr -> when {
+ expr.nameAsString in staticImports -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
+ context)
+ }
+ is FieldAccessExpr -> when {
+ expr.scope.toString() == protoLogGroupClassName || isClassImported &&
+ expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
+ context)
+ }
+ else -> throw InvalidProtoLogCallException("Invalid group argument " +
+ "- must be ProtoLogGroup enum member reference: $expr", context)
+ }
+ }
+
+ private fun isProtoCall(
+ call: MethodCallExpr,
+ isLogClassImported: Boolean,
+ staticLogImports: Collection<String>
+ ): Boolean {
+ return call.scope.isPresent && call.scope.get().toString() == protoLogClassName ||
+ isLogClassImported && call.scope.isPresent &&
+ call.scope.get().toString() == protoLogSimpleClassName ||
+ !call.scope.isPresent && staticLogImports.contains(call.name.toString())
+ }
+
+ fun process(code: CompilationUnit, logCallVisitor: ProtoLogCallVisitor?, fileName: String):
+ CompilationUnit {
+ return process(code, logCallVisitor, null, fileName)
+ }
+
+ override fun process(
+ code: CompilationUnit,
+ logCallVisitor: ProtoLogCallVisitor?,
+ otherCallVisitor: MethodCallVisitor?,
+ fileName: String
+ ): CompilationUnit {
+ CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName)
+ CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName)
+
+ val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName)
+ val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName)
+ val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code,
+ protoLogGroupClassName)
+ val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName)
+
+ code.findAll(MethodCallExpr::class.java)
+ .filter { call ->
+ isProtoCall(call, isLogClassImported, staticLogImports)
+ }.forEach { call ->
+ val context = ParsingContext(fileName, call)
+
+ val logMethods = LogLevel.entries.map { it.shortCode }
+ if (logMethods.contains(call.name.id)) {
+ // Process a log call
+ if (call.arguments.size < 2) {
+ throw InvalidProtoLogCallException("Method signature does not match " +
+ "any ProtoLog method: $call", context)
+ }
+
+ val messageString = CodeUtils.concatMultilineString(call.getArgument(1),
+ context)
+ val groupNameArg = call.getArgument(0)
+ val groupName =
+ getLogGroupName(groupNameArg, isGroupClassImported,
+ staticGroupImports, fileName)
+ if (groupName !in groupMap) {
+ throw InvalidProtoLogCallException("Unknown group argument " +
+ "- not a ProtoLogGroup enum member: $call", context)
+ }
+
+ logCallVisitor?.processCall(call, messageString, getLevelForMethodName(
+ call.name.toString(), call, context), groupMap.getValue(groupName))
+ } else {
+ // Process non-log message calls
+ otherCallVisitor?.processCall(call)
+ }
+ }
+ return code
+ }
+
+ private fun getLevelForMethodName(
+ name: String,
+ node: MethodCallExpr,
+ context: ParsingContext
+ ): LogLevel = when (name) {
+ "d" -> LogLevel.DEBUG
+ "v" -> LogLevel.VERBOSE
+ "i" -> LogLevel.INFO
+ "w" -> LogLevel.WARN
+ "e" -> LogLevel.ERROR
+ "wtf" -> LogLevel.WTF
+ else ->
+ throw InvalidProtoLogCallException("Unknown log level $name in $node", context)
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
index ce856cd..1381847 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -16,13 +16,22 @@
package com.android.protolog.tool
+import com.android.internal.protolog.common.LogLevel
+import com.android.internal.protolog.common.ProtoLog
+import com.android.internal.protolog.common.ProtoLogToolInjected
import com.android.protolog.tool.CommandOptions.Companion.USAGE
import com.github.javaparser.ParseProblemException
import com.github.javaparser.ParserConfiguration
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.StringLiteralExpr
import java.io.File
import java.io.FileInputStream
+import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.OutputStream
import java.time.LocalDateTime
@@ -30,9 +39,21 @@
import java.util.concurrent.Executors
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
+import kotlin.math.abs
+import kotlin.random.Random
import kotlin.system.exitProcess
object ProtoLogTool {
+ const val PROTOLOG_IMPL_SRC_PATH =
+ "frameworks/base/core/java/com/android/internal/protolog/ProtoLogImpl.java"
+
+ data class LogCall(
+ val messageString: String,
+ val logLevel: LogLevel,
+ val logGroup: LogGroup,
+ val position: String
+ )
+
private fun showHelpAndExit() {
println(USAGE)
exitProcess(-1)
@@ -51,26 +72,40 @@
}
private fun processClasses(command: CommandOptions) {
+ val generationHash = abs(Random.nextInt())
+ // Need to generate a new impl class to inject static constants into the class.
+ val generatedProtoLogImplClass =
+ "com.android.internal.protolog.ProtoLogImpl_$generationHash"
+
val groups = injector.readLogGroups(
command.protoLogGroupsJarArg,
command.protoLogGroupsClassNameArg)
val out = injector.fileOutputStream(command.outputSourceJarArg)
val outJar = JarOutputStream(out)
- val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
- command.protoLogGroupsClassNameArg, groups)
+ val processor = ProtoLogCallProcessorImpl(
+ command.protoLogClassNameArg,
+ command.protoLogGroupsClassNameArg,
+ groups)
+
+ val protologImplName = generatedProtoLogImplClass.split(".").last()
+ val protologImplPath = "gen/${generatedProtoLogImplClass.split(".")
+ .joinToString("/")}.java"
+ outJar.putNextEntry(zipEntry(protologImplPath))
+
+ outJar.write(generateProtoLogImpl(protologImplName, command.viewerConfigFilePathArg,
+ command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath).toByteArray())
val executor = newThreadPool()
try {
command.javaSourceArgs.map { path ->
executor.submitCallable {
- val transformer = SourceTransformer(command.protoLogImplClassNameArg,
- command.protoLogCacheClassNameArg, processor)
+ val transformer = SourceTransformer(generatedProtoLogImplClass, processor)
val file = File(path)
val text = injector.readText(file)
val outSrc = try {
val code = tryParse(text, path)
- if (containsProtoLogText(text, command.protoLogClassNameArg)) {
+ if (containsProtoLogText(text, ProtoLog::class.java.simpleName)) {
transformer.processClass(text, path, packagePath(file, code), code)
} else {
text
@@ -93,51 +128,77 @@
executor.shutdown()
}
- val cacheSplit = command.protoLogCacheClassNameArg.split(".")
- val cacheName = cacheSplit.last()
- val cachePackage = cacheSplit.dropLast(1).joinToString(".")
- val cachePath = "gen/${cacheSplit.joinToString("/")}.java"
-
- outJar.putNextEntry(zipEntry(cachePath))
- outJar.write(generateLogGroupCache(cachePackage, cacheName, groups,
- command.protoLogImplClassNameArg, command.protoLogGroupsClassNameArg).toByteArray())
-
outJar.close()
out.close()
}
- fun generateLogGroupCache(
- cachePackage: String,
- cacheName: String,
- groups: Map<String, LogGroup>,
- protoLogImplClassName: String,
- protoLogGroupsClassName: String
+ private fun generateProtoLogImpl(
+ protoLogImplGenName: String,
+ viewerConfigFilePath: String,
+ legacyViewerConfigFilePath: String?,
+ legacyOutputFilePath: String?,
): String {
- val fields = groups.values.map {
- "public static boolean ${it.name}_enabled = false;"
- }.joinToString("\n")
+ val file = File(PROTOLOG_IMPL_SRC_PATH)
- val updates = groups.values.map {
- "${it.name}_enabled = " +
- "$protoLogImplClassName.isEnabled($protoLogGroupsClassName.${it.name});"
- }.joinToString("\n")
+ val text = try {
+ injector.readText(file)
+ } catch (e: FileNotFoundException) {
+ throw RuntimeException("Expected to find '$PROTOLOG_IMPL_SRC_PATH' but file was not " +
+ "included in source for the ProtoLog Tool to process.")
+ }
- return """
- package $cachePackage;
+ val code = tryParse(text, PROTOLOG_IMPL_SRC_PATH)
- public class $cacheName {
-${fields.replaceIndent(" ")}
+ val classDeclarations = code.findAll(ClassOrInterfaceDeclaration::class.java)
+ require(classDeclarations.size == 1) { "Expected exactly one class declaration" }
+ val classDeclaration = classDeclarations[0]
- static {
- $protoLogImplClassName.sCacheUpdater = $cacheName::update;
- update();
- }
+ val classNameNode = classDeclaration.findFirst(SimpleName::class.java).get()
+ classNameNode.setId(protoLogImplGenName)
- static void update() {
-${updates.replaceIndent(" ")}
- }
- }
- """.trimIndent()
+ injectConstants(classDeclaration,
+ viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath)
+
+ return code.toString()
+ }
+
+ private fun injectConstants(
+ classDeclaration: ClassOrInterfaceDeclaration,
+ viewerConfigFilePath: String,
+ legacyViewerConfigFilePath: String?,
+ legacyOutputFilePath: String?
+ ) {
+ classDeclaration.fields.forEach { field ->
+ field.getAnnotationByClass(ProtoLogToolInjected::class.java)
+ .ifPresent { annotationExpr ->
+ if (annotationExpr.isSingleMemberAnnotationExpr) {
+ val valueName = annotationExpr.asSingleMemberAnnotationExpr()
+ .memberValue.asNameExpr().name.asString()
+ when (valueName) {
+ ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH.name -> {
+ field.setFinal(true)
+ field.variables.first()
+ .setInitializer(StringLiteralExpr(viewerConfigFilePath))
+ }
+ ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH.name -> {
+ field.setFinal(true)
+ field.variables.first()
+ .setInitializer(legacyOutputFilePath?.let {
+ StringLiteralExpr(it)
+ } ?: NullLiteralExpr())
+ }
+ ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH.name -> {
+ field.setFinal(true)
+ field.variables.first()
+ .setInitializer(legacyViewerConfigFilePath?.let {
+ StringLiteralExpr(it)
+ } ?: NullLiteralExpr())
+ }
+ else -> error("Unhandled ProtoLogToolInjected value: $valueName.")
+ }
+ }
+ }
+ }
}
private fun tryParse(code: String, fileName: String): CompilationUnit {
@@ -145,24 +206,53 @@
return StaticJavaParser.parse(code)
} catch (ex: ParseProblemException) {
val problem = ex.problems.first()
- throw ParsingException("Java parsing erro" +
- "r: ${problem.verboseMessage}",
+ throw ParsingException("Java parsing error: ${problem.verboseMessage}",
ParsingContext(fileName, problem.location.orElse(null)
?.begin?.range?.orElse(null)?.begin?.line
?: 0))
}
}
+ class LogCallRegistry {
+ private val statements = mutableMapOf<LogCall, Long>()
+
+ fun addLogCalls(calls: List<LogCall>) {
+ calls.forEach { logCall ->
+ if (logCall.logGroup.enabled) {
+ statements.putIfAbsent(logCall,
+ CodeUtils.hash(logCall.position, logCall.messageString,
+ logCall.logLevel, logCall.logGroup))
+ }
+ }
+ }
+
+ fun getStatements(): Map<LogCall, Long> {
+ return statements
+ }
+ }
+
+ interface ProtologViewerConfigBuilder {
+ fun build(statements: Map<LogCall, Long>): ByteArray
+ }
+
private fun viewerConf(command: CommandOptions) {
val groups = injector.readLogGroups(
command.protoLogGroupsJarArg,
command.protoLogGroupsClassNameArg)
- val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+ val processor = ProtoLogCallProcessorImpl(command.protoLogClassNameArg,
command.protoLogGroupsClassNameArg, groups)
- val builder = ViewerConfigBuilder(processor)
+ val outputType = command.viewerConfigTypeArg
+
+ val configBuilder: ProtologViewerConfigBuilder = when (outputType.lowercase()) {
+ "json" -> ViewerConfigJsonBuilder()
+ "proto" -> ViewerConfigProtoBuilder()
+ else -> error("Invalid output type provide. Provided '$outputType'.")
+ }
val executor = newThreadPool()
+ val logCallRegistry = LogCallRegistry()
+
try {
command.javaSourceArgs.map { path ->
executor.submitCallable {
@@ -171,7 +261,7 @@
if (containsProtoLogText(text, command.protoLogClassNameArg)) {
try {
val code = tryParse(text, path)
- builder.findLogCalls(code, path, packagePath(file, code))
+ findLogCalls(code, path, packagePath(file, code), processor)
} catch (ex: ParsingException) {
// If we cannot parse this file, skip it (and log why). Compilation will
// fail in a subsequent build step.
@@ -183,15 +273,38 @@
}
}
}.forEach { future ->
- builder.addLogCalls(future.get() ?: return@forEach)
+ logCallRegistry.addLogCalls(future.get() ?: return@forEach)
}
} finally {
executor.shutdown()
}
- val out = injector.fileOutputStream(command.viewerConfigJsonArg)
- out.write(builder.build().toByteArray())
- out.close()
+ val outFile = injector.fileOutputStream(command.viewerConfigFileNameArg)
+ outFile.write(configBuilder.build(logCallRegistry.getStatements()))
+ outFile.close()
+ }
+
+ private fun findLogCalls(
+ unit: CompilationUnit,
+ path: String,
+ packagePath: String,
+ processor: ProtoLogCallProcessorImpl
+ ): List<LogCall> {
+ val calls = mutableListOf<LogCall>()
+ val logCallVisitor = object : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ val logCall = LogCall(messageString, level, group, packagePath)
+ calls.add(logCall)
+ }
+ }
+ processor.process(unit, logCallVisitor, path)
+
+ return calls
}
private fun packagePath(file: File, code: CompilationUnit): String {
@@ -204,7 +317,7 @@
private fun read(command: CommandOptions) {
LogParser(ViewerConfigParser())
.parse(FileInputStream(command.logProtofileArg),
- FileInputStream(command.viewerConfigJsonArg), System.out)
+ FileInputStream(command.viewerConfigFileNameArg), System.out)
}
@JvmStatic
diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
index b50f357..2b71641 100644
--- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
@@ -22,11 +22,11 @@
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.NodeList
import com.github.javaparser.ast.body.VariableDeclarator
-import com.github.javaparser.ast.expr.BooleanLiteralExpr
import com.github.javaparser.ast.expr.CastExpr
import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.FieldAccessExpr
import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.LongLiteralExpr
import com.github.javaparser.ast.expr.MethodCallExpr
import com.github.javaparser.ast.expr.NameExpr
import com.github.javaparser.ast.expr.NullLiteralExpr
@@ -35,7 +35,6 @@
import com.github.javaparser.ast.expr.VariableDeclarationExpr
import com.github.javaparser.ast.stmt.BlockStmt
import com.github.javaparser.ast.stmt.ExpressionStmt
-import com.github.javaparser.ast.stmt.IfStmt
import com.github.javaparser.ast.type.ArrayType
import com.github.javaparser.ast.type.ClassOrInterfaceType
import com.github.javaparser.ast.type.PrimitiveType
@@ -45,15 +44,59 @@
class SourceTransformer(
protoLogImplClassName: String,
- protoLogCacheClassName: String,
private val protoLogCallProcessor: ProtoLogCallProcessor
-) : ProtoLogCallVisitor {
- override fun processCall(
- call: MethodCallExpr,
- messageString: String,
- level: LogLevel,
- group: LogGroup
- ) {
+) {
+ private val inlinePrinter: PrettyPrinter
+ private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object")
+
+ init {
+ val config = PrettyPrinterConfiguration()
+ config.endOfLineCharacter = " "
+ config.indentSize = 0
+ config.tabWidth = 1
+ inlinePrinter = PrettyPrinter(config)
+ }
+
+ fun processClass(
+ code: String,
+ path: String,
+ packagePath: String,
+ compilationUnit: CompilationUnit =
+ StaticJavaParser.parse(code)
+ ): String {
+ this.path = path
+ this.packagePath = packagePath
+ processedCode = code.split('\n').toMutableList()
+ offsets = IntArray(processedCode.size)
+ protoLogCallProcessor.process(compilationUnit, protoLogCallVisitor, otherCallVisitor, path)
+ return processedCode.joinToString("\n")
+ }
+
+ private val protoLogImplClassNode =
+ StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
+ private var processedCode: MutableList<String> = mutableListOf()
+ private var offsets: IntArray = IntArray(0)
+ /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */
+ private var path: String = ""
+ /** The path of the file being processed, relative to the root package */
+ private var packagePath: String = ""
+
+ private val protoLogCallVisitor = object : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ validateCall(call)
+ val processedCallStatement =
+ createProcessedCallStatement(call, group, level, messageString)
+ val parentStmt = call.parentNode.get() as ExpressionStmt
+ injectProcessedCallStatementInCode(processedCallStatement, parentStmt)
+ }
+ }
+
+ private fun validateCall(call: MethodCallExpr) {
// Input format: ProtoLog.e(GROUP, "msg %d", arg)
if (!call.parentNode.isPresent) {
// Should never happen
@@ -71,89 +114,79 @@
throw RuntimeException("Unable to process log call $call " +
"- no grandparent node in AST")
}
- val ifStmt: IfStmt
- if (group.enabled) {
- val hash = CodeUtils.hash(packagePath, messageString, level, group)
- val newCall = call.clone()
- if (!group.textEnabled) {
- // Remove message string if text logging is not enabled by default.
- // Out: ProtoLog.e(GROUP, null, arg)
- newCall.arguments[1].replace(NameExpr("null"))
- }
- // Insert message string hash as a second argument.
- // Out: ProtoLog.e(GROUP, 1234, null, arg)
- newCall.arguments.add(1, IntegerLiteralExpr(hash))
- val argTypes = LogDataType.parseFormatString(messageString)
- val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
- // Insert bitmap representing which Number parameters are to be considered as
- // floating point numbers.
- // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
- newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
- // Replace call to a stub method with an actual implementation.
- // Out: ProtoLogImpl.e(GROUP, 1234, null, arg)
- newCall.setScope(protoLogImplClassNode)
- // Create a call to ProtoLog$Cache.GROUP_enabled
- // Out: com.android.server.protolog.ProtoLog$Cache.GROUP_enabled
- val isLogEnabled = FieldAccessExpr(protoLogCacheClassNode, "${group.name}_enabled")
- if (argTypes.size != call.arguments.size - 2) {
- throw InvalidProtoLogCallException(
- "Number of arguments (${argTypes.size} does not mach format" +
- " string in: $call", ParsingContext(path, call))
- }
- val blockStmt = BlockStmt()
- if (argTypes.isNotEmpty()) {
- // Assign every argument to a variable to check its type in compile time
- // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
- // Out: long protoLogParam0 = arg
- argTypes.forEachIndexed { idx, type ->
- val varName = "protoLogParam$idx"
- val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
- getConversionForType(type)(newCall.arguments[idx + 4].clone()))
- blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
- newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
- }
- } else {
- // Assign (Object[])null as the vararg parameter to prevent allocating an empty
- // object array.
- val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
- newCall.addArgument(nullArray)
- }
- blockStmt.addStatement(ExpressionStmt(newCall))
- // Create an IF-statement with the previously created condition.
- // Out: if (ProtoLogImpl.isEnabled(GROUP)) {
- // long protoLogParam0 = arg;
- // ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0);
- // }
- ifStmt = IfStmt(isLogEnabled, blockStmt, null)
- } else {
- // Surround with if (false).
- val newCall = parentStmt.clone()
- ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null)
- newCall.setBlockComment(" ${group.name} is disabled ")
+ }
+
+ private fun createProcessedCallStatement(
+ call: MethodCallExpr,
+ group: LogGroup,
+ level: LogLevel,
+ messageString: String
+ ): BlockStmt {
+ val hash = CodeUtils.hash(packagePath, messageString, level, group)
+ val newCall = call.clone()
+ if (!group.textEnabled) {
+ // Remove message string if text logging is not enabled by default.
+ // Out: ProtoLog.e(GROUP, null, arg)
+ newCall.arguments[1].replace(NameExpr("null"))
}
+ // Insert message string hash as a second argument.
+ // Out: ProtoLog.e(GROUP, 1234, null, arg)
+ newCall.arguments.add(1, LongLiteralExpr("" + hash + "L"))
+ val argTypes = LogDataType.parseFormatString(messageString)
+ val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
+ // Insert bitmap representing which Number parameters are to be considered as
+ // floating point numbers.
+ // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
+ newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
+ // Replace call to a stub method with an actual implementation.
+ // Out: ProtoLogImpl.e(GROUP, 1234, null, arg)
+ newCall.setScope(protoLogImplClassNode)
+ if (argTypes.size != call.arguments.size - 2) {
+ throw InvalidProtoLogCallException(
+ "Number of arguments (${argTypes.size} does not match format" +
+ " string in: $call", ParsingContext(path, call))
+ }
+ val blockStmt = BlockStmt()
+ if (argTypes.isNotEmpty()) {
+ // Assign every argument to a variable to check its type in compile time
+ // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
+ // Out: long protoLogParam0 = arg
+ argTypes.forEachIndexed { idx, type ->
+ val varName = "protoLogParam$idx"
+ val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
+ getConversionForType(type)(newCall.arguments[idx + 4].clone()))
+ blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
+ newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
+ }
+ } else {
+ // Assign (Object[])null as the vararg parameter to prevent allocating an empty
+ // object array.
+ val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
+ newCall.addArgument(nullArray)
+ }
+ blockStmt.addStatement(ExpressionStmt(newCall))
+
+ return blockStmt
+ }
+
+ private fun injectProcessedCallStatementInCode(
+ processedCallStatement: BlockStmt,
+ parentStmt: ExpressionStmt
+ ) {
// Inline the new statement.
- val printedIfStmt = inlinePrinter.print(ifStmt)
+ val printedBlockStmt = inlinePrinter.print(processedCallStatement)
// Append blank lines to preserve line numbering in file (to allow debugging)
val parentRange = parentStmt.range.get()
val newLines = parentRange.end.line - parentRange.begin.line
- val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
+ val newStmt = printedBlockStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
// pre-workaround code, see explanation below
- /*
- val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt)
- LexicalPreservingPrinter.setup(inlinedIfStmt)
- // Replace the original call.
- if (!parentStmt.replace(inlinedIfStmt)) {
- // Should never happen
- throw RuntimeException("Unable to process log call $call " +
- "- unable to replace the call.")
- }
- */
+
/** Workaround for a bug in JavaParser (AST tree invalid after replacing a node when using
* LexicalPreservingPrinter (https://github.com/javaparser/javaparser/issues/2290).
* Replace the code below with the one commended-out above one the issue is resolved. */
if (!parentStmt.range.isPresent) {
// Should never happen
- throw RuntimeException("Unable to process log call $call " +
+ throw RuntimeException("Unable to process log call in $parentStmt " +
"- unable to replace the call.")
}
val range = parentStmt.range.get()
@@ -161,29 +194,38 @@
val oldLines = processedCode.subList(begin, range.end.line)
val oldCode = oldLines.joinToString("\n")
val newCode = oldCode.replaceRange(
- offsets[begin] + range.begin.column - 1,
- oldCode.length - oldLines.lastOrNull()!!.length +
- range.end.column + offsets[range.end.line - 1], newStmt)
+ offsets[begin] + range.begin.column - 1,
+ oldCode.length - oldLines.lastOrNull()!!.length +
+ range.end.column + offsets[range.end.line - 1], newStmt)
newCode.split("\n").forEachIndexed { idx, line ->
offsets[begin + idx] += line.length - processedCode[begin + idx].length
processedCode[begin + idx] = line
}
}
- private val inlinePrinter: PrettyPrinter
- private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object")
+ private val otherCallVisitor = object : MethodCallVisitor {
+ override fun processCall(call: MethodCallExpr) {
+ val newCall = call.clone()
+ newCall.setScope(protoLogImplClassNode)
- init {
- val config = PrettyPrinterConfiguration()
- config.endOfLineCharacter = " "
- config.indentSize = 0
- config.tabWidth = 1
- inlinePrinter = PrettyPrinter(config)
+ val range = call.range.get()
+ val begin = range.begin.line - 1
+ val oldLines = processedCode.subList(begin, range.end.line)
+ val oldCode = oldLines.joinToString("\n")
+ val newCode = oldCode.replaceRange(
+ offsets[begin] + range.begin.column - 1,
+ oldCode.length - oldLines.lastOrNull()!!.length +
+ range.end.column + offsets[range.end.line - 1], newCall.toString())
+ newCode.split("\n").forEachIndexed { idx, line ->
+ offsets[begin + idx] += line.length - processedCode[begin + idx].length
+ processedCode[begin + idx] = line
+ }
+ }
}
companion object {
private val stringType: ClassOrInterfaceType =
- StaticJavaParser.parseClassOrInterfaceType("String")
+ StaticJavaParser.parseClassOrInterfaceType("String")
fun getASTTypeForDataType(type: Int): Type {
return when (type) {
@@ -202,36 +244,10 @@
return when (type) {
LogDataType.STRING -> { expr ->
MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")),
- SimpleName("valueOf"), NodeList(expr))
+ SimpleName("valueOf"), NodeList(expr))
}
else -> { expr -> expr }
}
}
}
-
- private val protoLogImplClassNode =
- StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
- private val protoLogCacheClassNode =
- StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName)
- private var processedCode: MutableList<String> = mutableListOf()
- private var offsets: IntArray = IntArray(0)
- /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */
- private var path: String = ""
- /** The path of the file being processed, relative to the root package */
- private var packagePath: String = ""
-
- fun processClass(
- code: String,
- path: String,
- packagePath: String,
- compilationUnit: CompilationUnit =
- StaticJavaParser.parse(code)
- ): String {
- this.path = path
- this.packagePath = packagePath
- processedCode = code.split('\n').toMutableList()
- offsets = IntArray(processedCode.size)
- protoLogCallProcessor.process(compilationUnit, this, path)
- return processedCode.joinToString("\n")
- }
}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
deleted file mode 100644
index 0d5d022..0000000
--- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2019 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.protolog.tool
-
-import com.android.internal.protolog.common.LogLevel
-import com.android.json.stream.JsonWriter
-import com.github.javaparser.ast.CompilationUnit
-import com.android.protolog.tool.Constants.VERSION
-import com.github.javaparser.ast.expr.MethodCallExpr
-import java.io.StringWriter
-
-class ViewerConfigBuilder(
- private val processor: ProtoLogCallProcessor
-) {
- private fun addLogCall(logCall: LogCall, context: ParsingContext) {
- val group = logCall.logGroup
- val messageString = logCall.messageString
- if (group.enabled) {
- val key = logCall.key()
- if (statements.containsKey(key)) {
- if (statements[key] != logCall) {
- throw HashCollisionException(
- "Please modify the log message \"$messageString\" " +
- "or \"${statements[key]}\" - their hashes are equal.", context)
- }
- } else {
- groups.add(group)
- statements[key] = logCall
- }
- }
- }
-
- private val statements: MutableMap<Int, LogCall> = mutableMapOf()
- private val groups: MutableSet<LogGroup> = mutableSetOf()
-
- fun findLogCalls(
- unit: CompilationUnit,
- path: String,
- packagePath: String
- ): List<Pair<LogCall, ParsingContext>> {
- val calls = mutableListOf<Pair<LogCall, ParsingContext>>()
- val visitor = object : ProtoLogCallVisitor {
- override fun processCall(
- call: MethodCallExpr,
- messageString: String,
- level: LogLevel,
- group: LogGroup
- ) {
- val logCall = LogCall(messageString, level, group, packagePath)
- val context = ParsingContext(path, call)
- calls.add(logCall to context)
- }
- }
- processor.process(unit, visitor, path)
-
- return calls
- }
-
- fun addLogCalls(calls: List<Pair<LogCall, ParsingContext>>) {
- calls.forEach { (logCall, context) ->
- addLogCall(logCall, context)
- }
- }
-
- fun build(): String {
- val stringWriter = StringWriter()
- val writer = JsonWriter(stringWriter)
- writer.setIndent(" ")
- writer.beginObject()
- writer.name("version")
- writer.value(VERSION)
- writer.name("messages")
- writer.beginObject()
- statements.toSortedMap().forEach { (key, value) ->
- writer.name(key.toString())
- writer.beginObject()
- writer.name("message")
- writer.value(value.messageString)
- writer.name("level")
- writer.value(value.logLevel.name)
- writer.name("group")
- writer.value(value.logGroup.name)
- writer.name("at")
- writer.value(value.position)
- writer.endObject()
- }
- writer.endObject()
- writer.name("groups")
- writer.beginObject()
- groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group ->
- writer.name(group.name)
- writer.beginObject()
- writer.name("tag")
- writer.value(group.tag)
- writer.endObject()
- }
- writer.endObject()
- writer.endObject()
- stringWriter.buffer.append('\n')
- return stringWriter.toString()
- }
-
- data class LogCall(
- val messageString: String,
- val logLevel: LogLevel,
- val logGroup: LogGroup,
- val position: String
- ) {
- fun key() = CodeUtils.hash(position, messageString, logLevel, logGroup)
- }
-}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt
new file mode 100644
index 0000000..7714db2
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.protolog.tool
+
+import com.android.json.stream.JsonWriter
+import com.android.protolog.tool.Constants.VERSION
+import java.io.StringWriter
+
+class ViewerConfigJsonBuilder : ProtoLogTool.ProtologViewerConfigBuilder {
+ override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray {
+ val groups = statements.map { it.key.logGroup }.toSet()
+ val stringWriter = StringWriter()
+ val writer = JsonWriter(stringWriter)
+ writer.setIndent(" ")
+ writer.beginObject()
+ writer.name("version")
+ writer.value(VERSION)
+ writer.name("messages")
+ writer.beginObject()
+ statements.forEach { (log, key) ->
+ writer.name(key.toString())
+ writer.beginObject()
+ writer.name("message")
+ writer.value(log.messageString)
+ writer.name("level")
+ writer.value(log.logLevel.name)
+ writer.name("group")
+ writer.value(log.logGroup.name)
+ writer.name("at")
+ writer.value(log.position)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.name("groups")
+ writer.beginObject()
+ groups.toSortedSet { o1, o2 -> o1.name.compareTo(o2.name) }.forEach { group ->
+ writer.name(group.name)
+ writer.beginObject()
+ writer.name("tag")
+ writer.value(group.tag)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.endObject()
+ stringWriter.buffer.append('\n')
+ return stringWriter.toString().toByteArray()
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
index 7278db0..58be3a3 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
@@ -63,12 +63,12 @@
return GroupEntry(tag)
}
- fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> {
- val config: MutableMap<Int, MessageEntry> = mutableMapOf()
+ fun parseMessages(jsonReader: JsonReader): Map<Long, MessageEntry> {
+ val config: MutableMap<Long, MessageEntry> = mutableMapOf()
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val key = jsonReader.nextName()
- val hash = key.toIntOrNull()
+ val hash = key.toLongOrNull()
?: throw InvalidViewerConfigException("Invalid key in messages viewer config")
config[hash] = parseMessage(jsonReader)
}
@@ -89,8 +89,8 @@
data class ConfigEntry(val messageString: String, val level: String, val tag: String)
- open fun parseConfig(jsonReader: JsonReader): Map<Int, ConfigEntry> {
- var messages: Map<Int, MessageEntry>? = null
+ open fun parseConfig(jsonReader: JsonReader): Map<Long, ConfigEntry> {
+ var messages: Map<Long, MessageEntry>? = null
var groups: Map<String, GroupEntry>? = null
var version: String? = null
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt
new file mode 100644
index 0000000..cf0876a
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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.protolog.tool
+
+import perfetto.protos.PerfettoTrace.ProtoLogLevel
+import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig
+
+/**
+ * A builder class to construct the viewer configuration (i.e. mappings of protolog hashes to log
+ * message information used to decode the protolog messages) encoded as a proto message.
+ */
+class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder {
+ /**
+ * @return a byte array of a ProtoLogViewerConfig proto message encoding all the viewer
+ * configurations mapping protolog hashes to message information and log group information.
+ */
+ override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray {
+ val configBuilder = ProtoLogViewerConfig.newBuilder()
+
+ val groups = statements.map { it.key.logGroup }.toSet()
+ val groupIds = mutableMapOf<LogGroup, Int>()
+ groups.forEach {
+ groupIds.putIfAbsent(it, groupIds.size + 1)
+ }
+
+ groupIds.forEach { (group, id) ->
+ configBuilder.addGroups(ProtoLogViewerConfig.Group.newBuilder()
+ .setId(id)
+ .setName(group.name)
+ .setTag(group.tag)
+ .build())
+ }
+
+ statements.forEach { (log, key) ->
+ val groupId = groupIds[log.logGroup] ?: error("missing group id")
+
+ configBuilder.addMessages(
+ ProtoLogViewerConfig.MessageData.newBuilder()
+ .setMessageId(key)
+ .setMessage(log.messageString)
+ .setLevel(
+ ProtoLogLevel.forNumber(log.logLevel.ordinal + 1))
+ .setGroupId(groupId)
+ )
+ }
+
+ return configBuilder.build().toByteArray()
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
index b08d859..0cd02a5c 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
@@ -28,31 +28,31 @@
class CodeUtilsTest {
@Test
fun hash() {
- assertEquals(-1259556708, CodeUtils.hash("Test.java:50", "test",
+ assertEquals(3883826472308915399, CodeUtils.hash("Test.java:50", "test",
LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
}
@Test
fun hash_changeLocation() {
- assertEquals(15793504, CodeUtils.hash("Test.java:10", "test2",
+ assertEquals(4125273133972468649, CodeUtils.hash("Test.java:10", "test2",
LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
}
@Test
fun hash_changeLevel() {
- assertEquals(-731772463, CodeUtils.hash("Test.java:50", "test",
+ assertEquals(2618535069521361990, CodeUtils.hash("Test.java:50", "test",
LogLevel.ERROR, LogGroup("test", true, true, "TAG")))
}
@Test
fun hash_changeMessage() {
- assertEquals(-2026343204, CodeUtils.hash("Test.java:50", "test2",
+ assertEquals(8907822592109789043, CodeUtils.hash("Test.java:50", "test2",
LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
}
@Test
fun hash_changeGroup() {
- assertEquals(1607870166, CodeUtils.hash("Test.java:50", "test2",
+ assertEquals(-1299517016176640015, CodeUtils.hash("Test.java:50", "test2",
LogLevel.DEBUG, LogGroup("test2", true, true, "TAG")))
}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
index 3cfbb43..5ef2833 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
@@ -16,7 +16,9 @@
package com.android.protolog.tool
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
import org.junit.Test
class CommandOptionsTest {
@@ -35,6 +37,10 @@
private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" +
"services/core/services.core.wm.protologgroups/android_common/javac/" +
"services.core.wm.protologgroups.jar"
+ private const val TEST_VIEWER_CONFIG_FILE_PATH = "/some/viewer/config/file/path.pb"
+ private const val TEST_LEGACY_VIEWER_CONFIG_FILE_PATH =
+ "/some/viewer/config/file/path.json.gz"
+ private const val TEST_LEGACY_OUTPUT_FILE_PATH = "/some/output/file/path.winscope"
private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" +
"services.core.wm.protolog.srcjar"
private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" +
@@ -42,186 +48,263 @@
private const val TEST_LOG = "./test_log.pb"
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun noCommand() {
- CommandOptions(arrayOf())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(arrayOf())
+ }
+ assertThat(exception).hasMessageThat().contains("No command specified")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun invalidCommand() {
val testLine = "invalid"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("Unknown command")
}
@Test
fun transformClasses() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
val cmd = CommandOptions(testLine.split(' ').toTypedArray())
assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
- assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg)
assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+ assertEquals(TEST_LEGACY_VIEWER_CONFIG_FILE_PATH, cmd.legacyViewerConfigFilePathArg)
+ assertEquals(TEST_LEGACY_OUTPUT_FILE_PATH, cmd.legacyOutputFilePath)
assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
}
- @Test(expected = InvalidCommandException::class)
+ @Test
+ fun transformClasses_noViewerConfigFile() {
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--viewer-config-file-path")
+ }
+
+ @Test
+ fun transformClasses_noLegacyViewerConfigFile() {
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+ assertEquals(null, cmd.legacyViewerConfigFilePathArg)
+ assertEquals(TEST_LEGACY_OUTPUT_FILE_PATH, cmd.legacyOutputFilePath)
+ assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test
+ fun transformClasses_noLegacyOutputFile() {
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+ assertEquals(TEST_LEGACY_VIEWER_CONFIG_FILE_PATH, cmd.legacyViewerConfigFilePathArg)
+ assertEquals(null, cmd.legacyOutputFilePath)
+ assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test
fun transformClasses_noProtoLogClass() {
val testLine = "transform-protolog-calls " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--protolog-class")
}
- @Test(expected = InvalidCommandException::class)
- fun transformClasses_noProtoLogImplClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
- }
-
- @Test(expected = InvalidCommandException::class)
- fun transformClasses_noProtoLogCacheClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
- }
-
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_noProtoLogGroupClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--loggroups-class")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_noProtoLogGroupJar() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--loggroups-jar")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_noOutJar() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- TEST_JAVA_SRC.joinToString(" ")
- CommandOptions(testLine.split(' ').toTypedArray())
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+ "${TEST_JAVA_SRC.joinToString(" ")}"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--output-srcjar")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_noJavaInput() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("No java source input files")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_invalidProtoLogClass() {
- val testLine = "transform-protolog-calls --protolog-class invalid " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class invalid " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("class name invalid")
}
- @Test(expected = InvalidCommandException::class)
- fun transformClasses_invalidProtoLogImplClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class invalid " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
- }
-
- @Test(expected = InvalidCommandException::class)
- fun transformClasses_invalidProtoLogCacheClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class invalid " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
- }
-
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_invalidProtoLogGroupClass() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class invalid " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("class name invalid")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_invalidProtoLogGroupJar() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar invalid.txt " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat()
+ .contains("Jar file required, got invalid.txt instead")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_invalidOutJar() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+ "--output-srcjar invalid.pb ${TEST_JAVA_SRC.joinToString(" ")}"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat()
+ .contains("Source jar file required, got invalid.pb instead")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_invalidJavaInput() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR invalid.py"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val testLine = "transform-protolog-calls " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+ "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+ "--output-srcjar $TEST_SRC_JAR invalid.py"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat()
+ .contains("Not a java or kotlin source file invalid.py")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun transformClasses_unknownParam() {
val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
"--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
@@ -229,59 +312,88 @@
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
"--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
- }
-
- @Test(expected = InvalidCommandException::class)
- fun transformClasses_noValue() {
- val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
- "--protolog-impl-class " +
- "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
- "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
- "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--unknown")
}
@Test
- fun generateConfig() {
- val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ fun transformClasses_noValue() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("No value for --loggroups-class")
+ }
+
+ @Test
+ fun generateConfig_json() {
+ val testLine = "generate-viewer-config " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+ "--viewer-config-type json " +
+ "--viewer-config $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
val cmd = CommandOptions(testLine.split(' ').toTypedArray())
assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
- assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
}
- @Test(expected = InvalidCommandException::class)
+ @Test
+ fun generateConfig_proto() {
+ val testLine = "generate-viewer-config " +
+ "--protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-config-type proto " +
+ "--viewer-config $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test
fun generateConfig_noViewerConfig() {
val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
TEST_JAVA_SRC.joinToString(" ")
- CommandOptions(testLine.split(' ').toTypedArray())
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("--viewer-config required")
}
- @Test(expected = InvalidCommandException::class)
+ @Test
fun generateConfig_invalidViewerConfig() {
val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
"--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
"--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
- "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
- CommandOptions(testLine.split(' ').toTypedArray())
+ "--viewer-config invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
+ val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+ assertThat(exception).hasMessageThat().contains("required, got invalid.yaml instead")
}
@Test
fun readLog() {
- val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG"
+ val testLine = "read-log --viewer-config $TEST_VIEWER_JSON $TEST_LOG"
val cmd = CommandOptions(testLine.split(' ').toTypedArray())
assertEquals(CommandOptions.READ_LOG_CMD, cmd.command)
- assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
assertEquals(TEST_LOG, cmd.logProtofileArg)
}
}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
index 0d2b91d..822118c 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
@@ -16,22 +16,24 @@
package com.android.protolog.tool
-import org.junit.Assert
-import org.junit.Assert.assertTrue
-import org.junit.Test
+import com.android.protolog.tool.ProtoLogTool.PROTOLOG_IMPL_SRC_PATH
+import com.google.common.truth.Truth
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.OutputStream
import java.util.jar.JarInputStream
+import java.util.regex.Pattern
+import org.junit.Assert
+import org.junit.Test
class EndToEndTest {
@Test
fun e2e_transform() {
val output = run(
- src = "frameworks/base/org/example/Example.java" to """
+ srcs = mapOf("frameworks/base/org/example/Example.java" to """
package org.example;
import com.android.internal.protolog.common.ProtoLog;
import static com.android.internal.protolog.ProtoLogGroup.GROUP;
@@ -43,26 +45,29 @@
ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
}
}
- """.trimIndent(),
+ """.trimIndent()),
logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
commandOptions = CommandOptions(arrayOf("transform-protolog-calls",
"--protolog-class", "com.android.internal.protolog.common.ProtoLog",
- "--protolog-impl-class", "com.android.internal.protolog.ProtoLogImpl",
- "--protolog-cache-class",
- "com.android.server.wm.ProtoLogCache",
"--loggroups-class", "com.android.internal.protolog.ProtoLogGroup",
"--loggroups-jar", "not_required.jar",
+ "--viewer-config-file-path", "not_required.pb",
"--output-srcjar", "out.srcjar",
"frameworks/base/org/example/Example.java"))
)
val outSrcJar = assertLoadSrcJar(output, "out.srcjar")
- assertTrue(" 2066303299," in outSrcJar["frameworks/base/org/example/Example.java"]!!)
+ Truth.assertThat(outSrcJar["frameworks/base/org/example/Example.java"])
+ .containsMatch(Pattern.compile("\\{ String protoLogParam0 = " +
+ "String\\.valueOf\\(argString\\); long protoLogParam1 = argInt; " +
+ "com\\.android\\.internal\\.protolog.ProtoLogImpl_.*\\.d\\(" +
+ "GROUP, -6872339441335321086L, 4, null, protoLogParam0, protoLogParam1" +
+ "\\); \\}"))
}
@Test
fun e2e_viewerConfig() {
val output = run(
- src = "frameworks/base/org/example/Example.java" to """
+ srcs = mapOf("frameworks/base/org/example/Example.java" to """
package org.example;
import com.android.internal.protolog.common.ProtoLog;
import static com.android.internal.protolog.ProtoLogGroup.GROUP;
@@ -74,17 +79,27 @@
ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
}
}
- """.trimIndent(),
+ """.trimIndent()),
logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
commandOptions = CommandOptions(arrayOf("generate-viewer-config",
"--protolog-class", "com.android.internal.protolog.common.ProtoLog",
"--loggroups-class", "com.android.internal.protolog.ProtoLogGroup",
"--loggroups-jar", "not_required.jar",
- "--viewer-conf", "out.json",
+ "--viewer-config-type", "json",
+ "--viewer-config", "out.json",
"frameworks/base/org/example/Example.java"))
)
val viewerConfigJson = assertLoadText(output, "out.json")
- assertTrue("\"2066303299\"" in viewerConfigJson)
+ Truth.assertThat(viewerConfigJson).contains("""
+ "messages": {
+ "-6872339441335321086": {
+ "message": "Example: %s %d",
+ "level": "DEBUG",
+ "group": "GROUP",
+ "at": "org\/example\/Example.java"
+ }
+ }
+ """.trimIndent())
}
private fun assertLoadSrcJar(
@@ -112,21 +127,46 @@
}
fun run(
- src: Pair<String, String>,
+ srcs: Map<String, String>,
logGroup: LogGroup,
commandOptions: CommandOptions
): Map<String, ByteArray> {
val outputs = mutableMapOf<String, ByteArrayOutputStream>()
+ val srcs = srcs.toMutableMap()
+ srcs[PROTOLOG_IMPL_SRC_PATH] = """
+ package com.android.internal.protolog;
+
+ import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH;
+ import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH;
+ import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH;
+
+ import com.android.internal.protolog.common.ProtoLogToolInjected;
+
+ public class ProtoLogImpl {
+ @ProtoLogToolInjected(VIEWER_CONFIG_PATH)
+ private static String sViewerConfigPath;
+
+ @ProtoLogToolInjected(LEGACY_VIEWER_CONFIG_PATH)
+ private static String sLegacyViewerConfigPath;
+
+ @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
+ private static String sLegacyOutputFilePath;
+ }
+ """.trimIndent()
+
ProtoLogTool.injector = object : ProtoLogTool.Injector {
override fun fileOutputStream(file: String): OutputStream =
ByteArrayOutputStream().also { outputs[file] = it }
override fun readText(file: File): String {
- if (file.path == src.first) {
- return src.second
+ for (src in srcs.entries) {
+ val filePath = src.key
+ if (file.path == filePath) {
+ return src.value
+ }
}
- throw FileNotFoundException("expected: ${src.first}, but was $file")
+ throw FileNotFoundException("$file not found in [${srcs.keys.joinToString()}].")
}
override fun readLogGroups(jarPath: String, className: String) = mapOf(
diff --git a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
index 512d90c..1d32702 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
@@ -35,7 +35,7 @@
class LogParserTest {
private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java)
private val parser = LogParser(configParser)
- private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf()
+ private var config: MutableMap<Long, ViewerConfigParser.ConfigEntry> = mutableMapOf()
private var outStream: OutputStream = ByteArrayOutputStream()
private var printStream: PrintStream = PrintStream(outStream)
private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
similarity index 96%
rename from tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
rename to tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
index 90b8059..5e50f71 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
@@ -22,7 +22,7 @@
import org.junit.Assert.assertEquals
import org.junit.Test
-class ProtoLogCallProcessorTest {
+class ProtoLogCallProcessorImplTest {
private data class LogCall(
val call: MethodCallExpr,
val messageString: String,
@@ -32,8 +32,11 @@
private val groupMap: MutableMap<String, LogGroup> = mutableMapOf()
private val calls: MutableList<LogCall> = mutableListOf()
- private val visitor = ProtoLogCallProcessor("org.example.ProtoLog", "org.example.ProtoLogGroup",
- groupMap)
+ private val visitor = ProtoLogCallProcessorImpl(
+ "org.example.ProtoLog",
+ "org.example.ProtoLogGroup",
+ groupMap
+ )
private val processor = object : ProtoLogCallVisitor {
override fun processCall(
call: MethodCallExpr,
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt
deleted file mode 100644
index ea9a58d..0000000
--- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2019 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.protolog.tool
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class ProtoLogToolTest {
-
- @Test
- fun generateLogGroupCache() {
- val groups = mapOf(
- "GROUP1" to LogGroup("GROUP1", true, true, "TAG1"),
- "GROUP2" to LogGroup("GROUP2", true, true, "TAG2")
- )
- val code = ProtoLogTool.generateLogGroupCache("org.example", "ProtoLog\$Cache",
- groups, "org.example.ProtoLogImpl", "org.example.ProtoLogGroups")
-
- assertEquals("""
- package org.example;
-
- public class ProtoLog${'$'}Cache {
- public static boolean GROUP1_enabled = false;
- public static boolean GROUP2_enabled = false;
-
- static {
- org.example.ProtoLogImpl.sCacheUpdater = ProtoLog${'$'}Cache::update;
- update();
- }
-
- static void update() {
- GROUP1_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP1);
- GROUP2_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP2);
- }
- }
- """.trimIndent(), code)
- }
-}
\ No newline at end of file
diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
index f52bfec..de0b5ba 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
@@ -20,17 +20,14 @@
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.MethodCallExpr
-import com.github.javaparser.ast.stmt.IfStmt
+import com.google.common.truth.Truth
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
import org.junit.Test
import org.mockito.Mockito
class SourceTransformerTest {
companion object {
- private const val PROTO_LOG_IMPL_PATH = "org.example.ProtoLogImpl"
- /* ktlint-disable max-line-length */
private val TEST_CODE = """
package org.example;
@@ -79,7 +76,7 @@
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
}
}
""".trimIndent()
@@ -89,20 +86,20 @@
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2);
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2);
}
}
}
""".trimIndent()
- private val TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED = """
+ private val TRANSFORMED_CODE_MULTICALL_TEXT = """
package org.example;
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
}
}
""".trimIndent()
@@ -112,7 +109,7 @@
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { org.example.ProtoLogImpl.w(TEST_GROUP, -1741986185, 0, "test", (Object[]) null); }
+ { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); }
}
}
""".trimIndent()
@@ -122,7 +119,7 @@
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, null, protoLogParam0, protoLogParam1); }
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); }
}
}
""".trimIndent()
@@ -132,43 +129,19 @@
class Test {
void test() {
- if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, null, protoLogParam0, protoLogParam1, protoLogParam2);
+ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2);
}
}
}
""".trimIndent()
- private val TRANSFORMED_CODE_DISABLED = """
- package org.example;
-
- class Test {
- void test() {
- if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); }
- }
- }
- """.trimIndent()
-
- private val TRANSFORMED_CODE_MULTILINE_DISABLED = """
- package org.example;
-
- class Test {
- void test() {
- if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test");
-
- }
- }
- }
- """.trimIndent()
- /* ktlint-enable max-line-length */
-
private const val PATH = "com.example.Test.java"
}
private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
private val implName = "org.example.ProtoLogImpl"
- private val cacheName = "org.example.ProtoLogCache"
- private val sourceJarWriter = SourceTransformer(implName, cacheName, processor)
+ private val sourceJarWriter = SourceTransformer(implName, processor)
private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
@@ -176,9 +149,12 @@
fun processClass_textEnabled() {
var code = StaticJavaParser.parse(TEST_CODE)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
@@ -190,18 +166,15 @@
val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(3, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(1)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(6, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
assertEquals("protoLogParam0", methodCall.arguments[4].toString())
@@ -213,9 +186,12 @@
fun processClass_textEnabledMulticalls() {
var code = StaticJavaParser.parse(TEST_CODE_MULTICALLS)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
val calls = code.findAll(MethodCallExpr::class.java)
@@ -232,32 +208,32 @@
val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(3, ifStmts.size)
- val ifStmt = ifStmts[1]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(3, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(3)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(6, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
assertEquals("protoLogParam0", methodCall.arguments[4].toString())
assertEquals("protoLogParam1", methodCall.arguments[5].toString())
- assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED, out)
+ assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT, out)
}
@Test
fun processClass_textEnabledMultiline() {
var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
@@ -270,18 +246,15 @@
val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(4, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(1)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(7, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("1780316587", methodCall.arguments[1].toString())
+ assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
assertEquals("protoLogParam0", methodCall.arguments[4].toString())
assertEquals("protoLogParam1", methodCall.arguments[5].toString())
@@ -293,9 +266,12 @@
fun processClass_noParams() {
var code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test",
@@ -307,18 +283,15 @@
val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(1, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(1)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(5, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("-1741986185", methodCall.arguments[1].toString())
+ assertEquals("3218600869538902408L", methodCall.arguments[1].toString())
assertEquals(0.toString(), methodCall.arguments[2].toString())
assertEquals(TRANSFORMED_CODE_NO_PARAMS, out)
}
@@ -327,9 +300,12 @@
fun processClass_textDisabled() {
var code = StaticJavaParser.parse(TEST_CODE)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
@@ -341,18 +317,15 @@
val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(3, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(1)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(6, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
assertEquals("null", methodCall.arguments[3].toString())
assertEquals("protoLogParam0", methodCall.arguments[4].toString())
@@ -364,9 +337,12 @@
fun processClass_textDisabledMultiline() {
var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
+ Mockito.`when`(processor.process(
+ any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java),
+ any(MethodCallVisitor::class.java),
+ any(String::class.java))
+ ).thenAnswer { invocation ->
val visitor = invocation.arguments[1] as ProtoLogCallVisitor
visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
@@ -379,18 +355,15 @@
val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
code = StaticJavaParser.parse(out)
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
- assertFalse(ifStmt.elseStmt.isPresent)
- assertEquals(4, ifStmt.thenStmt.childNodes.size)
- val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
- assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+ it.scope.orElse(null)?.toString() == implName
+ }
+ Truth.assertThat(protoLogCalls).hasSize(1)
+ val methodCall = protoLogCalls[0] as MethodCallExpr
assertEquals("w", methodCall.name.asString())
assertEquals(7, methodCall.arguments.size)
assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
- assertEquals("1780316587", methodCall.arguments[1].toString())
+ assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
assertEquals("null", methodCall.arguments[3].toString())
assertEquals("protoLogParam0", methodCall.arguments[4].toString())
@@ -398,55 +371,4 @@
assertEquals("protoLogParam2", methodCall.arguments[6].toString())
assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out)
}
-
- @Test
- fun processClass_disabled() {
- var code = StaticJavaParser.parse(TEST_CODE)
-
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
- val visitor = invocation.arguments[1] as ProtoLogCallVisitor
-
- visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
- LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST"))
-
- invocation.arguments[0] as CompilationUnit
- }
-
- val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
- code = StaticJavaParser.parse(out)
-
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("false", ifStmt.condition.toString())
- assertEquals(TRANSFORMED_CODE_DISABLED, out)
- }
-
- @Test
- fun processClass_disabledMultiline() {
- var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
-
- Mockito.`when`(processor.process(any(CompilationUnit::class.java),
- any(ProtoLogCallVisitor::class.java), any(String::class.java)))
- .thenAnswer { invocation ->
- val visitor = invocation.arguments[1] as ProtoLogCallVisitor
-
- visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
- "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
- false, true, "WM_TEST"))
-
- invocation.arguments[0] as CompilationUnit
- }
-
- val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
- code = StaticJavaParser.parse(out)
-
- val ifStmts = code.findAll(IfStmt::class.java)
- assertEquals(1, ifStmts.size)
- val ifStmt = ifStmts[0]
- assertEquals("false", ifStmt.condition.toString())
- assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out)
- }
}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
similarity index 66%
rename from tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
rename to tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
index 52dce21..d27ae88 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
@@ -18,13 +18,12 @@
import com.android.internal.protolog.common.LogLevel
import com.android.json.stream.JsonReader
-import com.android.protolog.tool.ViewerConfigBuilder.LogCall
+import com.android.protolog.tool.ProtoLogTool.LogCall
+import java.io.StringReader
import org.junit.Assert.assertEquals
import org.junit.Test
-import org.mockito.Mockito
-import java.io.StringReader
-class ViewerConfigBuilderTest {
+class ViewerConfigJsonBuilderTest {
companion object {
private val TAG1 = "WM_TEST"
private val TAG2 = "WM_DEBUG"
@@ -39,20 +38,22 @@
private const val PATH = "/tmp/test.java"
}
- private val configBuilder = ViewerConfigBuilder(Mockito.mock(ProtoLogCallProcessor::class.java))
+ private val configBuilder = ViewerConfigJsonBuilder()
- private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> {
+ private fun parseConfig(json: String): Map<Long, ViewerConfigParser.ConfigEntry> {
return ViewerConfigParser().parseConfig(JsonReader(StringReader(json)))
}
@Test
fun processClass() {
- configBuilder.addLogCalls(listOf(
+ val logCallRegistry = ProtoLogTool.LogCallRegistry()
+ logCallRegistry.addLogCalls(listOf(
LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP2, PATH),
- LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH)).withContext())
+ LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH)))
- val parsedConfig = parseConfig(configBuilder.build())
+ val parsedConfig = parseConfig(
+ configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8))
assertEquals(3, parsedConfig.size)
assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH,
TEST1.messageString, LogLevel.INFO, GROUP1)])
@@ -64,32 +65,16 @@
@Test
fun processClass_nonUnique() {
- configBuilder.addLogCalls(listOf(
+ val logCallRegistry = ProtoLogTool.LogCallRegistry()
+ logCallRegistry.addLogCalls(listOf(
LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
- LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH)).withContext())
+ LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH)))
- val parsedConfig = parseConfig(configBuilder.build())
+ val parsedConfig = parseConfig(
+ configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8))
assertEquals(1, parsedConfig.size)
assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString,
- LogLevel.INFO, GROUP1)])
+ LogLevel.INFO, GROUP1)])
}
-
- @Test
- fun processClass_disabled() {
- configBuilder.addLogCalls(listOf(
- LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
- LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP_DISABLED, PATH),
- LogCall(TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED, PATH))
- .withContext())
-
- val parsedConfig = parseConfig(configBuilder.build())
- assertEquals(2, parsedConfig.size)
- assertEquals(TEST1, parsedConfig[CodeUtils.hash(
- PATH, TEST1.messageString, LogLevel.INFO, GROUP1)])
- assertEquals(TEST3, parsedConfig[CodeUtils.hash(
- PATH, TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED)])
- }
-
- private fun List<LogCall>.withContext() = map { it to ParsingContext() }
}