diff options
371 files changed, 10322 insertions, 2802 deletions
diff --git a/apct-tests/perftests/core/OWNERS b/apct-tests/perftests/core/OWNERS index 6abab6e27f8e..f8fe51c4fde9 100644 --- a/apct-tests/perftests/core/OWNERS +++ b/apct-tests/perftests/core/OWNERS @@ -12,3 +12,5 @@ per-file /apct-tests/perftests/core/src/android/content/res/* = file:/core/java/ per-file /apct-tests/perftests/core/src/android/content/om/* = felkachang@google.com per-file /apct-tests/perftests/core/src/android/content/om/* = file:/core/java/android/content/om/OWNERS +# Bug component: 44215 +per-file **Accessibility* = file:/core/java/android/view/accessibility/OWNERS
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 4f5eb37d871d..cbc9263a2c3d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1631,7 +1631,8 @@ public class JobSchedulerService extends com.android.server.SystemService jobStatus.getEstimatedNetworkDownloadBytes(), jobStatus.getEstimatedNetworkUploadBytes(), jobStatus.getWorkCount(), - ActivityManager.processStateAmToProto(mUidProcStates.get(jobStatus.getUid()))); + ActivityManager.processStateAmToProto(mUidProcStates.get(jobStatus.getUid())), + jobStatus.getNamespaceHash()); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -2059,7 +2060,8 @@ public class JobSchedulerService extends com.android.server.SystemService cancelled.getEstimatedNetworkDownloadBytes(), cancelled.getEstimatedNetworkUploadBytes(), cancelled.getWorkCount(), - ActivityManager.processStateAmToProto(mUidProcStates.get(cancelled.getUid()))); + ActivityManager.processStateAmToProto(mUidProcStates.get(cancelled.getUid())), + cancelled.getNamespaceHash()); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 90f1523104ee..0b08b6faf971 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -499,7 +499,8 @@ public final class JobServiceContext implements ServiceConnection { job.getEstimatedNetworkDownloadBytes(), job.getEstimatedNetworkUploadBytes(), job.getWorkCount(), - ActivityManager.processStateAmToProto(mService.getUidProcState(job.getUid()))); + ActivityManager.processStateAmToProto(mService.getUidProcState(job.getUid())), + job.getNamespaceHash()); sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount()); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { @@ -1557,7 +1558,8 @@ public final class JobServiceContext implements ServiceConnection { completedJob.getEstimatedNetworkUploadBytes(), completedJob.getWorkCount(), ActivityManager - .processStateAmToProto(mService.getUidProcState(completedJob.getUid()))); + .processStateAmToProto(mService.getUidProcState(completedJob.getUid())), + completedJob.getNamespaceHash()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", getId()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index a8b4c695889c..edd531d13965 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -43,6 +43,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.provider.MediaStore; import android.text.format.DateFormat; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -51,6 +52,7 @@ import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; @@ -65,10 +67,12 @@ import com.android.server.job.JobStatusShortInfoProto; import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.Random; import java.util.function.Predicate; /** @@ -88,6 +92,13 @@ public final class JobStatus { private static final String TAG = "JobScheduler.JobStatus"; static final boolean DEBUG = JobSchedulerService.DEBUG; + private static MessageDigest sMessageDigest; + /** Cache of namespace to hash to reduce how often we need to generate the namespace hash. */ + @GuardedBy("sNamespaceHashCache") + private static final ArrayMap<String, String> sNamespaceHashCache = new ArrayMap<>(); + /** Maximum size of {@link #sNamespaceHashCache}. */ + private static final int MAX_NAMESPACE_CACHE_SIZE = 128; + private static final int NUM_CONSTRAINT_CHANGE_HISTORY = 10; public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE; @@ -231,6 +242,8 @@ public final class JobStatus { final String sourceTag; @Nullable private final String mNamespace; + @Nullable + private final String mNamespaceHash; /** An ID that can be used to uniquely identify the job when logging statsd metrics. */ private final long mLoggingJobId; @@ -570,6 +583,7 @@ public final class JobStatus { this.callingUid = callingUid; this.standbyBucket = standbyBucket; mNamespace = namespace; + mNamespaceHash = generateNamespaceHash(namespace); mLoggingJobId = generateLoggingId(namespace, job.getId()); int tempSourceUid = -1; @@ -814,6 +828,56 @@ public final class JobStatus { return ((long) namespace.hashCode()) << 31 | jobId; } + @Nullable + private static String generateNamespaceHash(@Nullable String namespace) { + if (namespace == null) { + return null; + } + if (namespace.trim().isEmpty()) { + // Input is composed of all spaces (or nothing at all). + return namespace; + } + synchronized (sNamespaceHashCache) { + final int idx = sNamespaceHashCache.indexOfKey(namespace); + if (idx >= 0) { + return sNamespaceHashCache.valueAt(idx); + } + } + String hash = null; + try { + // .hashCode() can result in conflicts that would make distinguishing between + // namespaces hard and reduce the accuracy of certain metrics. Use SHA-256 + // to generate the hash since the probability of collision is extremely low. + if (sMessageDigest == null) { + sMessageDigest = MessageDigest.getInstance("SHA-256"); + } + final byte[] digest = sMessageDigest.digest(namespace.getBytes()); + // Convert to hexadecimal representation + StringBuilder hexBuilder = new StringBuilder(digest.length); + for (byte byteChar : digest) { + hexBuilder.append(String.format("%02X", byteChar)); + } + hash = hexBuilder.toString(); + } catch (Exception e) { + Slog.wtf(TAG, "Couldn't hash input", e); + } + if (hash == null) { + // If we get to this point, something went wrong with the MessageDigest above. + // Don't return the raw input value (which would defeat the purpose of hashing). + return "failed_namespace_hash"; + } + hash = hash.intern(); + synchronized (sNamespaceHashCache) { + if (sNamespaceHashCache.size() >= MAX_NAMESPACE_CACHE_SIZE) { + // Drop a random mapping instead of dropping at a predefined index to avoid + // potentially always dropping the same mapping. + sNamespaceHashCache.removeAt((new Random()).nextInt(MAX_NAMESPACE_CACHE_SIZE)); + } + sNamespaceHashCache.put(namespace, hash); + } + return hash; + } + public void enqueueWorkLocked(JobWorkItem work) { if (pendingWork == null) { pendingWork = new ArrayList<>(); @@ -1117,10 +1181,16 @@ public final class JobStatus { return true; } + @Nullable public String getNamespace() { return mNamespace; } + @Nullable + public String getNamespaceHash() { + return mNamespaceHash; + } + public String getSourceTag() { return sourceTag; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index f3763220f2b2..9dca29a4f287 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2169,6 +2169,10 @@ package android.net.wifi.sharedconnectivity.service { package android.os { + public class BatteryManager { + field public static final int BATTERY_PLUGGED_ANY = 15; // 0xf + } + public final class BatteryStatsManager { method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void resetBattery(boolean); method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void setBatteryLevel(int, boolean); @@ -3586,6 +3590,8 @@ package android.view { field public static final int ACCESSIBILITY_TITLE_CHANGED = 33554432; // 0x2000000 field public static final int FLAG_SLIPPERY = 536870912; // 0x20000000 field public CharSequence accessibilityTitle; + field public float preferredMaxDisplayRefreshRate; + field public float preferredMinDisplayRefreshRate; field public int privateFlags; } diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 12026aa3f72a..4cad58521c09 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -568,13 +568,13 @@ public abstract class Animator implements Cloneable { * repetition. lastPlayTime is similar and is used to calculate how many repeats have been * done between the two times. */ - void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) {} + void animateValuesInRange(long currentPlayTime, long lastPlayTime) {} /** * Internal use only. This animates any animation that has ended since lastPlayTime. * If an animation hasn't been finished, no change will be made. */ - void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) {} + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {} /** * Internal use only. Adds all start times (after delay) to and end times to times. diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 60659dc12342..70c3d7ae3f82 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -825,8 +825,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim private void animateBasedOnPlayTime( long currentPlayTime, long lastPlayTime, - boolean inReverse, - boolean notify + boolean inReverse ) { if (currentPlayTime < 0 || lastPlayTime < -1) { throw new UnsupportedOperationException("Error: Play time should never be negative."); @@ -857,8 +856,8 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim while (index < endIndex) { long playTime = startEndTimes[index]; if (lastPlayTime != playTime) { - animateSkipToEnds(playTime, lastPlayTime, notify); - animateValuesInRange(playTime, lastPlayTime, notify); + animateSkipToEnds(playTime, lastPlayTime); + animateValuesInRange(playTime, lastPlayTime); lastPlayTime = playTime; } index++; @@ -868,15 +867,15 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim index--; long playTime = startEndTimes[index]; if (lastPlayTime != playTime) { - animateSkipToEnds(playTime, lastPlayTime, notify); - animateValuesInRange(playTime, lastPlayTime, notify); + animateSkipToEnds(playTime, lastPlayTime); + animateValuesInRange(playTime, lastPlayTime); lastPlayTime = playTime; } } } if (currentPlayTime != lastPlayTime) { - animateSkipToEnds(currentPlayTime, lastPlayTime, notify); - animateValuesInRange(currentPlayTime, lastPlayTime, notify); + animateSkipToEnds(currentPlayTime, lastPlayTime); + animateValuesInRange(currentPlayTime, lastPlayTime); } } @@ -896,13 +895,11 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim } @Override - void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) { + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) { initAnimation(); if (lastPlayTime > currentPlayTime) { - if (notify) { - notifyStartListeners(true); - } + notifyStartListeners(true); for (int i = mEvents.size() - 1; i >= 0; i--) { AnimationEvent event = mEvents.get(i); Node node = event.mNode; @@ -916,31 +913,25 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim if (currentPlayTime <= start && start < lastPlayTime) { animator.animateSkipToEnds( 0, - lastPlayTime - node.mStartTime, - notify + lastPlayTime - node.mStartTime ); - if (notify) { - mPlayingSet.remove(node); - } + mPlayingSet.remove(node); } else if (start <= currentPlayTime && currentPlayTime <= end) { animator.animateSkipToEnds( currentPlayTime - node.mStartTime, - lastPlayTime - node.mStartTime, - notify + lastPlayTime - node.mStartTime ); - if (notify && !mPlayingSet.contains(node)) { + if (!mPlayingSet.contains(node)) { mPlayingSet.add(node); } } } } - if (currentPlayTime <= 0 && notify) { + if (currentPlayTime <= 0) { notifyEndListeners(true); } } else { - if (notify) { - notifyStartListeners(false); - } + notifyStartListeners(false); int eventsSize = mEvents.size(); for (int i = 0; i < eventsSize; i++) { AnimationEvent event = mEvents.get(i); @@ -955,45 +946,39 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim if (lastPlayTime < end && end <= currentPlayTime) { animator.animateSkipToEnds( end - node.mStartTime, - lastPlayTime - node.mStartTime, - notify + lastPlayTime - node.mStartTime ); - if (notify) { - mPlayingSet.remove(node); - } + mPlayingSet.remove(node); } else if (start <= currentPlayTime && currentPlayTime <= end) { animator.animateSkipToEnds( currentPlayTime - node.mStartTime, - lastPlayTime - node.mStartTime, - notify + lastPlayTime - node.mStartTime ); - if (notify && !mPlayingSet.contains(node)) { + if (!mPlayingSet.contains(node)) { mPlayingSet.add(node); } } } } - if (currentPlayTime >= getTotalDuration() && notify) { + if (currentPlayTime >= getTotalDuration()) { notifyEndListeners(false); } } } @Override - void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) { + void animateValuesInRange(long currentPlayTime, long lastPlayTime) { initAnimation(); - if (notify) { - if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { - notifyStartListeners(false); - } else { - long duration = getTotalDuration(); - if (duration >= 0 - && (lastPlayTime > duration || (lastPlayTime == duration - && currentPlayTime < duration)) - ) { - notifyStartListeners(true); - } + if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { + notifyStartListeners(false); + } else { + long duration = getTotalDuration(); + if (duration >= 0 + && (lastPlayTime > duration || (lastPlayTime == duration + && currentPlayTime < duration)) + ) { + notifyStartListeners(true); } } @@ -1014,8 +999,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim ) { animator.animateValuesInRange( currentPlayTime - node.mStartTime, - Math.max(-1, lastPlayTime - node.mStartTime), - notify + Math.max(-1, lastPlayTime - node.mStartTime) ); } } @@ -1111,7 +1095,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim } } mSeekState.setPlayTime(playTime, mReversing); - animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true); + animateBasedOnPlayTime(playTime, lastPlayTime, mReversing); } /** @@ -1144,16 +1128,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim private void initChildren() { if (!isInitialized()) { mChildrenInitialized = true; - - // We have to initialize all the start values so that they are based on the previous - // values. - long[] times = ensureChildStartAndEndTimes(); - - long previousTime = -1; - for (long time : times) { - animateBasedOnPlayTime(time, previousTime, false, false); - previousTime = time; - } + skipToEndValue(false); } } @@ -1489,6 +1464,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim anim.mPauseTime = -1; anim.mSeekState = new SeekState(); anim.mSelfPulse = true; + anim.mStartListenersCalled = false; anim.mPlayingSet = new ArrayList<Node>(); anim.mNodeMap = new ArrayMap<Animator, Node>(); anim.mNodes = new ArrayList<Node>(nodeCount); diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index ead238f75ba4..5de7f387b206 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1417,21 +1417,19 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio * will be called. */ @Override - void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) { + void animateValuesInRange(long currentPlayTime, long lastPlayTime) { if (currentPlayTime < 0 || lastPlayTime < -1) { throw new UnsupportedOperationException("Error: Play time should never be negative."); } initAnimation(); long duration = getTotalDuration(); - if (notify) { - if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { - notifyStartListeners(false); - } else if (lastPlayTime > duration - || (lastPlayTime == duration && currentPlayTime < duration) - ) { - notifyStartListeners(true); - } + if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) { + notifyStartListeners(false); + } else if (lastPlayTime > duration + || (lastPlayTime == duration && currentPlayTime < duration) + ) { + notifyStartListeners(true); } if (duration >= 0) { lastPlayTime = Math.min(duration, lastPlayTime); @@ -1448,7 +1446,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio iteration = Math.min(iteration, mRepeatCount); lastIteration = Math.min(lastIteration, mRepeatCount); - if (notify && iteration != lastIteration) { + if (iteration != lastIteration) { notifyListeners(AnimatorCaller.ON_REPEAT, false); } } @@ -1464,7 +1462,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio } @Override - void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) { + void animateSkipToEnds(long currentPlayTime, long lastPlayTime) { boolean inReverse = currentPlayTime < lastPlayTime; boolean doSkip; if (currentPlayTime <= 0 && lastPlayTime > 0) { @@ -1474,13 +1472,9 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio doSkip = duration >= 0 && currentPlayTime >= duration && lastPlayTime < duration; } if (doSkip) { - if (notify) { - notifyStartListeners(inReverse); - } + notifyStartListeners(inReverse); skipToEndValue(inReverse); - if (notify) { - notifyEndListeners(inReverse); - } + notifyEndListeners(inReverse); } } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index aa1f5c0e5643..b2298064aaa4 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1084,6 +1084,16 @@ public class Activity extends ContextThemeWrapper } /** + * Update the forced status bar appearance. + * @hide + */ + @Override + public void updateStatusBarAppearance(int appearance) { + mTaskDescription.setStatusBarAppearance(appearance); + setTaskDescription(mTaskDescription); + } + + /** * Update the forced navigation bar color. * @hide */ diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 1df1781fa3f1..d76e20114816 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -81,6 +81,7 @@ import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Singleton; import android.util.Size; +import android.view.WindowInsetsController.Appearance; import android.window.TaskSnapshot; import com.android.internal.app.LocalePicker; @@ -1558,6 +1559,8 @@ public class ActivityManager { private int mColorBackgroundFloating; private int mStatusBarColor; private int mNavigationBarColor; + @Appearance + private int mStatusBarAppearance; private boolean mEnsureStatusBarContrastWhenTransparent; private boolean mEnsureNavigationBarContrastWhenTransparent; private int mResizeMode; @@ -1658,8 +1661,8 @@ public class ActivityManager { final Icon icon = mIconRes == Resources.ID_NULL ? null : Icon.createWithResource(ActivityThread.currentPackageName(), mIconRes); return new TaskDescription(mLabel, icon, mPrimaryColor, mBackgroundColor, - mStatusBarColor, mNavigationBarColor, false, false, RESIZE_MODE_RESIZEABLE, - -1, -1, 0); + mStatusBarColor, mNavigationBarColor, 0, false, false, + RESIZE_MODE_RESIZEABLE, -1, -1, 0); } } @@ -1677,7 +1680,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) { this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes), - colorPrimary, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + colorPrimary, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1695,7 +1698,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, @DrawableRes int iconRes) { this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes), - 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1707,7 +1710,7 @@ public class ActivityManager { */ @Deprecated public TaskDescription(String label) { - this(label, null, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + this(label, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1717,7 +1720,7 @@ public class ActivityManager { */ @Deprecated public TaskDescription() { - this(null, null, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + this(null, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1733,7 +1736,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, Bitmap icon, int colorPrimary) { this(label, icon != null ? Icon.createWithBitmap(icon) : null, colorPrimary, 0, 0, 0, - false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1749,14 +1752,15 @@ public class ActivityManager { */ @Deprecated public TaskDescription(String label, Bitmap icon) { - this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, false, false, - RESIZE_MODE_RESIZEABLE, -1, -1, 0); + this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, false, + false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @hide */ public TaskDescription(@Nullable String label, @Nullable Icon icon, int colorPrimary, int colorBackground, int statusBarColor, int navigationBarColor, + @Appearance int statusBarAppearance, boolean ensureStatusBarContrastWhenTransparent, boolean ensureNavigationBarContrastWhenTransparent, int resizeMode, int minWidth, int minHeight, int colorBackgroundFloating) { @@ -1766,6 +1770,7 @@ public class ActivityManager { mColorBackground = colorBackground; mStatusBarColor = statusBarColor; mNavigationBarColor = navigationBarColor; + mStatusBarAppearance = statusBarAppearance; mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = ensureNavigationBarContrastWhenTransparent; @@ -1794,6 +1799,7 @@ public class ActivityManager { mColorBackground = other.mColorBackground; mStatusBarColor = other.mStatusBarColor; mNavigationBarColor = other.mNavigationBarColor; + mStatusBarAppearance = other.mStatusBarAppearance; mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = other.mEnsureNavigationBarContrastWhenTransparent; @@ -1823,6 +1829,9 @@ public class ActivityManager { if (other.mNavigationBarColor != 0) { mNavigationBarColor = other.mNavigationBarColor; } + if (other.mStatusBarAppearance != 0) { + mStatusBarAppearance = other.mStatusBarAppearance; + } mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = @@ -2094,6 +2103,14 @@ public class ActivityManager { /** * @hide */ + @Appearance + public int getStatusBarAppearance() { + return mStatusBarAppearance; + } + + /** + * @hide + */ public void setEnsureStatusBarContrastWhenTransparent( boolean ensureStatusBarContrastWhenTransparent) { mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent; @@ -2102,6 +2119,13 @@ public class ActivityManager { /** * @hide */ + public void setStatusBarAppearance(@Appearance int statusBarAppearance) { + mStatusBarAppearance = statusBarAppearance; + } + + /** + * @hide + */ public boolean getEnsureNavigationBarContrastWhenTransparent() { return mEnsureNavigationBarContrastWhenTransparent; } @@ -2223,6 +2247,7 @@ public class ActivityManager { dest.writeInt(mColorBackground); dest.writeInt(mStatusBarColor); dest.writeInt(mNavigationBarColor); + dest.writeInt(mStatusBarAppearance); dest.writeBoolean(mEnsureStatusBarContrastWhenTransparent); dest.writeBoolean(mEnsureNavigationBarContrastWhenTransparent); dest.writeInt(mResizeMode); @@ -2246,6 +2271,7 @@ public class ActivityManager { mColorBackground = source.readInt(); mStatusBarColor = source.readInt(); mNavigationBarColor = source.readInt(); + mStatusBarAppearance = source.readInt(); mEnsureStatusBarContrastWhenTransparent = source.readBoolean(); mEnsureNavigationBarContrastWhenTransparent = source.readBoolean(); mResizeMode = source.readInt(); @@ -2294,6 +2320,7 @@ public class ActivityManager { && mColorBackground == other.mColorBackground && mStatusBarColor == other.mStatusBarColor && mNavigationBarColor == other.mNavigationBarColor + && mStatusBarAppearance == other.mStatusBarAppearance && mEnsureStatusBarContrastWhenTransparent == other.mEnsureStatusBarContrastWhenTransparent && mEnsureNavigationBarContrastWhenTransparent diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 7be00a045403..3487e0b1f3e8 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -827,6 +827,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS = 1 << 4; + /** + * Whether AbiOverride was used when installing this application. + * @hide + */ + public static final int PRIVATE_FLAG_EXT_CPU_OVERRIDE = 1 << 5; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_EXT_" }, value = { PRIVATE_FLAG_EXT_PROFILEABLE, @@ -834,6 +840,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE, PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK, PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS, + PRIVATE_FLAG_EXT_CPU_OVERRIDE, }) @Retention(RetentionPolicy.SOURCE) public @interface ApplicationInfoPrivateFlagsExt {} diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 34a697ab285b..96af2b6caf9f 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -187,7 +187,7 @@ public final class UserProperties implements Parcelable { * device policies from its parent profile. * *<p> All the user restrictions and device policies would be not propagated to the profile - * with this property value. The {(TODO:b/256978256) @link DevicePolicyEngine} + * with this property value. The {@link com.android.server.devicepolicy.DevicePolicyEngine} * uses this property to determine and propagate only select ones to the given profile. * * @hide diff --git a/core/java/android/hardware/SensorAdditionalInfo.java b/core/java/android/hardware/SensorAdditionalInfo.java index 12edc5eb7e33..59def9fb3325 100644 --- a/core/java/android/hardware/SensorAdditionalInfo.java +++ b/core/java/android/hardware/SensorAdditionalInfo.java @@ -63,7 +63,7 @@ public class SensorAdditionalInfo { public final int[] intValues; /** - * Typical values of additional infomation type. The set of values is subject to extension in + * Typical values of additional information type. The set of values is subject to extension in * newer versions and vendors have the freedom of define their own custom values. * * @hide diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index c2aebd7cfaca..f033f9740b3d 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -893,9 +893,9 @@ public abstract class SensorManager { /** * Flushes the FIFO of all the sensors registered for this listener. If there are events - * in the FIFO of the sensor, they are returned as if the maxReportLantecy of the FIFO has + * in the FIFO of the sensor, they are returned as if the maxReportLatency of the FIFO has * expired. Events are returned in the usual way through the SensorEventListener. - * This call doesn't affect the maxReportLantecy for this sensor. This call is asynchronous and + * This call doesn't affect the maxReportLatency for this sensor. This call is asynchronous and * returns immediately. * {@link android.hardware.SensorEventListener2#onFlushCompleted onFlushCompleted} is called * after all the events in the batch at the time of calling this method have been delivered @@ -923,7 +923,7 @@ public abstract class SensorManager { * Create a sensor direct channel backed by shared memory wrapped in MemoryFile object. * * The resulting channel can be used for delivering sensor events to native code, other - * processes, GPU/DSP or other co-processors without CPU intervention. This is the recommanded + * processes, GPU/DSP or other co-processors without CPU intervention. This is the recommended * for high performance sensor applications that use high sensor rates (e.g. greater than 200Hz) * and cares about sensor event latency. * @@ -945,7 +945,7 @@ public abstract class SensorManager { * Create a sensor direct channel backed by shared memory wrapped in HardwareBuffer object. * * The resulting channel can be used for delivering sensor events to native code, other - * processes, GPU/DSP or other co-processors without CPU intervention. This is the recommanded + * processes, GPU/DSP or other co-processors without CPU intervention. This is the recommended * for high performance sensor applications that use high sensor rates (e.g. greater than 200Hz) * and cares about sensor event latency. * @@ -1355,11 +1355,11 @@ public abstract class SensorManager { * returned by {@link #getRotationMatrix}. * * @param X - * defines the axis of the new cooridinate system that coincide with the X axis of the + * defines the axis of the new coordinate system that coincide with the X axis of the * original coordinate system. * * @param Y - * defines the axis of the new cooridinate system that coincide with the Y axis of the + * defines the axis of the new coordinate system that coincide with the Y axis of the * original coordinate system. * * @param outR @@ -1847,7 +1847,7 @@ public abstract class SensorManager { * This method is used to inject raw sensor data into the HAL. Call {@link * initDataInjection(boolean)} before this method to set the HAL in data injection mode. This * method should be called only if a previous call to initDataInjection has been successful and - * the HAL and SensorService are already opreating in data injection mode. + * the HAL and SensorService are already operating in data injection mode. * * @param sensor The sensor to inject. * @param values Sensor values to inject. The length of this diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index d8ab6f7da82d..0f6f6019c0f7 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -565,7 +565,7 @@ public class SystemSensorManager extends SensorManager { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_DYNAMIC_SENSOR_CHANGED)) { if (DEBUG_DYNAMIC_SENSOR) { - Log.i(TAG, "DYNS received DYNAMIC_SENSOR_CHANED broadcast"); + Log.i(TAG, "DYNS received DYNAMIC_SENSOR_CHANGED broadcast"); } // Dynamic sensors probably changed mDynamicSensorListDirty = true; @@ -610,7 +610,7 @@ public class SystemSensorManager extends SensorManager { protected void unregisterDynamicSensorCallbackImpl( DynamicSensorCallback callback) { if (DEBUG_DYNAMIC_SENSOR) { - Log.i(TAG, "Removing dynamic sensor listerner"); + Log.i(TAG, "Removing dynamic sensor listener"); } mDynamicSensorCallbacks.remove(callback); } @@ -740,7 +740,7 @@ public class SystemSensorManager extends SensorManager { } if (hardwareBuffer.getWidth() < MIN_DIRECT_CHANNEL_BUFFER_SIZE) { throw new IllegalArgumentException( - "Width if HaradwareBuffer must be greater than " + "Width if HardwareBuffer must be greater than " + MIN_DIRECT_CHANNEL_BUFFER_SIZE); } if ((hardwareBuffer.getUsage() & HardwareBuffer.USAGE_SENSOR_DIRECT_DATA) == 0) { @@ -771,7 +771,7 @@ public class SystemSensorManager extends SensorManager { /* * BaseEventQueue is the communication channel with the sensor service, - * SensorEventQueue, TriggerEventQueue are subclases and there is one-to-one mapping between + * SensorEventQueue, TriggerEventQueue are subclasses and there is one-to-one mapping between * the queues and the listeners. InjectEventQueue is also a sub-class which is a special case * where data is being injected into the sensor HAL through the sensor service. It is not * associated with any listener and there is one InjectEventQueue associated with a diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index b4533042b4a3..76efce56dcf0 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -53,8 +53,6 @@ import android.view.Surface; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; -import libcore.util.EmptyArray; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; @@ -667,7 +665,7 @@ public final class DisplayManager { } else if (category == null || DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED.equals(category)) { return getDisplays(displayIds, Objects::nonNull); } - return (Display[]) EmptyArray.OBJECT; + return new Display[0]; } private Display[] getDisplays(int[] displayIds, Predicate<Display> predicate) { diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index d6df03321298..94bff893b5a8 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -413,6 +413,15 @@ public abstract class DisplayManagerInternal { public abstract HostUsiVersion getHostUsiVersion(int displayId); /** + * Get the ALS data for a particular display. + * + * @param displayId The id of the display. + * @return {@link AmbientLightSensorData} + */ + @Nullable + public abstract AmbientLightSensorData getAmbientLightSensorData(int displayId); + + /** * Get all available DisplayGroupIds. */ public abstract IntArray getDisplayGroupIds(); @@ -669,4 +678,23 @@ public abstract class DisplayManagerInternal { return "RefreshRateLimitation(" + type + ": " + range + ")"; } } + + /** + * Class to provide Ambient sensor data using the API + * {@link DisplayManagerInternal#getAmbientLightSensorData(int)} + */ + public static final class AmbientLightSensorData { + public String sensorName; + public String sensorType; + + public AmbientLightSensorData(String name, String type) { + sensorName = name; + sensorType = type; + } + + @Override + public String toString() { + return "AmbientLightSensorData(" + sensorName + ", " + sensorType + ")"; + } + } } diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 6bc0f6ea947c..092923e927a3 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -20,6 +20,7 @@ import android.Manifest.permission; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; @@ -232,6 +233,7 @@ public class BatteryManager { OsProtoEnums.CHARGING_POLICY_ADAPTIVE_LONGLIFE; // = 4 /** @hide */ + @TestApi public static final int BATTERY_PLUGGED_ANY = BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS | BATTERY_PLUGGED_DOCK; diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 01a886434376..0144d22d7aec 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -742,7 +742,7 @@ public class UserManager { * The default value is <code>false</code>. * * <p>Holders of the permission - * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} * can set this restriction using the DevicePolicyManager APIs mentioned below. * * <p>Key for user restrictions. @@ -947,7 +947,7 @@ public class UserManager { * this restriction will be set as a base restriction which cannot be removed by any admin. * * <p>Holders of the permission - * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} * can set this restriction using the DevicePolicyManager APIs mentioned below. * * <p>Key for user restrictions. @@ -1457,7 +1457,7 @@ public class UserManager { * affected. The default value is <code>false</code>. * * <p>Holders of the permission - * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} * can set this restriction using the DevicePolicyManager APIs mentioned below. * * <p>Key for user restrictions. @@ -1603,7 +1603,7 @@ public class UserManager { * set. * * <p>Holders of the permission - * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} * can set this restriction using the DevicePolicyManager APIs mentioned below. * * <p>The default value is <code>false</code>. diff --git a/core/java/android/os/image/DynamicSystemClient.java b/core/java/android/os/image/DynamicSystemClient.java index 218ecc8c5571..88096ab91261 100644 --- a/core/java/android/os/image/DynamicSystemClient.java +++ b/core/java/android/os/image/DynamicSystemClient.java @@ -209,6 +209,13 @@ public class DynamicSystemClient { public static final String ACTION_HIDE_NOTIFICATION = "android.os.image.action.HIDE_NOTIFICATION"; + /** + * Intent action: notify the service to post a status update when keyguard is dismissed. + * @hide + */ + public static final String ACTION_NOTIFY_KEYGUARD_DISMISSED = + "android.os.image.action.NOTIFY_KEYGUARD_DISMISSED"; + /* * Intent Keys */ diff --git a/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java b/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java index fb6061916d99..4aa5c00a6a52 100644 --- a/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java +++ b/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java @@ -48,7 +48,7 @@ import java.io.PrintWriter; * * @hide */ -public final class ContentCaptureServiceInfo { +public class ContentCaptureServiceInfo { private static final String TAG = ContentCaptureServiceInfo.class.getSimpleName(); private static final String XML_TAG_SERVICE = "content-capture-service"; diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index 946fc30eecb3..fb6de56b0952 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -31,6 +31,7 @@ import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.text.style.URLSpan; +import android.text.TextUtils; import android.util.Log; import android.util.Patterns; import android.webkit.WebView; @@ -672,8 +673,15 @@ public class Linkify { @Nullable Context context) { PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); final Context ctx = (context != null) ? context : ActivityThread.currentApplication(); - final String regionCode = (ctx != null) ? ctx.getSystemService(TelephonyManager.class). - getSimCountryIso().toUpperCase(Locale.US) : Locale.getDefault().getCountry(); + String regionCode = Locale.getDefault().getCountry(); + if (ctx != null) { + String simCountryIso = ctx.getSystemService(TelephonyManager.class).getSimCountryIso(); + if (!TextUtils.isEmpty(simCountryIso)) { + // Assign simCountryIso if it is available + regionCode = simCountryIso.toUpperCase(Locale.US); + } + } + Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(), regionCode, Leniency.POSSIBLE, Long.MAX_VALUE); for (PhoneNumberMatch match : matches) { diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 6551a1884af0..253073a83827 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -129,27 +129,25 @@ public class HapticFeedbackConstants { public static final int REJECT = 17; /** - * A haptic effect to provide texture while a rotary input device is being scrolled. + * A haptic effect to provide texture while scrolling. * * @hide */ - public static final int ROTARY_SCROLL_TICK = 18; + public static final int SCROLL_TICK = 18; /** - * A haptic effect to signal that a list element has been focused while scrolling using a rotary - * input device. + * A haptic effect to signal that a list element has been focused while scrolling. * * @hide */ - public static final int ROTARY_SCROLL_ITEM_FOCUS = 19; + public static final int SCROLL_ITEM_FOCUS = 19; /** - * A haptic effect to signal reaching the scrolling limits of a list while scrolling using a - * rotary input device. + * A haptic effect to signal reaching the scrolling limits of a list while scrolling. * * @hide */ - public static final int ROTARY_SCROLL_LIMIT = 20; + public static final int SCROLL_LIMIT = 20; /** * The user has toggled a switch or button into the on position. diff --git a/core/java/android/view/HapticScrollFeedbackProvider.java b/core/java/android/view/HapticScrollFeedbackProvider.java new file mode 100644 index 000000000000..7e103a53d4ed --- /dev/null +++ b/core/java/android/view/HapticScrollFeedbackProvider.java @@ -0,0 +1,141 @@ +/** + * 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 android.view; + +import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * {@link ScrollFeedbackProvider} that performs haptic feedback when scrolling. + * + * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state + * is isolated. + * + * @hide + */ +public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { + private static final String TAG = "HapticScrollFeedbackProvider"; + + private static final int TICK_INTERVAL_NO_TICK = 0; + private static final int TICK_INTERVAL_UNSET = Integer.MAX_VALUE; + + private final View mView; + + + // Info about the cause of the latest scroll event. + /** The ID of the {link @InputDevice} that caused the latest scroll event. */ + private int mDeviceId = -1; + /** The axis on which the latest scroll event happened. */ + private int mAxis = -1; + /** The {@link InputDevice} source from which the latest scroll event happened. */ + private int mSource = -1; + + /** + * Cache for tick interval for scroll tick caused by a {@link InputDevice#SOURCE_ROTARY_ENCODER} + * on {@link MotionEvent#AXIS_SCROLL}. Set to -1 if the value has not been fetched and cached. + */ + private int mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_UNSET; + /** The tick interval corresponding to the current InputDevice/source/axis. */ + private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK; + private int mTotalScrollPixels = 0; + private boolean mCanPlayLimitFeedback = true; + + public HapticScrollFeedbackProvider(View view) { + this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET); + } + + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) { + mView = view; + mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels; + } + + @Override + public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) { + maybeUpdateCurrentConfig(event, axis); + + if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) { + // There's no valid tick interval. Exit early before doing any further computation. + return; + } + + mTotalScrollPixels += deltaInPixels; + + if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) { + mTotalScrollPixels %= mTickIntervalPixels; + // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); + } + + mCanPlayLimitFeedback = true; + } + + @Override + public void onScrollLimit(MotionEvent event, int axis, boolean isStart) { + maybeUpdateCurrentConfig(event, axis); + + if (!mCanPlayLimitFeedback) { + return; + } + + // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT); + + mCanPlayLimitFeedback = false; + } + + @Override + public void onSnapToItem(MotionEvent event, int axis) { + // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + mCanPlayLimitFeedback = true; + } + + private void maybeUpdateCurrentConfig(MotionEvent event, int axis) { + int source = event.getSource(); + int deviceId = event.getDeviceId(); + + if (mAxis != axis || mSource != source || mDeviceId != deviceId) { + mSource = source; + mAxis = axis; + mDeviceId = deviceId; + + mCanPlayLimitFeedback = true; + mTotalScrollPixels = 0; + calculateTickIntervals(source, axis); + } + } + + private void calculateTickIntervals(int source, int axis) { + mTickIntervalPixels = TICK_INTERVAL_NO_TICK; + + if (axis == MotionEvent.AXIS_SCROLL && source == InputDevice.SOURCE_ROTARY_ENCODER) { + if (mRotaryEncoderAxisScrollTickIntervalPixels == TICK_INTERVAL_UNSET) { + // Value has not been fetched yet. Fetch and cache it. + mRotaryEncoderAxisScrollTickIntervalPixels = + mView.getContext().getResources().getDimensionPixelSize( + config_rotaryEncoderAxisScrollTickInterval); + if (mRotaryEncoderAxisScrollTickIntervalPixels < 0) { + mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_NO_TICK; + } + } + mTickIntervalPixels = mRotaryEncoderAxisScrollTickIntervalPixels; + } + } +} diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 48ae59b618fb..a9a5888207aa 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -480,10 +480,23 @@ interface IWindowManager * Requests Keyboard Shortcuts from the displayed window. * * @param receiver The receiver to deliver the results to. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * @see #requestImeKeyboardShortcuts(IResultReceiver, int) */ void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId); /** + * Requests Keyboard Shortcuts from currently selected IME. + * + * @param receiver The receiver to deliver the results to. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * @see #requestAppKeyboardShortcuts(IResultReceiver, int) + */ + void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId); + + /** * Retrieves the current stable insets from the primary display. */ @UnsupportedAppUsage diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 7e4e4022f00f..5019b85ca503 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1099,21 +1099,25 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // TODO: Support a ResultReceiver for IME. // TODO(b/123718661): Make show() work for multi-session IME. int typesReady = 0; + final boolean imeVisible = mState.isSourceOrDefaultVisible( + mImeSourceConsumer.getId(), ime()); for (int type = FIRST; type <= LAST; type = type << 1) { if ((types & type) == 0) { continue; } @AnimationType final int animationType = getAnimationType(type); final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0; - final boolean isImeAnimation = type == ime(); - if (requestedVisible && animationType == ANIMATION_TYPE_NONE - || animationType == ANIMATION_TYPE_SHOW) { + final boolean isIme = type == ime(); + var alreadyVisible = requestedVisible && (!isIme || imeVisible) + && animationType == ANIMATION_TYPE_NONE; + var alreadyAnimatingShow = animationType == ANIMATION_TYPE_SHOW; + if (alreadyVisible || alreadyAnimatingShow) { // no-op: already shown or animating in (because window visibility is // applied before starting animation). if (DEBUG) Log.d(TAG, String.format( "show ignored for type: %d animType: %d requestedVisible: %s", type, animationType, requestedVisible)); - if (isImeAnimation) { + if (isIme) { ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); } @@ -1121,13 +1125,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } if (fromIme && animationType == ANIMATION_TYPE_USER) { // App is already controlling the IME, don't cancel it. - if (isImeAnimation) { + if (isIme) { ImeTracker.forLogging().onFailed( statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); } continue; } - if (isImeAnimation) { + if (isIme) { ImeTracker.forLogging().onProgress( statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION); } diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index a47f34f4125e..83bdb087a539 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -227,6 +227,10 @@ public class InsetsFrameProvider implements Parcelable { if (mArbitraryRectangle != null) { sb.append(", mArbitraryRectangle=").append(mArbitraryRectangle.toShortString()); } + if (mMinimalInsetsSizeInDisplayCutoutSafe != null) { + sb.append(", mMinimalInsetsSizeInDisplayCutoutSafe=") + .append(mMinimalInsetsSizeInDisplayCutoutSafe); + } sb.append("}"); return sb.toString(); } @@ -252,6 +256,7 @@ public class InsetsFrameProvider implements Parcelable { mInsetsSize = in.readTypedObject(Insets.CREATOR); mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR); mArbitraryRectangle = in.readTypedObject(Rect.CREATOR); + mMinimalInsetsSizeInDisplayCutoutSafe = in.readTypedObject(Insets.CREATOR); } @Override @@ -262,6 +267,7 @@ public class InsetsFrameProvider implements Parcelable { out.writeTypedObject(mInsetsSize, flags); out.writeTypedArray(mInsetsSizeOverrides, flags); out.writeTypedObject(mArbitraryRectangle, flags); + out.writeTypedObject(mMinimalInsetsSizeInDisplayCutoutSafe, flags); } public boolean idEquals(InsetsFrameProvider o) { @@ -280,13 +286,16 @@ public class InsetsFrameProvider implements Parcelable { return mId == other.mId && mSource == other.mSource && mFlags == other.mFlags && Objects.equals(mInsetsSize, other.mInsetsSize) && Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides) - && Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle); + && Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle) + && Objects.equals(mMinimalInsetsSizeInDisplayCutoutSafe, + other.mMinimalInsetsSizeInDisplayCutoutSafe); } @Override public int hashCode() { return Objects.hash(mId, mSource, mFlags, mInsetsSize, - Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle); + Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle, + mMinimalInsetsSizeInDisplayCutoutSafe); } public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR = diff --git a/core/java/android/view/ScrollFeedbackProvider.java b/core/java/android/view/ScrollFeedbackProvider.java new file mode 100644 index 000000000000..8d3491df5d9e --- /dev/null +++ b/core/java/android/view/ScrollFeedbackProvider.java @@ -0,0 +1,63 @@ +/** + * 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 android.view; + +/** + * Interface to represent an entity giving consistent feedback for different events surrounding view + * scroll. + * + * @hide + */ +public interface ScrollFeedbackProvider { + /** + * The view has snapped to an item, with a motion from a given {@link MotionEvent} on a given + * {@code axis}. + * + * <p>The interface is not aware of the internal scroll states of the view for which scroll + * feedback is played. As such, the client should call + * {@link #onScrollLimit(MotionEvent, int, int)} when scrolling has reached limit. + * + * @param event the {@link MotionEvent} that caused the item to snap. + * @param axis the axis of {@code event} that caused the item to snap. + */ + void onSnapToItem(MotionEvent event, int axis); + + /** + * The view has reached the scroll limit when scrolled by the motion from a given + * {@link MotionEvent} on a given {@code axis}. + * + * @param event the {@link MotionEvent} that caused scrolling to hit the limit. + * @param axis the axis of {@code event} that caused scrolling to hit the limit. + * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and + * {@code false} if the scrolling hit limit at the end of the scrolling list. + */ + void onScrollLimit(MotionEvent event, int axis, boolean isStart); + + /** + * The view has scrolled by {@code deltaInPixels} due to the motion from a given + * {@link MotionEvent} on a given {@code axis}. + * + * <p>The interface is not aware of the internal scroll states of the view for which scroll + * feedback is played. As such, the client should call + * {@link #onScrollLimit(MotionEvent, int, int)} when scrolling has reached limit. + * + * @param event the {@link MotionEvent} that caused scroll progress. + * @param axis the axis of {@code event} that caused scroll progress. + * @param deltaInPixels the amount of scroll progress, in pixels. + */ + void onScrollProgress(MotionEvent event, int axis, int deltaInPixels); +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 36954a9941c4..38e3526ac6b8 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -8414,6 +8414,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * for the matching <activity> entry in your application’s manifest or updating the title at * runtime with{@link android.app.Activity#setTitle(CharSequence)}. * + * <p> + * <b>Note:</b> Use + * {@link androidx.core.view.ViewCompat#setAccessibilityPaneTitle(View, CharSequence)} + * for backwards-compatibility. </aside> * @param accessibilityPaneTitle The pane's title. Setting to {@code null} indicates that this * View is not a pane. * @@ -9329,8 +9333,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final AccessibilityNodeInfo info = createAccessibilityNodeInfo(); structure.setChildCount(1); final ViewStructure root = structure.newChild(0); - populateVirtualStructure(root, provider, info, forAutofill); - info.recycle(); + if (info != null) { + populateVirtualStructure(root, provider, info, forAutofill); + info.recycle(); + } else { + Log.w(AUTOFILL_LOG_TAG, "AccessibilityNodeInfo is null."); + } } } @@ -13717,6 +13725,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Returns whether the view should be treated as a focusable unit by screen reader * accessibility tools. + * <p> + * <b>Note:</b> Use + * {@link androidx.core.view.ViewCompat#setScreenReaderFocusable(View, boolean)} + * for backwards-compatibility. </aside> * @see #setScreenReaderFocusable(boolean) * * @return Whether the view should be treated as a focusable unit by screen reader. @@ -13762,6 +13774,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Users of some accessibility services can choose to navigate between headings * instead of between paragraphs, words, etc. Apps that provide headings on * sections of text can help the text navigation experience. + * <p> + * <b>Note:</b> Use {@link androidx.core.view.ViewCompat#setAccessibilityHeading(View, boolean)} + * for backwards-compatibility. </aside> * * @param isHeading {@code true} if the view is a heading, {@code false} otherwise. * @@ -14622,6 +14637,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p> * If the view's changes should interrupt ongoing speech and notify the user * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. + * <p> + * <b>Note:</b> Use {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)} + * for backwards-compatibility. </aside> * * @param mode The live region mode for this view, one of: * <ul> @@ -31419,6 +31437,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Starting in {@link android.os.Build.VERSION_CODES#M API 23}, delegate * methods are called <i>after</i> host methods, which all properties to be * modified without being overwritten by the host class. + * <aside class="note"> + * <b>Note:</b> Use a {@link androidx.core.view.AccessibilityDelegateCompat} + * wrapper instead of this class for backwards-compatibility. + * </aside> + * */ public static class AccessibilityDelegate { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 20b1fed02502..d110ecb15c44 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1899,9 +1899,10 @@ public final class ViewRootImpl implements ViewParent, && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); final boolean displayChanged = mDisplay.getDisplayId() != displayId; final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale; + final boolean dragResizingChanged = mPendingDragResizing != dragResizing; if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged && !displayChanged && !forceNextWindowRelayout - && !compatScaleChanged) { + && !compatScaleChanged && !dragResizingChanged) { return; } @@ -2425,7 +2426,7 @@ public final class ViewRootImpl implements ViewParent, * * @hide */ - void notifyRendererOfExpensiveFrame() { + public void notifyRendererOfExpensiveFrame() { if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.notifyExpensiveFrame(); } @@ -7016,6 +7017,10 @@ public final class ViewRootImpl implements ViewParent, private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; boolean handled = mHandwritingInitiator.onTouchEvent(event); + if (handled) { + // If handwriting is started, toolkit doesn't receive ACTION_UP. + mLastClickToolType = event.getToolType(event.getActionIndex()); + } mAttachInfo.mUnbufferedDispatchRequested = false; mAttachInfo.mHandlingPointerEvent = true; diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 7596459b130b..2f04b0c695da 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -658,6 +658,12 @@ public abstract class Window { void updateStatusBarColor(int color); /** + * Update the status bar appearance. + */ + + void updateStatusBarAppearance(int appearance); + + /** * Update the navigation bar color to a forced one. */ void updateNavigationBarColor(int color); @@ -1039,6 +1045,9 @@ public abstract class Window { if (mDecorCallback != null) { mDecorCallback.onSystemBarAppearanceChanged(appearance); } + if (mWindowControllerCallback != null) { + mWindowControllerCallback.updateStatusBarAppearance(appearance); + } } /** @hide */ diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d40c032eb21c..d702367965a1 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -435,11 +435,15 @@ public interface WindowManager extends ViewManager { int TRANSIT_KEYGUARD_GOING_AWAY = 7; /** * A window is appearing above a locked keyguard. + * @deprecated use {@link #TRANSIT_TO_FRONT} + {@link #TRANSIT_FLAG_KEYGUARD_OCCLUDING} for + * keyguard occluding with Shell transition. * @hide */ int TRANSIT_KEYGUARD_OCCLUDE = 8; /** * A window is made invisible revealing a locked keyguard. + * @deprecated use {@link #TRANSIT_TO_BACK} + {@link #TRANSIT_FLAG_KEYGUARD_UNOCCLUDING} for + * keyguard occluding with Shell transition. * @hide */ int TRANSIT_KEYGUARD_UNOCCLUDE = 9; @@ -562,6 +566,25 @@ public interface WindowManager extends ViewManager { int TRANSIT_FLAG_INVISIBLE = (1 << 10); // 0x400 /** + * Transition flag: Indicates that keyguard will be showing (locked) with this transition, + * which is the opposite of {@link #TRANSIT_FLAG_KEYGUARD_GOING_AWAY}. + * @hide + */ + int TRANSIT_FLAG_KEYGUARD_APPEARING = (1 << 11); // 0x800 + + /** + * Transition flag: Indicates that keyguard is becoming hidden by an app + * @hide + */ + int TRANSIT_FLAG_KEYGUARD_OCCLUDING = (1 << 12); // 0x1000 + + /** + * Transition flag: Indicates that keyguard is being revealed after an app was occluding it. + * @hide + */ + int TRANSIT_FLAG_KEYGUARD_UNOCCLUDING = (1 << 13); // 0x2000 + + /** * @hide */ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = { @@ -576,11 +599,28 @@ public interface WindowManager extends ViewManager { TRANSIT_FLAG_KEYGUARD_GOING_AWAY, TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_LAUNCHER_CLEAR_SNAPSHOT, TRANSIT_FLAG_INVISIBLE, + TRANSIT_FLAG_KEYGUARD_APPEARING, + TRANSIT_FLAG_KEYGUARD_OCCLUDING, + TRANSIT_FLAG_KEYGUARD_UNOCCLUDING }) @Retention(RetentionPolicy.SOURCE) @interface TransitionFlags {} /** + * Transit flags used to signal keyguard visibility is changing for animations. + * + * <p>These roughly correspond to CLOSE, OPEN, TO_BACK, and TO_FRONT on a hypothetical Keyguard + * container. Since Keyguard isn't a container we can't include it in changes and need to send + * this information in its own channel. + * @hide + */ + int KEYGUARD_VISIBILITY_TRANSIT_FLAGS = + (TRANSIT_FLAG_KEYGUARD_GOING_AWAY + | TRANSIT_FLAG_KEYGUARD_APPEARING + | TRANSIT_FLAG_KEYGUARD_OCCLUDING + | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING); + + /** * Remove content mode: Indicates remove content mode is currently not defined. * @hide */ @@ -1344,15 +1384,28 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"; /** - * Request for keyboard shortcuts to be retrieved asynchronously. + * Request for app's keyboard shortcuts to be retrieved asynchronously. * * @param receiver The callback to be triggered when the result is ready. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. * * @hide */ public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId); /** + * Request for ime's keyboard shortcuts to be retrieved asynchronously. + * + * @param receiver The callback to be triggered when the result is ready. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * + * @hide + */ + default void requestImeKeyboardShortcuts(KeyboardShortcutsReceiver receiver, int deviceId) {}; + + /** * Return the touch region for the current IME window, or an empty region if there is none. * * @return The region of the IME that is accepting touch inputs, or null if there is no IME, no @@ -3763,6 +3816,7 @@ public interface WindowManager extends ViewManager { * This value is ignored if {@link #preferredDisplayModeId} is set. * @hide */ + @TestApi public float preferredMinDisplayRefreshRate; /** @@ -3771,6 +3825,7 @@ public interface WindowManager extends ViewManager { * This value is ignored if {@link #preferredDisplayModeId} is set. * @hide */ + @TestApi public float preferredMaxDisplayRefreshRate; /** Indicates whether this window wants the HDR conversion is disabled. */ diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index df3e0bb74292..b57163c4e435 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -215,14 +215,36 @@ public final class WindowManagerImpl implements WindowManager { @Override public void send(int resultCode, Bundle resultData) throws RemoteException { List<KeyboardShortcutGroup> result = - resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, android.view.KeyboardShortcutGroup.class); + resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, + android.view.KeyboardShortcutGroup.class); receiver.onKeyboardShortcutsReceived(result); } }; try { WindowManagerGlobal.getWindowManagerService() - .requestAppKeyboardShortcuts(resultReceiver, deviceId); + .requestAppKeyboardShortcuts(resultReceiver, deviceId); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public void requestImeKeyboardShortcuts( + final KeyboardShortcutsReceiver receiver, int deviceId) { + IResultReceiver resultReceiver = new IResultReceiver.Stub() { + @Override + public void send(int resultCode, Bundle resultData) throws RemoteException { + List<KeyboardShortcutGroup> result = + resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, + android.view.KeyboardShortcutGroup.class); + receiver.onKeyboardShortcutsReceived(result); + } + }; + try { + WindowManagerGlobal.getWindowManagerService() + .requestImeKeyboardShortcuts(resultReceiver, deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index b31aa288feac..7199f60e93b0 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -100,6 +100,9 @@ import java.util.Objects; * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a> * developer guide.</p> * </div> + * <aside class="note"> + * <b>Note:</b> Use a {@link androidx.core.view.accessibility.AccessibilityNodeInfoCompat} + * wrapper instead of this class for backwards-compatibility. </aside> * * @see android.accessibilityservice.AccessibilityService * @see AccessibilityEvent @@ -1571,6 +1574,10 @@ public class AccessibilityNodeInfo implements Parcelable { * describes the action. * </p> * <p> + * Use {@link androidx.core.view.ViewCompat#addAccessibilityAction(View, + * AccessibilityNodeInfoCompat.AccessibilityActionCompat)} to register an action directly on the + * view. + * <p> * <strong>Note:</strong> Cannot be called from an * {@link android.accessibilityservice.AccessibilityService}. * This class is made immutable before being delivered to an AccessibilityService. @@ -5149,12 +5156,17 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} and are performed * within {@link View#performAccessibilityAction(int, Bundle)}. * </p> - * <p class="note"> - * <strong>Note:</strong> Views which support these actions should invoke + * <aside class="note"> + * <b>Note:</b> Views which support these actions should invoke * {@link View#setImportantForAccessibility(int)} with * {@link View#IMPORTANT_FOR_ACCESSIBILITY_YES} to ensure an {@link AccessibilityService} * can discover the set of supported actions. * </p> + * <aside class="note"> + * <b>Note:</b> Use {@link androidx.core.view.ViewCompat#addAccessibilityAction(View, + * AccessibilityNodeInfoCompat.AccessibilityActionCompat)} to register an action directly on the + * view. + * </p> */ public static final class AccessibilityAction implements Parcelable { diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 1960d6546c9a..106a77b80627 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -292,6 +292,9 @@ public class AutofillFeatureFlags { private static final String DEFAULT_AFAA_NON_AUTOFILLABLE_IME_ACTIONS = "3,4"; private static final boolean DEFAULT_AFAA_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES = true; private static final boolean DEFAULT_AFAA_SHOULD_ENABLE_MULTILINE_FILTER = true; + private static final boolean + DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = true; + // END AUTOFILL FOR ALL APPS DEFAULTS private AutofillFeatureFlags() {}; @@ -447,7 +450,8 @@ public class AutofillFeatureFlags { public static boolean shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE, false); + DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE, + DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE); } /** diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index c37e311209df..c9afdc0ad074 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -52,6 +52,7 @@ import android.view.contentcapture.ContentCaptureSession.FlushReason; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.RingBuffer; import com.android.internal.util.SyncResultReceiver; import java.io.PrintWriter; @@ -446,6 +447,9 @@ public final class ContentCaptureManager { @Nullable // set on-demand by addDumpable() private Dumper mDumpable; + // Created here in order to live across activity and session changes + @Nullable private final RingBuffer<ContentCaptureEvent> mContentProtectionEventBuffer; + /** @hide */ public interface ContentCaptureClient { /** @@ -456,12 +460,15 @@ public final class ContentCaptureManager { } /** @hide */ - static class StrippedContext { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public static class StrippedContext { @NonNull final String mPackageName; @NonNull final String mContext; final @UserIdInt int mUserId; - private StrippedContext(@NonNull Context context) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public StrippedContext(@NonNull Context context) { mPackageName = context.getPackageName(); mContext = context.toString(); mUserId = context.getUserId(); @@ -502,6 +509,16 @@ public final class ContentCaptureManager { mHandler = Handler.createAsync(Looper.getMainLooper()); mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager(); + + if (mOptions.contentProtectionOptions.enableReceiver + && mOptions.contentProtectionOptions.bufferSize > 0) { + mContentProtectionEventBuffer = + new RingBuffer( + ContentCaptureEvent.class, + mOptions.contentProtectionOptions.bufferSize); + } else { + mContentProtectionEventBuffer = null; + } } /** @@ -870,6 +887,13 @@ public final class ContentCaptureManager { activity.addDumpable(mDumpable); } + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + @Nullable + public RingBuffer<ContentCaptureEvent> getContentProtectionEventBuffer() { + return mContentProtectionEventBuffer; + } + // NOTE: ContentCaptureManager cannot implement it directly as it would be exposed as public API private final class Dumper implements Dumpable { @Override diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl index a64111069c9b..14879977d2a5 100644 --- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl +++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl @@ -17,6 +17,7 @@ package android.view.contentcapture; import android.content.ComponentName; +import android.content.pm.ParceledListSlice; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.DataRemovalRequest; @@ -108,4 +109,9 @@ oneway interface IContentCaptureManager { */ void registerContentCaptureOptionsCallback(String packageName, in IContentCaptureOptionsCallback callback); + + /** + * Notifies the system server that a login was detected. + */ + void onLoginDetected(in ParceledListSlice<ContentCaptureEvent> events); } diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java new file mode 100644 index 000000000000..e49df21692c5 --- /dev/null +++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java @@ -0,0 +1,228 @@ +/* + * 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 android.view.contentprotection; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.text.InputType; +import android.util.Log; +import android.view.contentcapture.ContentCaptureEvent; +import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.ViewNode; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.RingBuffer; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow. + * + * @hide + */ +public class ContentProtectionEventProcessor { + + private static final String TAG = "ContentProtectionEventProcessor"; + + private static final List<Integer> PASSWORD_FIELD_INPUT_TYPES = + Collections.unmodifiableList( + Arrays.asList( + InputType.TYPE_NUMBER_VARIATION_PASSWORD, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, + InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)); + + private static final List<String> PASSWORD_TEXTS = + Collections.unmodifiableList( + Arrays.asList("password", "pass word", "code", "pin", "credential")); + + private static final List<String> ADDITIONAL_SUSPICIOUS_TEXTS = + Collections.unmodifiableList( + Arrays.asList("user", "mail", "phone", "number", "login", "log in", "sign in")); + + private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3); + + private static final String ANDROID_CLASS_NAME_PREFIX = "android."; + + private static final Set<Integer> EVENT_TYPES_TO_STORE = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + ContentCaptureEvent.TYPE_VIEW_APPEARED, + ContentCaptureEvent.TYPE_VIEW_DISAPPEARED, + ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED))); + + @NonNull private final RingBuffer<ContentCaptureEvent> mEventBuffer; + + @NonNull private final Handler mHandler; + + @NonNull private final IContentCaptureManager mContentCaptureManager; + + @NonNull private final String mPackageName; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public boolean mPasswordFieldDetected = false; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public boolean mSuspiciousTextDetected = false; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @Nullable + public Instant mLastFlushTime; + + public ContentProtectionEventProcessor( + @NonNull RingBuffer<ContentCaptureEvent> eventBuffer, + @NonNull Handler handler, + @NonNull IContentCaptureManager contentCaptureManager, + @NonNull String packageName) { + mEventBuffer = eventBuffer; + mHandler = handler; + mContentCaptureManager = contentCaptureManager; + mPackageName = packageName; + } + + /** Main entry point for {@link ContentCaptureEvent} processing. */ + @UiThread + public void processEvent(@NonNull ContentCaptureEvent event) { + if (EVENT_TYPES_TO_STORE.contains(event.getType())) { + storeEvent(event); + } + if (event.getType() == ContentCaptureEvent.TYPE_VIEW_APPEARED) { + processViewAppearedEvent(event); + } + } + + @UiThread + private void storeEvent(@NonNull ContentCaptureEvent event) { + // Ensure receiver gets the package name which might not be set + ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode(); + viewNode.setTextIdEntry(mPackageName); + event.setViewNode(viewNode); + mEventBuffer.append(event); + } + + @UiThread + private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { + mPasswordFieldDetected |= isPasswordField(event); + mSuspiciousTextDetected |= isSuspiciousText(event); + if (mPasswordFieldDetected && mSuspiciousTextDetected) { + loginDetected(); + } + } + + @UiThread + private void loginDetected() { + if (mLastFlushTime == null + || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { + flush(); + } + mPasswordFieldDetected = false; + mSuspiciousTextDetected = false; + } + + @UiThread + private void flush() { + mLastFlushTime = Instant.now(); + + // Note the thread annotations, do not move clearEvents to mHandler + ParceledListSlice<ContentCaptureEvent> events = clearEvents(); + mHandler.post(() -> handlerOnLoginDetected(events)); + } + + @UiThread + @NonNull + private ParceledListSlice<ContentCaptureEvent> clearEvents() { + List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); + mEventBuffer.clear(); + return new ParceledListSlice<>(events); + } + + private void handlerOnLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { + try { + mContentCaptureManager.onLoginDetected(events); + } catch (Exception ex) { + Log.e(TAG, "Failed to flush events for: " + mPackageName, ex); + } + } + + private boolean isPasswordField(@NonNull ContentCaptureEvent event) { + return isPasswordField(event.getViewNode()); + } + + private boolean isPasswordField(@Nullable ViewNode viewNode) { + if (viewNode == null) { + return false; + } + return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode); + } + + private boolean isAndroidPasswordField(@NonNull ViewNode viewNode) { + if (!isAndroidViewNode(viewNode)) { + return false; + } + int inputType = viewNode.getInputType(); + return PASSWORD_FIELD_INPUT_TYPES.stream() + .anyMatch(passwordInputType -> (inputType & passwordInputType) != 0); + } + + private boolean isWebViewPasswordField(@NonNull ViewNode viewNode) { + if (viewNode.getClassName() != null) { + return false; + } + return isPasswordText(ContentProtectionUtils.getViewNodeText(viewNode)); + } + + private boolean isAndroidViewNode(@NonNull ViewNode viewNode) { + String className = viewNode.getClassName(); + return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX); + } + + private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) { + return isSuspiciousText(ContentProtectionUtils.getEventText(event)) + || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event)); + } + + private boolean isSuspiciousText(@Nullable String text) { + if (text == null) { + return false; + } + if (isPasswordText(text)) { + return true; + } + String lowerCaseText = text.toLowerCase(); + return ADDITIONAL_SUSPICIOUS_TEXTS.stream() + .anyMatch(suspiciousText -> lowerCaseText.contains(suspiciousText)); + } + + private boolean isPasswordText(@Nullable String text) { + if (text == null) { + return false; + } + String lowerCaseText = text.toLowerCase(); + return PASSWORD_TEXTS.stream() + .anyMatch(passwordText -> lowerCaseText.contains(passwordText)); + } +} diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index 8b04d35734ec..63bf5622fbb8 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -21,6 +21,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; @@ -301,7 +302,8 @@ public final class TextClassification implements Parcelable { Objects.requireNonNull(intent); return v -> { try { - intent.send(); + intent.send(ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle()); } catch (PendingIntent.CanceledException e) { Log.e(LOG_TAG, "Error sending PendingIntent", e); } diff --git a/core/java/android/webkit/OWNERS b/core/java/android/webkit/OWNERS index b33da57c42e3..e7fd7a5d1096 100644 --- a/core/java/android/webkit/OWNERS +++ b/core/java/android/webkit/OWNERS @@ -1,4 +1,3 @@ -changwan@google.com +# Bug component: 76427 ntfschr@google.com -tobiasjs@google.com torne@google.com diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 31cfed5376a6..8af3ac66f1a7 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -232,6 +232,7 @@ public class RemoteViews implements Parcelable, Filter { private static final int NIGHT_MODE_REFLECTION_ACTION_TAG = 30; private static final int SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG = 31; private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32; + private static final int SET_REMOTE_ADAPTER_TAG = 33; /** @hide **/ @IntDef(prefix = "MARGIN_", value = { @@ -960,6 +961,11 @@ public class RemoteViews implements Parcelable, Filter { return SET_REMOTE_VIEW_ADAPTER_LIST_TAG; } + @Override + public String getUniqueKey() { + return (SET_REMOTE_ADAPTER_TAG + "_" + viewId); + } + int viewTypeCount; ArrayList<RemoteViews> list; } @@ -1082,6 +1088,11 @@ public class RemoteViews implements Parcelable, Filter { public int getActionTag() { return SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG; } + + @Override + public String getUniqueKey() { + return (SET_REMOTE_ADAPTER_TAG + "_" + viewId); + } } private class SetRemoteViewsAdapterIntent extends Action { diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index ee6ac1297b63..65984f55ded2 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -155,8 +155,7 @@ public class Toast { * Construct an empty Toast object. You must call {@link #setView} before you * can call {@link #show}. * - * @param context The context to use. Usually your {@link android.app.Application} - * or {@link android.app.Activity} object. + * @param context The context to use. Usually your {@link android.app.Activity} object. */ public Toast(Context context) { this(context, null); @@ -482,8 +481,7 @@ public class Toast { /** * Make a standard toast that just contains text. * - * @param context The context to use. Usually your {@link android.app.Application} - * or {@link android.app.Activity} object. + * @param context The context to use. Usually your {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} @@ -539,8 +537,7 @@ public class Toast { /** * Make a standard toast that just contains text from a resource. * - * @param context The context to use. Usually your {@link android.app.Application} - * or {@link android.app.Activity} object. + * @param context The context to use. Usually your {@link android.app.Activity} object. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 8c05130bf5fe..59238b40e0c8 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -99,9 +99,6 @@ public final class TransitionInfo implements Parcelable { /** The container is the display. */ public static final int FLAG_IS_DISPLAY = 1 << 5; - /** The container can show on top of lock screen. */ - public static final int FLAG_OCCLUDES_KEYGUARD = 1 << 6; - /** * Only for IS_DISPLAY containers. Is set if the display has system alert windows. This is * used to prevent seamless rotation. @@ -175,7 +172,6 @@ public final class TransitionInfo implements Parcelable { FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT, FLAG_IS_VOICE_INTERACTION, FLAG_IS_DISPLAY, - FLAG_OCCLUDES_KEYGUARD, FLAG_DISPLAY_HAS_ALERT_WINDOWS, FLAG_IS_INPUT_METHOD, FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, @@ -424,8 +420,8 @@ public final class TransitionInfo implements Parcelable { case TRANSIT_NONE: return "NONE"; case TRANSIT_OPEN: return "OPEN"; case TRANSIT_CLOSE: return "CLOSE"; - case TRANSIT_TO_FRONT: return "SHOW"; - case TRANSIT_TO_BACK: return "HIDE"; + case TRANSIT_TO_FRONT: return "TO_FRONT"; + case TRANSIT_TO_BACK: return "TO_BACK"; case TRANSIT_CHANGE: return "CHANGE"; default: return "<unknown:" + mode + ">"; } @@ -457,9 +453,6 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_IS_DISPLAY) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("IS_DISPLAY"); } - if ((flags & FLAG_OCCLUDES_KEYGUARD) != 0) { - sb.append(sb.length() == 0 ? "" : "|").append("OCCLUDES_KEYGUARD"); - } if ((flags & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("DISPLAY_HAS_ALERT_WINDOWS"); } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 421d9983d6b7..632208cdb97c 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -163,19 +163,25 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { // Re-populate the top callback to WM if the removed callback was previously the top one. if (previousTopCallback == callback) { // We should call onBackCancelled() when an active callback is removed from dispatcher. - if (mProgressAnimator.isBackAnimationInProgress() - && callback instanceof OnBackAnimationCallback) { - // The ProgressAnimator will handle the new topCallback, so we don't want to call - // onBackCancelled() on it. We call immediately the callback instead. - OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; - animatedCallback.onBackCancelled(); - Log.d(TAG, "The callback was removed while a back animation was in progress, " - + "an onBackCancelled() was dispatched."); - } + sendCancelledIfInProgress(callback); setTopOnBackInvokedCallback(getTopCallback()); } } + private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { + boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); + if (isInProgress && callback instanceof OnBackAnimationCallback) { + OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; + animatedCallback.onBackCancelled(); + if (DEBUG) { + Log.d(TAG, "sendCancelIfRunning: callback canceled"); + } + } else { + Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress + + "callback=" + callback); + } + } + @Override public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); @@ -188,9 +194,20 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { mImeDispatcher = null; } if (!mAllCallbacks.isEmpty()) { + OnBackInvokedCallback topCallback = getTopCallback(); + if (topCallback != null) { + sendCancelledIfInProgress(topCallback); + } else { + // Should not be possible + Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); + } // Clear binder references in WM. setTopOnBackInvokedCallback(null); } + + // We should also stop running animations since all callbacks have been removed. + // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. + Handler.getMain().post(mProgressAnimator::reset); mAllCallbacks.clear(); mOnBackInvokedCallbacks.clear(); } @@ -342,12 +359,17 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { + boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallbackRef.get(); if (callback == null) { Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference."); return; } + if (callback instanceof OnBackAnimationCallback && !isInProgress) { + Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked()."); + return; + } callback.onBackInvoked(); }); } diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index 3d95dd341cc0..c9e76009136a 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -81,6 +81,10 @@ public class SystemUiSystemPropertiesFlags { /** Gating the logging of DND state change events. */ public static final Flag LOG_DND_STATE_EVENTS = releasedFlag("persist.sysui.notification.log_dnd_state_events"); + + /** Gating the holding of WakeLocks until NLSes are told about a new notification. */ + public static final Flag WAKE_LOCK_FOR_POSTING_NOTIFICATION = + devFlag("persist.sysui.notification.wake_lock_for_posting_notification"); } //// == End of flags. Everything below this line is the implementation. == //// diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 0f41229dd0c1..5c1d91ff540e 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -278,7 +278,7 @@ int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) if (events & ALOOPER_EVENT_INPUT) { JNIEnv* env = AndroidRuntime::getJNIEnv(); - status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr); + status_t status = consumeEvents(env, /*consumeBatches=*/false, -1, nullptr); mMessageQueue->raiseAndClearException(env, "handleReceiveCallback"); return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK; } @@ -398,7 +398,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onFocusEvent, jboolean(focusEvent->getHasFocus())); - finishInputEvent(seq, true /* handled */); + finishInputEvent(seq, /*handled=*/true); continue; } case InputEventType::CAPTURE: { @@ -411,7 +411,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onPointerCaptureEvent, jboolean(captureEvent->getPointerCaptureEnabled())); - finishInputEvent(seq, true /* handled */); + finishInputEvent(seq, /*handled=*/true); continue; } case InputEventType::DRAG: { @@ -423,7 +423,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent, jboolean(dragEvent->isExiting()), dragEvent->getX(), dragEvent->getY()); - finishInputEvent(seq, true /* handled */); + finishInputEvent(seq, /*handled=*/true); continue; } case InputEventType::TOUCH_MODE: { @@ -436,7 +436,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onTouchModeChanged, jboolean(touchModeEvent->isInTouchMode())); - finishInputEvent(seq, true /* handled */); + finishInputEvent(seq, /*handled=*/true); continue; } @@ -571,8 +571,8 @@ static jboolean nativeConsumeBatchedInputEvents(JNIEnv* env, jclass clazz, jlong sp<NativeInputEventReceiver> receiver = reinterpret_cast<NativeInputEventReceiver*>(receiverPtr); bool consumedBatch; - status_t status = receiver->consumeEvents(env, true /*consumeBatches*/, frameTimeNanos, - &consumedBatch); + status_t status = + receiver->consumeEvents(env, /*consumeBatches=*/true, frameTimeNanos, &consumedBatch); if (status && status != DEAD_OBJECT && !env->ExceptionCheck()) { std::string message = android::base::StringPrintf("Failed to consume batched input event. status=%s(%d)", diff --git a/core/jni/android_view_InputQueue.cpp b/core/jni/android_view_InputQueue.cpp index 21db37e1ef7c..a0d081d2cd26 100644 --- a/core/jni/android_view_InputQueue.cpp +++ b/core/jni/android_view_InputQueue.cpp @@ -239,7 +239,7 @@ static jlong nativeSendMotionEvent(JNIEnv* env, jobject clazz, jlong ptr, jobjec return -1; } MotionEvent* event = queue->createMotionEvent(); - event->copyFrom(originalEvent, true /* keepHistory */); + event->copyFrom(originalEvent, /*keepHistory=*/true); queue->enqueueEvent(event); return reinterpret_cast<jlong>(event); } diff --git a/core/jni/android_view_KeyCharacterMap.cpp b/core/jni/android_view_KeyCharacterMap.cpp index 8fa03cfc5b6f..ddaeb5a4d272 100644 --- a/core/jni/android_view_KeyCharacterMap.cpp +++ b/core/jni/android_view_KeyCharacterMap.cpp @@ -76,7 +76,7 @@ jobject android_view_KeyCharacterMap_create(JNIEnv* env, int32_t deviceId, reinterpret_cast<jlong>(nativeMap)); } -static jobject nativeObtainEmptyKeyCharacterMap(JNIEnv* env, jobject /* clazz */, jint deviceId) { +static jobject nativeObtainEmptyKeyCharacterMap(JNIEnv* env, /*clazz=*/jobject, jint deviceId) { return android_view_KeyCharacterMap_create(env, deviceId, nullptr); } @@ -202,7 +202,7 @@ static jobjectArray nativeGetEvents(JNIEnv *env, jobject clazz, jlong ptr, jcharArray charsArray) { NativeKeyCharacterMap* map = reinterpret_cast<NativeKeyCharacterMap*>(ptr); if (!map || !map->getMap()) { - return env->NewObjectArray(0 /* size */, gKeyEventClassInfo.clazz, NULL); + return env->NewObjectArray(/*size=*/0, gKeyEventClassInfo.clazz, NULL); } jchar* chars = env->GetCharArrayElements(charsArray, NULL); if (!chars) { diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp index 791d46332868..9017d58913b3 100644 --- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp +++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp @@ -149,7 +149,12 @@ copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntr return INSTALL_FAILED_INVALID_APK; } - if (!extractNativeLibs && (!debuggable || strcmp(fileName, "wrap.sh") != 0)) { + // Always extract wrap.sh for debuggable, even if extractNativeLibs=false. This makes it + // easier to use wrap.sh because it only works when it is extracted, see + // frameworks/base/services/core/java/com/android/server/am/ProcessList.java. + bool forceExtractCurrentFile = debuggable && strcmp(fileName, "wrap.sh") == 0; + + if (!extractNativeLibs && !forceExtractCurrentFile) { // check if library is uncompressed and page-aligned if (method != ZipFileRO::kCompressStored) { ALOGE("Library '%s' is compressed - will not be able to open it directly from apk.\n", diff --git a/core/res/res/layout/alert_dialog_button_bar_leanback.xml b/core/res/res/layout/alert_dialog_button_bar_leanback.xml index ea94af662dcf..466811f6d116 100644 --- a/core/res/res/layout/alert_dialog_button_bar_leanback.xml +++ b/core/res/res/layout/alert_dialog_button_bar_leanback.xml @@ -21,13 +21,13 @@ android:layout_height="wrap_content" android:scrollbarAlwaysDrawVerticalTrack="true" android:scrollIndicators="top|bottom" - android:fillViewport="true" - style="?attr/buttonBarStyle"> + android:fillViewport="true"> <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layoutDirection="locale" android:orientation="horizontal" + style="?attr/buttonBarStyle" android:gravity="start"> <Button diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 6d8d368ab352..06ba4da2360f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1679,16 +1679,56 @@ in the config_autoBrightnessLevels array. This array should have size one greater than the size of the config_autoBrightnessLevels array. The brightness values must be between 0 and 255 and be non-decreasing. + This must be overridden in platform specific overlays --> <integer-array name="config_autoBrightnessButtonBacklightValues"> </integer-array> + <!-- Smoothing constant for Ambient keyboard backlight change. It should contain value + in the range (0.0, 1.0] that will be used to calculate smoothed lux values using + simple exponential smoothing. This value indicated how quickly we transition to + the lux values provided by the Ambient light sensor. + Simple formula for newLuxValue = (1-constant)*currLuxValue + constant*rawLuxValue, where + rawLuxValue is the value provided by the Ambient light sensor. (e.g. value of 1.0 means we + immediately start using the value provided by the Ambient light sensor) + This must be overridden in platform specific overlays --> + <item name="config_autoKeyboardBrightnessSmoothingConstant" format="float" type="dimen"> + 1.0 + </item> + <!-- Array of output values for keyboard backlight corresponding to the lux values - in the config_autoBrightnessLevels array. This array should have size one greater - than the size of the config_autoBrightnessLevels array. + in the config_autoKeyboardBacklight(Increase/Decrease)LuxThreshold arrays. The brightness values must be between 0 and 255 and be non-decreasing. - This must be overridden in platform specific overlays --> - <integer-array name="config_autoBrightnessKeyboardBacklightValues"> + + This can be overridden in platform specific overlays --> + <integer-array name="config_autoKeyboardBacklightBrightnessValues"> + <item>102</item> + <item>153</item> + <item>0</item> + </integer-array> + + <!-- Array of threshold values for keyboard backlight corresponding to the values + in the config_autoKeyboardBacklightBrightnessValues array. + These lux values indicate when to move to a lower keyboard backlight value, + as defined in config_autoKeyboardBacklightBrightnessValues array. + + This can be overridden in platform specific overlays --> + <integer-array name="config_autoKeyboardBacklightDecreaseLuxThreshold"> + <item>-1</item> + <item>40</item> + <item>150</item> + </integer-array> + + <!-- Array of threshold values for keyboard backlight corresponding to the values + in the config_autoKeyboardBacklightBrightnessValues array. + These lux values indicate when to move to a higher keyboard backlight value, + as defined in config_autoKeyboardBacklightBrightnessValues array. + + This can be overridden in platform specific overlays --> + <integer-array name="config_autoKeyboardBacklightIncreaseLuxThreshold"> + <item>55</item> + <item>200</item> + <item>-1</item> </integer-array> <!-- An array describing the screen's backlight values corresponding to the brightness @@ -2766,6 +2806,11 @@ measured in dips per second. Setting this to -1dp disables rotary encoder fling. --> <dimen name="config_viewMaxRotaryEncoderFlingVelocity">-1dp</dimen> + <!-- Tick intervals in dp for rotary encoder scrolls on {@link MotionEvent#AXIS_SCROLL} + generated by {@link InputDevice#SOURCE_ROTARY_ENCODER} devices. A valid tick interval value + is a positive value. Setting this to 0dp disables scroll tick. --> + <dimen name="config_rotaryEncoderAxisScrollTickInterval">21dp</dimen> + <!-- Amount of time in ms the user needs to press the relevant key to bring up the global actions dialog --> <integer name="config_globalActionsKeyTimeout">500</integer> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b7e85f6526ee..16836fb0f634 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -522,6 +522,7 @@ <java-symbol type="dimen" name="config_viewConfigurationHandwritingSlop" /> <java-symbol type="dimen" name="config_viewConfigurationHoverSlop" /> <java-symbol type="dimen" name="config_ambiguousGestureMultiplier" /> + <java-symbol type="dimen" name="config_autoKeyboardBrightnessSmoothingConstant" /> <java-symbol type="dimen" name="config_viewMinFlingVelocity" /> <java-symbol type="dimen" name="config_viewMaxFlingVelocity" /> <java-symbol type="dimen" name="config_viewMinRotaryEncoderFlingVelocity" /> @@ -1893,11 +1894,13 @@ <java-symbol type="anim" name="dream_activity_open_enter" /> <java-symbol type="anim" name="dream_activity_close_exit" /> <java-symbol type="array" name="config_autoBrightnessButtonBacklightValues" /> - <java-symbol type="array" name="config_autoBrightnessKeyboardBacklightValues" /> <java-symbol type="array" name="config_autoBrightnessLcdBacklightValues" /> <java-symbol type="array" name="config_autoBrightnessLcdBacklightValues_doze" /> <java-symbol type="array" name="config_autoBrightnessLevels" /> <java-symbol type="array" name="config_autoBrightnessLevelsIdle" /> + <java-symbol type="array" name="config_autoKeyboardBacklightBrightnessValues" /> + <java-symbol type="array" name="config_autoKeyboardBacklightDecreaseLuxThreshold" /> + <java-symbol type="array" name="config_autoKeyboardBacklightIncreaseLuxThreshold" /> <java-symbol type="array" name="config_ambientThresholdLevels" /> <java-symbol type="array" name="config_ambientBrighteningThresholds" /> <java-symbol type="array" name="config_ambientDarkeningThresholds" /> @@ -2032,6 +2035,7 @@ <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" /> <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" /> <java-symbol type="integer" name="config_notificationServiceArchiveSize" /> + <java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" /> <java-symbol type="integer" name="config_previousVibrationsDumpLimit" /> <java-symbol type="integer" name="config_defaultVibrationAmplitude" /> <java-symbol type="dimen" name="config_hapticChannelMaxVibrationAmplitude" /> diff --git a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java index a7538701807a..0525443ecf82 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import android.util.Property; import android.view.View; import androidx.test.annotation.UiThreadTest; @@ -576,6 +577,42 @@ public class AnimatorSetActivityTest { }); } + @Test + public void testInitializeWithoutReadingValues() throws Throwable { + // Some consumers crash while reading values before the animator starts + Property<int[], Integer> property = new Property<>(Integer.class, "firstValue") { + @Override + public Integer get(int[] target) { + throw new IllegalStateException("Shouldn't be called"); + } + + @Override + public void set(int[] target, Integer value) { + target[0] = value; + } + }; + + int[] target1 = new int[1]; + int[] target2 = new int[1]; + int[] target3 = new int[1]; + ObjectAnimator animator1 = ObjectAnimator.ofInt(target1, property, 0, 100); + ObjectAnimator animator2 = ObjectAnimator.ofInt(target2, property, 0, 100); + ObjectAnimator animator3 = ObjectAnimator.ofInt(target3, property, 0, 100); + AnimatorSet set = new AnimatorSet(); + set.playSequentially(animator1, animator2, animator3); + + mActivityRule.runOnUiThread(() -> { + set.setCurrentPlayTime(900); + assertEquals(100, target1[0]); + assertEquals(100, target2[0]); + assertEquals(100, target3[0]); + set.setCurrentPlayTime(0); + assertEquals(0, target1[0]); + assertEquals(0, target2[0]); + assertEquals(0, target3[0]); + }); + } + /** * Check that the animator list contains exactly the given animators and nothing else. */ diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java index 81eb213ce78a..5ac99db3aea5 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java @@ -128,6 +128,7 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor + 0, // statusBarAppearance true, // ensureStatusBarContrastWhenTransparent true, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_RESIZEABLE, // resizeMode @@ -152,6 +153,7 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor + 0, // statusBarAppearance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -167,6 +169,7 @@ public class ActivityManagerTest extends AndroidTestCase { 0x2222222, // colorBackground 0x3333332, // statusBarColor 0x4444442, // navigationBarColor + 0, // statusBarAppearance true, // ensureStatusBarContrastWhenTransparent true, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_RESIZEABLE, // resizeMode @@ -197,6 +200,7 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor + 0, // statusBarAppearance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -219,6 +223,7 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor + 0, // statusBarAppearance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -250,6 +255,7 @@ public class ActivityManagerTest extends AndroidTestCase { assertEquals(td1.getBackgroundColor(), td2.getBackgroundColor()); assertEquals(td1.getStatusBarColor(), td2.getStatusBarColor()); assertEquals(td1.getNavigationBarColor(), td2.getNavigationBarColor()); + assertEquals(td1.getStatusBarAppearance(), td2.getStatusBarAppearance()); assertEquals(td1.getResizeMode(), td2.getResizeMode()); assertEquals(td1.getMinWidth(), td2.getMinWidth()); assertEquals(td1.getMinHeight(), td2.getMinHeight()); diff --git a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java new file mode 100644 index 000000000000..6bdb07d70212 --- /dev/null +++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java @@ -0,0 +1,389 @@ +/* + * 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 android.view; + +import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; +import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; +import static android.view.HapticFeedbackConstants.SCROLL_TICK; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@Presubmit +public final class HapticScrollFeedbackProviderTest { + private static final int INPUT_DEVICE_1 = 1; + private static final int INPUT_DEVICE_2 = 2; + + private static final int TICK_INTERVAL_PIXELS = 100; + + private TestView mView; + private long mCurrentTimeMillis = 1000; // arbitrary starting time value + + private HapticScrollFeedbackProvider mProvider; + + @Before + public void setUp() { + mView = new TestView(InstrumentationRegistry.getContext()); + mProvider = new HapticScrollFeedbackProvider(mView, TICK_INTERVAL_PIXELS); + } + + @Test + public void testSnapToItem() { + mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + } + + @Test + public void testScrollLimit_start() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_stop() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollProgress_zeroTickInterval() { + mProvider = + new HapticScrollFeedbackProvider( + mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 0); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 30); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 30); + + assertNoFeedback(mView); + } + + @Test + public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold() { + mProvider = + new HapticScrollFeedbackProvider( + mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 80); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 80); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 40); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold() { + mProvider = + new HapticScrollFeedbackProvider( + mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -20); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -80); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -70); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -40); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_positiveAndNegativeProgresses() { + mProvider = + new HapticScrollFeedbackProvider( + mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -90); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10); + + assertNoFeedback(mView); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -50); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 40); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 50); + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 60); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2); + } + + @Test + public void testScrollProgress_singleProgressExceedsThreshold() { + mProvider = + new HapticScrollFeedbackProvider( + mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), + MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 1000); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1); + } + + @Test + public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT); + } + + @Test + public void testScrollLimit_enabledWithProgress() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + mProvider.onScrollProgress( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithSnap() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarSnap() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + mProvider.onSnapToItem(createTouchMoveEvent(), MotionEvent.AXIS_X); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarProgress() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + mProvider.onScrollProgress( + createTouchMoveEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); + } + + @Test + public void testScrollLimit_enabledWithDissimilarLimit() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + mProvider.onScrollLimit(createTouchMoveEvent(), MotionEvent.AXIS_SCROLL, false); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3); + } + + @Test + public void testScrollLimit_enabledWithMotionFromDifferentDeviceId() { + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_1), + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_2), + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollLimit( + createRotaryEncoderScrollEvent(INPUT_DEVICE_1), + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3); + } + + private void assertNoFeedback(TestView view) { + for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + assertFeedbackCount(view, feedback, 0); + } + } + + private void assertOnlyFeedback(TestView view, int expectedFeedback) { + assertOnlyFeedback(view, expectedFeedback, /* expectedCount= */ 1); + } + + private void assertOnlyFeedback(TestView view, int expectedFeedback, int expectedCount) { + for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + assertFeedbackCount(view, feedback, (feedback == expectedFeedback) ? expectedCount : 0); + } + } + + private void assertFeedbackCount(TestView view, int feedback, int expectedCount) { + int count = view.mFeedbackCount.getOrDefault(feedback, 0); + assertThat(count).isEqualTo(expectedCount); + } + + private MotionEvent createTouchMoveEvent() { + long downTime = mCurrentTimeMillis; + long eventTime = mCurrentTimeMillis + 2; // arbitrary increment from the down time. + ++mCurrentTimeMillis; + return MotionEvent.obtain( + downTime , eventTime, MotionEvent.ACTION_MOVE, /* x= */ 3, /* y= */ 5, 0); + } + + private MotionEvent createRotaryEncoderScrollEvent() { + return createRotaryEncoderScrollEvent(INPUT_DEVICE_1); + } + + private MotionEvent createRotaryEncoderScrollEvent(int deviceId) { + MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); + props.id = 0; + + MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.setAxisValue(MotionEvent.AXIS_SCROLL, 20); + + return MotionEvent.obtain(0 /* downTime */, + ++mCurrentTimeMillis, + MotionEvent.ACTION_SCROLL, + /* pointerCount= */ 1, + new MotionEvent.PointerProperties[] {props}, + new MotionEvent.PointerCoords[] {coords}, + /* metaState= */ 0, + /* buttonState= */ 0, + /* xPrecision= */ 0, + /* yPrecision= */ 0, + deviceId, + /* edgeFlags= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, + /* flags= */ 0); + } + + private static class TestView extends View { + final Map<Integer, Integer> mFeedbackCount = new HashMap<>(); + + TestView(Context context) { + super(context); + } + + @Override + public boolean performHapticFeedback(int feedback) { + if (!mFeedbackCount.containsKey(feedback)) { + mFeedbackCount.put(feedback, 0); + } + mFeedbackCount.put(feedback, mFeedbackCount.get(feedback) + 1); + return true; + } + } +} diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java index 17ed4c478350..101f7c21fa19 100644 --- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java @@ -15,14 +15,17 @@ */ package android.view.contentcapture; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED; + import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; import static org.testng.Assert.assertThrows; import android.content.ContentCaptureOptions; import android.content.Context; +import com.android.internal.util.RingBuffer; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -37,9 +40,15 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ContentCaptureManagerTest { + private static final int BUFFER_SIZE = 100; + + private static final ContentCaptureOptions EMPTY_OPTIONS = new ContentCaptureOptions(null); + @Mock private Context mMockContext; + @Mock private IContentCaptureManager mMockContentCaptureManager; + @Test public void testConstructor_invalidParametersThrowsException() { assertThrows(NullPointerException.class, @@ -48,11 +57,65 @@ public class ContentCaptureManagerTest { } @Test + public void testConstructor_contentProtection_default_bufferNotCreated() { + ContentCaptureManager manager = + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS); + + assertThat(manager.getContentProtectionEventBuffer()).isNull(); + } + + @Test + public void testConstructor_contentProtection_disabled_bufferNotCreated() { + ContentCaptureOptions options = + createOptions( + new ContentCaptureOptions.ContentProtectionOptions( + /* enableReceiver= */ false, BUFFER_SIZE)); + + ContentCaptureManager manager = + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options); + + assertThat(manager.getContentProtectionEventBuffer()).isNull(); + } + + @Test + public void testConstructor_contentProtection_invalidBufferSize_bufferNotCreated() { + ContentCaptureOptions options = + createOptions( + new ContentCaptureOptions.ContentProtectionOptions( + /* enableReceiver= */ true, /* bufferSize= */ 0)); + + ContentCaptureManager manager = + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options); + + assertThat(manager.getContentProtectionEventBuffer()).isNull(); + } + + @Test + public void testConstructor_contentProtection_enabled_bufferCreated() { + ContentCaptureOptions options = + createOptions( + new ContentCaptureOptions.ContentProtectionOptions( + /* enableReceiver= */ true, BUFFER_SIZE)); + + ContentCaptureManager manager = + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options); + RingBuffer<ContentCaptureEvent> buffer = manager.getContentProtectionEventBuffer(); + + assertThat(buffer).isNotNull(); + ContentCaptureEvent[] expected = new ContentCaptureEvent[BUFFER_SIZE]; + int offset = 3; + for (int i = 0; i < BUFFER_SIZE + offset; i++) { + ContentCaptureEvent event = new ContentCaptureEvent(i, TYPE_SESSION_STARTED); + buffer.append(event); + expected[(i + BUFFER_SIZE - offset) % BUFFER_SIZE] = event; + } + assertThat(buffer.toArray()).isEqualTo(expected); + } + + @Test public void testRemoveData_invalidParametersThrowsException() { - final IContentCaptureManager mockService = mock(IContentCaptureManager.class); - final ContentCaptureOptions options = new ContentCaptureOptions(null); final ContentCaptureManager manager = - new ContentCaptureManager(mMockContext, mockService, options); + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS); assertThrows(NullPointerException.class, () -> manager.removeData(null)); } @@ -60,10 +123,8 @@ public class ContentCaptureManagerTest { @Test @SuppressWarnings("GuardedBy") public void testFlushViewTreeAppearingEventDisabled_setAndGet() { - final IContentCaptureManager mockService = mock(IContentCaptureManager.class); - final ContentCaptureOptions options = new ContentCaptureOptions(null); final ContentCaptureManager manager = - new ContentCaptureManager(mMockContext, mockService, options); + new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS); assertThat(manager.getFlushViewTreeAppearingEventDisabled()).isFalse(); manager.setFlushViewTreeAppearingEventDisabled(true); @@ -71,4 +132,18 @@ public class ContentCaptureManagerTest { manager.setFlushViewTreeAppearingEventDisabled(false); assertThat(manager.getFlushViewTreeAppearingEventDisabled()).isFalse(); } + + private ContentCaptureOptions createOptions( + ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) { + return new ContentCaptureOptions( + /* loggingLevel= */ 0, + /* maxBufferSize= */ 0, + /* idleFlushingFrequencyMs= */ 0, + /* textChangeFlushingFrequencyMs= */ 0, + /* logHistorySize= */ 0, + /* disableFlushForViewTreeAppearing= */ false, + /* enableReceiver= */ true, + contentProtectionOptions, + /* whitelistedComponents= */ null); + } } diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java new file mode 100644 index 000000000000..4adadf11903e --- /dev/null +++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java @@ -0,0 +1,516 @@ +/* + * 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 android.view.contentprotection; + +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.os.Looper; +import android.text.InputType; +import android.view.View; +import android.view.contentcapture.ContentCaptureEvent; +import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.ViewNode; +import android.view.contentcapture.ViewNode.ViewStructureImpl; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.util.RingBuffer; + +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Test for {@link ContentProtectionEventProcessor}. + * + * <p>Run with: {@code atest + * FrameworksCoreTests:android.view.contentprotection.ContentProtectionEventProcessorTest} + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ContentProtectionEventProcessorTest { + + private static final String PACKAGE_NAME = "com.test.package.name"; + + private static final String ANDROID_CLASS_NAME = "android.test.some.class.name"; + + private static final String PASSWORD_TEXT = "ENTER PASSWORD HERE"; + + private static final String SUSPICIOUS_TEXT = "PLEASE SIGN IN"; + + private static final String SAFE_TEXT = "SAFE TEXT"; + + private static final ContentCaptureEvent PROCESS_EVENT = createProcessEvent(); + + private static final ContentCaptureEvent[] BUFFERED_EVENTS = + new ContentCaptureEvent[] {PROCESS_EVENT}; + + private static final Set<Integer> EVENT_TYPES_TO_STORE = + ImmutableSet.of(TYPE_VIEW_APPEARED, TYPE_VIEW_DISAPPEARED, TYPE_VIEW_TEXT_CHANGED); + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock private RingBuffer<ContentCaptureEvent> mMockEventBuffer; + + @Mock private IContentCaptureManager mMockContentCaptureManager; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + + private ContentProtectionEventProcessor mContentProtectionEventProcessor; + + @Before + public void setup() { + mContentProtectionEventProcessor = + new ContentProtectionEventProcessor( + mMockEventBuffer, + new Handler(Looper.getMainLooper()), + mMockContentCaptureManager, + PACKAGE_NAME); + } + + @Test + public void processEvent_buffer_storesOnlySubsetOfEventTypes() { + List<ContentCaptureEvent> expectedEvents = new ArrayList<>(); + for (int type = -100; type <= 100; type++) { + ContentCaptureEvent event = createEvent(type); + if (EVENT_TYPES_TO_STORE.contains(type)) { + expectedEvents.add(event); + } + + mContentProtectionEventProcessor.processEvent(event); + } + + assertThat(expectedEvents).hasSize(EVENT_TYPES_TO_STORE.size()); + expectedEvents.forEach((expectedEvent) -> verify(mMockEventBuffer).append(expectedEvent)); + verifyNoMoreInteractions(mMockEventBuffer); + } + + @Test + public void processEvent_buffer_setsTextIdEntry_withoutExistingViewNode() { + ContentCaptureEvent event = createStoreEvent(); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(event.getViewNode()).isNotNull(); + assertThat(event.getViewNode().getTextIdEntry()).isEqualTo(PACKAGE_NAME); + verify(mMockEventBuffer).append(event); + } + + @Test + public void processEvent_buffer_setsTextIdEntry_withExistingViewNode() { + ViewNode viewNode = new ViewNode(); + viewNode.setTextIdEntry(PACKAGE_NAME + "TO BE OVERWRITTEN"); + ContentCaptureEvent event = createStoreEvent(); + event.setViewNode(viewNode); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(event.getViewNode()).isSameInstanceAs(viewNode); + assertThat(viewNode.getTextIdEntry()).isEqualTo(PACKAGE_NAME); + verify(mMockEventBuffer).append(event); + } + + @Test + public void processEvent_loginDetected_inspectsOnlyTypeViewAppeared() { + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + + for (int type = -100; type <= 100; type++) { + if (type == TYPE_VIEW_APPEARED) { + continue; + } + + mContentProtectionEventProcessor.processEvent(createEvent(type)); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + } + + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void processEvent_loginDetected() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + assertOnLoginDetected(); + } + + @Test + public void processEvent_loginDetected_passwordFieldNotDetected() { + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void processEvent_loginDetected_suspiciousTextNotDetected() { + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void processEvent_loginDetected_withoutViewNode() { + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void processEvent_multipleLoginsDetected_belowFlushThreshold() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + assertOnLoginDetected(); + } + + @Test + public void processEvent_multipleLoginsDetected_aboveFlushThreshold() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + mContentProtectionEventProcessor.mLastFlushTime = Instant.now().minusSeconds(5); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, times(2)).clear(); + verify(mMockEventBuffer, times(2)).toArray(); + assertOnLoginDetected(PROCESS_EVENT, /* times= */ 2); + } + + @Test + public void isPasswordField_android() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_android_withoutClassName() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + /* className= */ null, InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_android_wrongClassName() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + "wrong.prefix" + ANDROID_CLASS_NAME, + InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_android_wrongInputType() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_NORMAL); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_webView() throws Exception { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ null, /* eventText= */ null, PASSWORD_TEXT); + when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[] {event}); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + assertOnLoginDetected(event, /* times= */ 1); + } + + @Test + public void isPasswordField_webView_withClassName() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ "any.class.name", /* eventText= */ null, PASSWORD_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_webView_withSafeViewNodeText() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ null, /* eventText= */ null, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isPasswordField_webView_withEventText() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent(/* className= */ null, PASSWORD_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isSuspiciousText_withSafeText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isSuspiciousText_eventText_suspiciousText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SUSPICIOUS_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isSuspiciousText_viewNodeText_suspiciousText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SUSPICIOUS_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isSuspiciousText_eventText_passwordText() { + ContentCaptureEvent event = createSuspiciousTextEvent(PASSWORD_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + @Test + public void isSuspiciousText_viewNodeText_passwordText() { + // Specify the class to differ from {@link isPasswordField_webView} test in this version + ContentCaptureEvent event = + createProcessEvent( + "test.class.not.a.web.view", /* inputType= */ 0, SAFE_TEXT, PASSWORD_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); + } + + private static ContentCaptureEvent createEvent(int type) { + return new ContentCaptureEvent(/* sessionId= */ 123, type); + } + + private static ContentCaptureEvent createStoreEvent() { + return createEvent(TYPE_VIEW_TEXT_CHANGED); + } + + private static ContentCaptureEvent createProcessEvent() { + return createEvent(TYPE_VIEW_APPEARED); + } + + private ContentCaptureEvent createProcessEvent( + @Nullable String className, + int inputType, + @Nullable String eventText, + @Nullable String viewNodeText) { + View view = new View(mContext); + ViewStructureImpl viewStructure = new ViewStructureImpl(view); + if (className != null) { + viewStructure.setClassName(className); + } + if (viewNodeText != null) { + viewStructure.setText(viewNodeText); + } + viewStructure.setInputType(inputType); + + ContentCaptureEvent event = createProcessEvent(); + event.setViewNode(viewStructure.getNode()); + if (eventText != null) { + event.setText(eventText); + } + + return event; + } + + private ContentCaptureEvent createAndroidPasswordFieldEvent( + @Nullable String className, int inputType) { + return createProcessEvent( + className, inputType, /* eventText= */ null, /* viewNodeText= */ null); + } + + private ContentCaptureEvent createWebViewPasswordFieldEvent( + @Nullable String className, @Nullable String eventText, @Nullable String viewNodeText) { + return createProcessEvent(className, /* inputType= */ 0, eventText, viewNodeText); + } + + private ContentCaptureEvent createSuspiciousTextEvent( + @Nullable String eventText, @Nullable String viewNodeText) { + return createProcessEvent( + /* className= */ null, /* inputType= */ 0, eventText, viewNodeText); + } + + private void assertOnLoginDetected() throws Exception { + assertOnLoginDetected(PROCESS_EVENT, /* times= */ 1); + } + + private void assertOnLoginDetected(ContentCaptureEvent event, int times) throws Exception { + ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class); + verify(mMockContentCaptureManager, times(times)).onLoginDetected(captor.capture()); + + assertThat(captor.getValue()).isNotNull(); + List<ContentCaptureEvent> actual = captor.getValue().getList(); + assertThat(actual).isNotNull(); + assertThat(actual).containsExactly(event); + } +} diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 2ef2d3a968e0..a6e74d0d6b94 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -340,4 +341,42 @@ public class WindowOnBackInvokedDispatcherTest { verify(mCallback1).onBackCancelled(); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); } + + @Test + public void onBackInvoked_calledAfterOnBackStarted() throws RemoteException { + mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1); + OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); + + callbackInfo.getCallback().onBackStarted(mBackEvent); + + waitForIdle(); + verify(mCallback1).onBackStarted(any(BackEvent.class)); + + callbackInfo.getCallback().onBackInvoked(); + + waitForIdle(); + verify(mCallback1).onBackInvoked(); + verify(mCallback1, never()).onBackCancelled(); + } + + @Test + public void onDetachFromWindow_cancelCallbackAndIgnoreOnBackInvoked() throws RemoteException { + mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1); + + OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); + + callbackInfo.getCallback().onBackStarted(mBackEvent); + + waitForIdle(); + verify(mCallback1).onBackStarted(any(BackEvent.class)); + + // This should trigger mCallback1.onBackCancelled() + mDispatcher.detachFromWindow(); + // This should be ignored by mCallback1 + callbackInfo.getCallback().onBackInvoked(); + + waitForIdle(); + verify(mCallback1, never()).onBackInvoked(); + verify(mCallback1).onBackCancelled(); + } } diff --git a/data/etc/com.android.networkstack.xml b/data/etc/com.android.networkstack.xml index 06fec1cdab1e..003ca53b03b0 100644 --- a/data/etc/com.android.networkstack.xml +++ b/data/etc/com.android.networkstack.xml @@ -25,6 +25,7 @@ <permission name="android.permission.LOCAL_MAC_ADDRESS"/> <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS"/> <permission name="android.permission.MANAGE_USB"/> + <permission name="android.permission.MANAGE_USERS"/> <permission name="android.permission.NETWORK_BYPASS_PRIVATE_DNS"/> <permission name="android.permission.PACKET_KEEPALIVE_OFFLOAD"/> <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index e4defcfa359f..136d1a25255b 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -3379,6 +3379,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "975028389": { + "message": "unable to call receiver for empty keyboard shortcuts", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "975275467": { "message": "Set animatingExit: reason=remove\/isAnimating win=%s", "level": "VERBOSE", 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 39338e5baddb..146774189490 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 @@ -2152,7 +2152,7 @@ public class BubbleController implements ConfigurationChangeListener, pw.println(" suppressing: " + key); } - pw.print("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values()); + pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values()); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 12d51f54a09c..47d58afb6aba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -25,6 +25,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; @@ -86,13 +87,14 @@ public class TvWMShellModule { TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController, @ShellMainThread ShellExecutor mainExecutor, Handler mainHandler, SystemWindows systemWindows) { return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, - transactionPool, iconProvider, recentTasks, mainExecutor, mainHandler, - systemWindows); + transactionPool, iconProvider, recentTasks, launchAdjacentController, mainExecutor, + mainHandler, systemWindows); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 5a9c25f7a710..7cc2b9e64ec8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -46,6 +46,7 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; @@ -70,6 +71,8 @@ import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -77,8 +80,6 @@ import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipTouchHandler; -import com.android.wm.shell.keyguard.KeyguardTransitionHandler; -import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; @@ -275,6 +276,13 @@ public abstract class WMShellBaseModule { return new WindowManagerShellWrapper(mainExecutor); } + @WMSingleton + @Provides + static LaunchAdjacentController provideLaunchAdjacentController( + SyncTransactionQueue syncQueue) { + return new LaunchAdjacentController(syncQueue); + } + // // Back animation // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 2dbccaceeddc..e2010c220bc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -41,6 +41,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; @@ -334,11 +335,12 @@ public abstract class WMShellModule { TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController, @ShellMainThread ShellExecutor mainExecutor) { return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, - transactionPool, iconProvider, recentTasks, mainExecutor); + transactionPool, iconProvider, recentTasks, launchAdjacentController, mainExecutor); } // @@ -679,6 +681,7 @@ public abstract class WMShellModule { static DesktopTasksController provideDesktopTasksController( Context context, ShellInit shellInit, + ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, ShellTaskOrganizer shellTaskOrganizer, @@ -688,12 +691,13 @@ public abstract class WMShellModule { EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + LaunchAdjacentController launchAdjacentController, @ShellMainThread ShellExecutor mainExecutor ) { - return new DesktopTasksController(context, shellInit, shellController, displayController, - shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions, - enterDesktopTransitionHandler, exitDesktopTransitionHandler, - desktopModeTaskRepository, mainExecutor); + return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, + displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, + transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, + desktopModeTaskRepository, launchAdjacentController, mainExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 402bb96dc0c0..a490fb8289d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -25,6 +25,7 @@ import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.util.KtProtoLog +import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer @@ -339,6 +340,25 @@ class DesktopModeTaskRepository { return displayData[displayId]?.stashed ?: false } + internal fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("${prefix}DesktopModeTaskRepository") + dumpDisplayData(pw, innerPrefix) + pw.println("${innerPrefix}freeformTasksInZOrder=${freeformTasksInZOrder.toDumpString()}") + pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}") + pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}") + } + + private fun dumpDisplayData(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + displayData.forEach { displayId, data -> + pw.println("${prefix}Display $displayId:") + pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}") + pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}") + pw.println("${innerPrefix}stashed=${data.stashed}") + } + } + /** * Defines interface for classes that can listen to changes for active tasks in desktop mode. */ @@ -367,3 +387,7 @@ class DesktopModeTaskRepository { fun onStashedChanged(displayId: Int, stashed: Boolean) {} } } + +private fun <T> Iterable<T>.toDumpString(): String { + return joinToString(separator = ", ", prefix = "[", postfix = "]") +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 37cbbcd1853a..821fc13d7653 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -46,6 +46,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener @@ -54,11 +55,14 @@ import com.android.wm.shell.common.annotations.ExternalThread import com.android.wm.shell.common.annotations.ShellMainThread import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.KtProtoLog +import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration +import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer @@ -66,6 +70,7 @@ import java.util.function.Consumer class DesktopTasksController( private val context: Context, shellInit: ShellInit, + private val shellCommandHandler: ShellCommandHandler, private val shellController: ShellController, private val displayController: DisplayController, private val shellTaskOrganizer: ShellTaskOrganizer, @@ -75,6 +80,7 @@ class DesktopTasksController( private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val launchAdjacentController: LaunchAdjacentController, @ShellMainThread private val mainExecutor: ShellExecutor ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { @@ -85,6 +91,11 @@ class DesktopTasksController( visualIndicator?.releaseVisualIndicator(t) visualIndicator = null } + private val taskVisibilityListener = object : VisibleTasksListener { + override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { + launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks + } + } init { desktopMode = DesktopModeImpl() @@ -95,12 +106,14 @@ class DesktopTasksController( private fun onInit() { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") + shellCommandHandler.addDumpCallback(this::dump, this) shellController.addExternalInterface( ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, { createExternalInterface() }, this ) transitions.addHandler(this) + desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) } /** Show all tasks, that are part of the desktop, on top of launcher */ @@ -602,14 +615,17 @@ class DesktopTasksController( * @param taskInfo the task being dragged. * @param position position of surface when drag ends. * @param y the Y position of the motion event. + * @param windowDecor the window decoration for the task being dragged */ fun onDragPositioningEnd( taskInfo: RunningTaskInfo, position: Point, - y: Float + y: Float, + windowDecor: DesktopModeWindowDecoration ) { val statusBarHeight = getStatusBarHeight(taskInfo) if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + windowDecor.incrementRelayoutBlock() moveToFullscreenWithAnimation(taskInfo, position) } } @@ -710,6 +726,12 @@ class DesktopTasksController( desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor) } + private fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("${prefix}DesktopTasksController") + desktopModeTaskRepository.dump(pw, innerPrefix) + } + /** The interface for calls from outside the shell, within the host process. */ @ExternalThread private inner class DesktopModeImpl : DesktopMode { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index 513638eeb960..cef7e1666331 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -17,31 +17,25 @@ package com.android.wm.shell.keyguard; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_SLEEP; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; -import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; +import static android.view.WindowManager.TRANSIT_SLEEP; import static com.android.wm.shell.util.TransitionUtil.isOpeningType; -import static com.android.wm.shell.util.TransitionUtil.isClosingType; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.RemoteException; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.TransitionInfo; @@ -56,8 +50,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; -import java.util.Map; - /** * The handler for Keyguard enter/exit and occlude/unocclude animations. * @@ -70,7 +62,7 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler private final Handler mMainHandler; private final ShellExecutor mMainExecutor; - private final Map<IBinder, IRemoteTransition> mStartedTransitions = new ArrayMap<>(); + private final ArrayMap<IBinder, StartedTransition> mStartedTransitions = new ArrayMap<>(); /** * Local IRemoteTransition implementations registered by the keyguard service. @@ -81,6 +73,18 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler private IRemoteTransition mOccludeByDreamTransition = null; private IRemoteTransition mUnoccludeTransition = null; + private final class StartedTransition { + final TransitionInfo mInfo; + final SurfaceControl.Transaction mFinishT; + final IRemoteTransition mPlayer; + + public StartedTransition(TransitionInfo info, + SurfaceControl.Transaction finishT, IRemoteTransition player) { + mInfo = info; + mFinishT = finishT; + mPlayer = player; + } + } public KeyguardTransitionHandler( @NonNull ShellInit shellInit, @NonNull Transitions transitions, @@ -105,10 +109,7 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler } public static boolean handles(TransitionInfo info) { - return (info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0 - || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0 - || info.getType() == TRANSIT_KEYGUARD_OCCLUDE - || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE; + return (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0; } @Override @@ -120,39 +121,14 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler return false; } - boolean hasOpeningOcclude = false; - boolean hasClosingOcclude = false; - boolean hasOpeningDream = false; - boolean hasClosingApp = false; - - // Check for occluding/dream/closing apps - for (int i = info.getChanges().size() - 1; i >= 0; i--) { - final TransitionInfo.Change change = info.getChanges().get(i); - if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { - continue; - } else if (isOpeningType(change.getMode())) { - hasOpeningOcclude |= change.hasFlags(FLAG_OCCLUDES_KEYGUARD); - hasOpeningDream |= (change.getTaskInfo() != null - && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_DREAM); - } else if (isClosingType(change.getMode())) { - hasClosingOcclude |= change.hasFlags(FLAG_OCCLUDES_KEYGUARD); - hasClosingApp = true; - } - } - // Choose a transition applicable for the changes and keyguard state. if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { return startAnimation(mExitTransition, "going-away", transition, info, startTransaction, finishTransaction, finishCallback); } - if (hasOpeningOcclude || info.getType() == TRANSIT_KEYGUARD_OCCLUDE) { - if (hasClosingOcclude) { - // Transitions between apps on top of the keyguard can use the default handler. - // WM sends a final occlude status update after the transition is finished. - return false; - } - if (hasOpeningDream) { + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_OCCLUDING) != 0) { + if (hasOpeningDream(info)) { return startAnimation(mOccludeByDreamTransition, "occlude-by-dream", transition, info, startTransaction, finishTransaction, finishCallback); @@ -161,12 +137,12 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler "occlude", transition, info, startTransaction, finishTransaction, finishCallback); } - } else if (hasClosingApp || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) { + } else if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { return startAnimation(mUnoccludeTransition, "unocclude", transition, info, startTransaction, finishTransaction, finishCallback); } else { - Log.w(TAG, "Failed to play: " + info); + Log.i(TAG, "Refused to play keyguard transition: " + info); return false; } } @@ -194,7 +170,8 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler }); } }); - mStartedTransitions.put(transition, remoteHandler); + mStartedTransitions.put(transition, + new StartedTransition(info, finishTransaction, remoteHandler)); } catch (RemoteException e) { Log.wtf(TAG, "RemoteException thrown from local IRemoteTransition", e); return false; @@ -207,20 +184,35 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo, @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition, @NonNull TransitionFinishCallback nextFinishCallback) { - final IRemoteTransition playing = mStartedTransitions.get(currentTransition); - + final StartedTransition playing = mStartedTransitions.get(currentTransition); if (playing == null) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "unknown keyguard transition %s", currentTransition); return; } - - if (nextInfo.getType() == TRANSIT_SLEEP) { + if ((nextInfo.getFlags() & WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 + && (playing.mInfo.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { + // Keyguard unlocking has been canceled. Merge the unlock and re-lock transitions to + // avoid a flicker where we flash one frame with the screen fully unlocked. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "canceling keyguard exit transition %s", currentTransition); + playing.mFinishT.merge(nextT); + try { + playing.mPlayer.mergeAnimation(nextTransition, nextInfo, nextT, currentTransition, + new FakeFinishCallback()); + } catch (RemoteException e) { + // There is no good reason for this to happen because the player is a local object + // implementing an AIDL interface. + Log.wtf(TAG, "RemoteException thrown from KeyguardService transition", e); + } + nextFinishCallback.onTransitionFinished(null, null); + } else if (nextInfo.getType() == TRANSIT_SLEEP) { // An empty SLEEP transition comes in as a signal to abort transitions whenever a sleep // token is held. In cases where keyguard is showing, we are running the animation for // the device sleeping/waking, so it's best to ignore this and keep playing anyway. return; - } else { + } else if (handles(nextInfo)) { + // In all other cases, fast-forward to let the next queued transition start playing. finishAnimationImmediately(currentTransition, playing); } } @@ -228,7 +220,7 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler @Override public void onTransitionConsumed(IBinder transition, boolean aborted, SurfaceControl.Transaction finishTransaction) { - final IRemoteTransition playing = mStartedTransitions.remove(transition); + final StartedTransition playing = mStartedTransitions.remove(transition); if (playing != null) { finishAnimationImmediately(transition, playing); } @@ -241,13 +233,26 @@ public class KeyguardTransitionHandler implements Transitions.TransitionHandler return null; } - private void finishAnimationImmediately(IBinder transition, IRemoteTransition playing) { + private static boolean hasOpeningDream(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isOpeningType(change.getMode()) + && change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_DREAM) { + return true; + } + } + return false; + } + + private void finishAnimationImmediately(IBinder transition, StartedTransition playing) { final IBinder fakeTransition = new Binder(); final TransitionInfo fakeInfo = new TransitionInfo(TRANSIT_SLEEP, 0x0); final SurfaceControl.Transaction fakeT = new SurfaceControl.Transaction(); final FakeFinishCallback fakeFinishCb = new FakeFinishCallback(); try { - playing.mergeAnimation(fakeTransition, fakeInfo, fakeT, transition, fakeFinishCb); + playing.mPlayer.mergeAnimation( + fakeTransition, fakeInfo, fakeT, transition, fakeFinishCb); } catch (RemoteException e) { // There is no good reason for this to happen because the player is a local object // implementing an AIDL interface. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 281cae5e4ffa..abe2db094a5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -70,6 +70,7 @@ public class PipResizeGestureHandler { private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipMotionHelper mMotionHelper; private final PipBoundsState mPipBoundsState; + private final PipTouchState mPipTouchState; private final PipTaskOrganizer mPipTaskOrganizer; private final PhonePipMenuController mPhonePipMenuController; private final PipDismissTargetHandler mPipDismissTargetHandler; @@ -104,7 +105,6 @@ public class PipResizeGestureHandler { private boolean mAllowGesture; private boolean mIsAttached; private boolean mIsEnabled; - private boolean mEnableTouch; private boolean mEnablePinchResize; private boolean mEnableDragCornerResize; private boolean mIsSysUiStateValid; @@ -122,7 +122,8 @@ public class PipResizeGestureHandler { public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper motionHelper, - PipTaskOrganizer pipTaskOrganizer, PipDismissTargetHandler pipDismissTargetHandler, + PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer, + PipDismissTargetHandler pipDismissTargetHandler, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, ShellExecutor mainExecutor) { @@ -132,6 +133,7 @@ public class PipResizeGestureHandler { mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipBoundsState = pipBoundsState; mMotionHelper = motionHelper; + mPipTouchState = pipTouchState; mPipTaskOrganizer = pipTaskOrganizer; mPipDismissTargetHandler = pipDismissTargetHandler; mMovementBoundsSupplier = movementBoundsSupplier; @@ -139,7 +141,6 @@ public class PipResizeGestureHandler { mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); - mEnableTouch = true; mUpdateResizeBoundsCallback = (rect) -> { mUserResizeBounds.set(rect); @@ -250,8 +251,8 @@ public class PipResizeGestureHandler { return; } - if (!mEnableTouch) { - // No need to handle anything if touches are not enabled for resizing. + if (!mPipTouchState.getAllowInputEvents()) { + // No need to handle anything if touches are not enabled return; } @@ -588,13 +589,13 @@ public class PipResizeGestureHandler { mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); - // disable the resizing until the final bounds are updated - mEnableTouch = false; + // disable any touch events beyond resizing too + mPipTouchState.setAllowInputEvents(false); mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { - // reset the pinch resizing to its default state - mEnableTouch = true; + // enable touch events + mPipTouchState.setAllowInputEvents(true); }); } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 8f6cee76b68a..500094335258 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -199,11 +199,6 @@ public class PipTouchHandler { mMotionHelper = pipMotionHelper; mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, mMotionHelper, mainExecutor); - mPipResizeGestureHandler = - new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, - mMotionHelper, pipTaskOrganizer, mPipDismissTargetHandler, - this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger, - menuController, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), () -> { if (mPipBoundsState.isStashed()) { @@ -220,6 +215,11 @@ public class PipTouchHandler { }, menuController::hideMenu, mainExecutor); + mPipResizeGestureHandler = + new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, + mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler, + this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger, + menuController, mainExecutor); mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), this::onAccessibilityShowMenu, this::updateMovementBounds, @@ -556,6 +556,11 @@ public class PipTouchHandler { return true; } + // do not process input event if not allowed + if (!mTouchState.getAllowInputEvents()) { + return true; + } + MotionEvent ev = (MotionEvent) inputEvent; if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) { // Initialize the touch state for the gesture, but immediately reset to invalidate the diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java index d7d69f27f9f8..7f62c629c7f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -37,7 +37,7 @@ public class PipTouchState { private static final boolean DEBUG = false; @VisibleForTesting - public static final long DOUBLE_TAP_TIMEOUT = 200; + public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); static final long HOVER_EXIT_TIMEOUT = 50; private final ShellExecutor mMainExecutor; @@ -55,6 +55,9 @@ public class PipTouchState { private final PointF mLastDelta = new PointF(); private final PointF mVelocity = new PointF(); private boolean mAllowTouches = true; + + // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing + private boolean mAllowInputEvents = true; private boolean mIsUserInteracting = false; // Set to true only if the multiple taps occur within the double tap timeout private boolean mIsDoubleTap = false; @@ -77,6 +80,20 @@ public class PipTouchState { } /** + * @return true if input processing is enabled for PiP in general. + */ + public boolean getAllowInputEvents() { + return mAllowInputEvents; + } + + /** + * @param allowInputEvents true to enable input processing for PiP in general. + */ + public void setAllowInputEvents(boolean allowInputEvents) { + mAllowInputEvents = allowInputEvents; + } + + /** * Resets this state. */ public void reset() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index ea33a1f1b56d..d0ea611dc403 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -76,6 +76,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ExternalInterfaceBinder; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -170,6 +171,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final TransactionPool mTransactionPool; private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; + private final LaunchAdjacentController mLaunchAdjacentController; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; private final String[] mAppsSupportMultiInstances; @@ -196,6 +198,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController, ShellExecutor mainExecutor) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; @@ -212,6 +215,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mTransactionPool = transactionPool; mIconProvider = iconProvider; mRecentTasksOptional = recentTasks; + mLaunchAdjacentController = launchAdjacentController; mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic // override for this controller from the base module @@ -241,6 +245,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, TransactionPool transactionPool, IconProvider iconProvider, RecentTasksController recentTasks, + LaunchAdjacentController launchAdjacentController, ShellExecutor mainExecutor, StageCoordinator stageCoordinator) { mShellCommandHandler = shellCommandHandler; @@ -258,6 +263,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mTransactionPool = transactionPool; mIconProvider = iconProvider; mRecentTasksOptional = Optional.of(recentTasks); + mLaunchAdjacentController = launchAdjacentController; mStageCoordinator = stageCoordinator; mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); shellInit.addInitCallback(this::onInit, this); @@ -296,7 +302,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, - mIconProvider, mMainExecutor, mRecentTasksOptional); + mIconProvider, mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index b2526ee97a21..8497f9f156f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -123,6 +123,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -196,6 +197,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // if user is opening another task(s). private final ArrayList<Integer> mPausingTasks = new ArrayList<>(); private final Optional<RecentTasksController> mRecentTasks; + private final LaunchAdjacentController mLaunchAdjacentController; private final Rect mTempRect1 = new Rect(); private final Rect mTempRect2 = new Rect(); @@ -273,7 +275,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks) { + Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -281,6 +284,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mRecentTasks = recentTasks; + mLaunchAdjacentController = launchAdjacentController; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); @@ -327,7 +331,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks) { + Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -344,6 +349,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mRecentTasks = recentTasks; + mLaunchAdjacentController = launchAdjacentController; mDisplayController.addDisplayWindowListener(this); mDisplayLayout = new DisplayLayout(); transitions.addHandler(this); @@ -579,6 +585,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRecentTasks.get().removeSplitPair(taskId1); } options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, null); wct.startTask(taskId1, options1); mSplitTransitions.startFullscreenTransition(wct, remoteTransition); return; @@ -600,6 +607,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (taskId == INVALID_TASK_ID) { options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, null); wct.sendPendingIntent(pendingIntent, fillInIntent, options1); mSplitTransitions.startFullscreenTransition(wct, remoteTransition); return; @@ -620,6 +628,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (taskId == INVALID_TASK_ID) { options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, null); wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); mSplitTransitions.startFullscreenTransition(wct, remoteTransition); return; @@ -678,6 +687,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (pendingIntent2 == null) { options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, null); if (shortcutInfo1 != null) { wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); } else { @@ -1743,7 +1753,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true); // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); - wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); setRootForceTranslucent(true, wct); mSplitLayout.getInvisibleBounds(mTempRect1); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); @@ -1751,6 +1760,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue.runInSync(t -> { t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); }); + mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token); } /** Callback when split roots have child task appeared under it, this is a little different from @@ -1780,9 +1790,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void onRootTaskVanished() { final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (mRootTaskInfo != null) { - wct.clearLaunchAdjacentFlagRoot(mRootTaskInfo.token); - } + mLaunchAdjacentController.clearLaunchAdjacentRoot(); applyExitSplitScreen(null /* childrenToTop */, wct, EXIT_REASON_ROOT_TASK_VANISHED); mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } @@ -2435,7 +2443,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, continue; } final StageTaskListener stage = getStageOfTask(taskInfo); - if (stage == null) continue; + if (stage == null) { + if (change.getParent() == null && !isClosingType(change.getMode()) + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + record.mContainShowFullscreenChange = true; + } + continue; + } if (isOpeningType(change.getMode())) { if (!stage.containsTask(taskInfo.taskId)) { Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called" @@ -2450,22 +2464,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } } - // If the size of dismissStages == 1, one of the task is closed without prepare pending - // transition, which could happen if all activities were finished after finish top - // activity in a task, so the trigger task is null when handleRequest. - // Note if the size of dismissStages == 2, it's starting a new task, so don't handle it. final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0 || dismissStages.size() == 1) { + // If the size of dismissStages == 1, one of the task is closed without prepare + // pending transition, which could happen if all activities were finished after + // finish top activity in a task, so the trigger task is null when handleRequest. + // Note if the size of dismissStages == 2, it's starting a new task, + // so don't handle it. Log.e(TAG, "Somehow removed the last task in a stage outside of a proper " + "transition."); final WindowContainerTransaction wct = new WindowContainerTransaction(); final int dismissTop = (dismissStages.size() == 1 && getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN) || mMainStage.getChildCount() == 0 ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; - prepareExitSplitScreen(dismissTop, wct); + // If there is a fullscreen opening change, we should not bring stage to top. + prepareExitSplitScreen(record.mContainShowFullscreenChange + ? STAGE_TYPE_UNDEFINED : dismissTop, wct); mSplitTransitions.startDismissTransition(wct, this, dismissTop, - EXIT_REASON_UNKNOWN); + EXIT_REASON_APP_FINISHED); // This can happen in some pathological cases. For example: // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C] // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time @@ -2473,7 +2490,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // TODO(b/184679596): Find a way to either include task-org information in // the transition, or synchronize task-org callbacks. } - // Use normal animations. return false; } else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) { @@ -2490,6 +2506,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } static class StageChangeRecord { + boolean mContainShowFullscreenChange = false; static class StageChange { final StageTaskListener mStageTaskListener; final IntArray mAddedTaskId = new IntArray(); @@ -2622,6 +2639,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, Log.w(TAG, "Launched a task in split, but didn't receive any task in transition."); mSplitTransitions.mPendingEnter.cancel((cancelWct, cancelT) -> prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, cancelWct)); + mSplitUnsupportedToast.show(); return true; } } else { @@ -2632,6 +2650,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); mSplitTransitions.mPendingEnter.cancel( (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); + mSplitUnsupportedToast.show(); return true; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index 27d520d81c41..f05f32442cd6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -27,6 +27,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; @@ -58,6 +59,7 @@ public class TvSplitScreenController extends SplitScreenController { private final TransactionPool mTransactionPool; private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; + private final LaunchAdjacentController mLaunchAdjacentController; private final Handler mMainHandler; private final SystemWindows mSystemWindows; @@ -77,13 +79,14 @@ public class TvSplitScreenController extends SplitScreenController { TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController, ShellExecutor mainExecutor, Handler mainHandler, SystemWindows systemWindows) { super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, - iconProvider, recentTasks, mainExecutor); + iconProvider, recentTasks, launchAdjacentController, mainExecutor); mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; @@ -96,6 +99,7 @@ public class TvSplitScreenController extends SplitScreenController { mTransactionPool = transactionPool; mIconProvider = iconProvider; mRecentTasksOptional = recentTasks; + mLaunchAdjacentController = launchAdjacentController; mMainHandler = mainHandler; mSystemWindows = systemWindows; @@ -111,7 +115,7 @@ public class TvSplitScreenController extends SplitScreenController { mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, mMainExecutor, mMainHandler, - mRecentTasksOptional, mSystemWindows); + mRecentTasksOptional, mLaunchAdjacentController, mSystemWindows); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index 4d563fbb7f04..0b11922ce113 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -24,6 +24,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; @@ -51,10 +52,11 @@ public class TvStageCoordinator extends StageCoordinator IconProvider iconProvider, ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController, SystemWindows systemWindows) { super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, - mainExecutor, recentTasks); + mainExecutor, recentTasks, launchAdjacentController); mTvSplitMenuController = new TvSplitMenuController(context, this, systemWindows, mainHandler); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 5ee5324f8758..f58f24be4984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -31,6 +31,7 @@ import static com.android.wm.shell.util.TransitionUtil.isOpeningType; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.IBinder; +import android.util.Log; import android.util.Pair; import android.view.SurfaceControl; import android.view.WindowManager; @@ -589,11 +590,18 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - boolean consumed = mKeyguardHandler.startAnimation( - mixed.mTransition, info, startTransaction, finishTransaction, finishCallback); - if (!consumed) { + final Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + mixed.mInFlightSubAnimations--; + if (mixed.mInFlightSubAnimations == 0) { + mActiveTransitions.remove(mixed); + finishCallback.onTransitionFinished(wct, wctCB); + } + }; + if (!mKeyguardHandler.startAnimation( + mixed.mTransition, info, startTransaction, finishTransaction, finishCB)) { return false; } + mixed.mInFlightSubAnimations++; // Sync pip state. if (mPipHandler != null) { // We don't know when to apply `startTransaction` so use a separate transaction here. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index ce8d792ef302..de20c2d90066 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -554,7 +555,10 @@ public class Transitions implements RemoteCallable<Transitions>, layer = -zSplitLine - i; } } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - if (isOpening) { + if (isOpening + // This is for when an activity launches while a different transition is + // collecting. + || change.hasFlags(FLAG_MOVED_TO_TOP)) { // put on top layer = zSplitLine + numChanges - i; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index bb0eba6a0fc7..c504f57216f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -173,6 +173,17 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene mTransition = null; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull TransitionFinishCallback finishCallback) { + if (info.getType() == TRANSIT_CHANGE) { + // Apply changes happening during the unfold animation immediately + t.apply(); + finishCallback.onTransitionFinished(null, null); + } + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 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 2fd34d9dc699..f0f06a6f07dd 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 @@ -193,7 +193,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change) { if (change.getMode() == WindowManager.TRANSIT_CHANGE - && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE)) { + && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE + || info.getType() == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE + || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE)) { mWindowDecorByTaskId.get(change.getTaskInfo().taskId) .addTransitionPausingRelayout(transition); } @@ -411,7 +413,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo, - position, e.getRawY())); + position, e.getRawY(), mWindowDecorByTaskId.get(mTaskId))); final boolean wasDragging = mIsDragging; mIsDragging = false; return wasDragging; @@ -577,9 +579,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return; } else if (mDragToDesktopAnimationStarted) { Point position = new Point((int) ev.getX(), (int) ev.getY()); + relevantDecor.incrementRelayoutBlock(); mDesktopTasksController.ifPresent( - c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo, - position)); + c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo, position)); mDragToDesktopAnimationStarted = false; return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt index 514ea52cb8ae..76c80f7bbe85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -4,7 +4,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.graphics.Color import android.view.View - +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS /** * Encapsulates the root [View] of a window decoration and its children to facilitate looking up * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. @@ -23,6 +23,10 @@ internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { * with the caption background color. */ protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { - return Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5 + return if (Color.alpha(taskInfo.taskDescription.statusBarColor) != 0) { + Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5 + } else { + taskInfo.taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 + } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index a4ac261d1946..ef7bedf49a92 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder @@ -55,20 +54,24 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - pipApp.launchViaIntent(wmHelper) - pipApp.enableAutoEnterForPipActivity() - } - teardown { - // close gracefully so that onActivityUnpinned() can be called before force exit - pipApp.closePipWindow(wmHelper) - pipApp.exit(wmHelper) - } - transitions { tapl.goHome() } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { tapl.goHome() } + } + + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + pipApp.launchViaIntent(wmHelper) + pipApp.enableAutoEnterForPipActivity() } + } + + override val defaultTeardown: FlickerBuilder.() -> Unit = { + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) + } + } @Presubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt index 98fc91b334cf..afcc1729ed16 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt @@ -54,40 +54,38 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ClosePipBySwipingDownTest(flicker: FlickerTest) : ClosePipTransition(flicker) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - val pipRegion = wmHelper.getWindowRegion(pipApp).bounds - val pipCenterX = pipRegion.centerX() - val pipCenterY = pipRegion.centerY() - val displayCenterX = device.displayWidth / 2 - val barComponent = - if (flicker.scenario.isTablet) { - ComponentNameMatcher.TASK_BAR - } else { - ComponentNameMatcher.NAV_BAR - } - val barLayerHeight = - wmHelper.currentState.layerState - .getLayerWithBuffer(barComponent) - ?.visibleRegion - ?.height - ?: error("Couldn't find Nav or Task bar layer") - // The dismiss button doesn't appear at the complete bottom of the screen, - // it appears above the hot seat but `hotseatBarSize` is not available outside - // the platform - val displayY = (device.displayHeight * 0.9).toInt() - barLayerHeight - device.swipe(pipCenterX, pipCenterY, displayCenterX, displayY, 50) - // Wait until the other app is no longer visible - wmHelper - .StateSyncBuilder() - .withPipGone() - .withWindowSurfaceDisappeared(pipApp) - .withAppTransitionIdle() - .waitForAndVerify() - } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { + val pipRegion = wmHelper.getWindowRegion(pipApp).bounds + val pipCenterX = pipRegion.centerX() + val pipCenterY = pipRegion.centerY() + val displayCenterX = device.displayWidth / 2 + val barComponent = + if (flicker.scenario.isTablet) { + ComponentNameMatcher.TASK_BAR + } else { + ComponentNameMatcher.NAV_BAR + } + val barLayerHeight = + wmHelper.currentState.layerState + .getLayerWithBuffer(barComponent) + ?.visibleRegion + ?.height + ?: error("Couldn't find Nav or Task bar layer") + // The dismiss button doesn't appear at the complete bottom of the screen, + // it appears above the hot seat but `hotseatBarSize` is not available outside + // the platform + val displayY = (device.displayHeight * 0.9).toInt() - barLayerHeight + device.swipe(pipCenterX, pipCenterY, displayCenterX, displayY, 50) + // Wait until the other app is no longer visible + wmHelper + .StateSyncBuilder() + .withPipGone() + .withWindowSurfaceDisappeared(pipApp) + .withAppTransitionIdle() + .waitForAndVerify() } + } /** Checks that the focus doesn't change between windows during the transition */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt index 2417c45bf9a0..e52b71e602f9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt @@ -28,11 +28,10 @@ import org.junit.runners.Parameterized /** Base class for exiting pip (closing pip window) without returning to the app */ abstract class ClosePipTransition(flicker: FlickerTest) : PipTransition(flicker) { - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - setup { this.setRotation(flicker.scenario.startRotation) } - teardown { this.setRotation(Rotation.ROTATION_0) } - } + override val thisTransition: FlickerBuilder.() -> Unit = { + setup { this.setRotation(flicker.scenario.startRotation) } + teardown { this.setRotation(Rotation.ROTATION_0) } + } /** * Checks that [pipApp] window is pinned and visible at the start and then becomes unpinned and diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt index d16583271e8c..86fe583c94e6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt @@ -54,12 +54,9 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ClosePipWithDismissButtonTest(flicker: FlickerTest) : ClosePipTransition(flicker) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { pipApp.closePipWindow(wmHelper) } - } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.closePipWindow(wmHelper) } + } /** * Checks that the focus changes between the pip menu window and the launcher when clicking the diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index f52e877ec2b1..01d67cc35a14 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -45,20 +45,24 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTransition(flicker) { - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - pipApp.launchViaIntent(wmHelper) - pipApp.enableEnterPipOnUserLeaveHint() - } - teardown { - // close gracefully so that onActivityUnpinned() can be called before force exit - pipApp.closePipWindow(wmHelper) - pipApp.exit(wmHelper) - } - transitions { tapl.goHome() } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { tapl.goHome() } + } + + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + pipApp.launchViaIntent(wmHelper) + pipApp.enableEnterPipOnUserLeaveHint() + } + } + + override val defaultTeardown: FlickerBuilder.() -> Unit = { + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) } + } @Presubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 4b4613704a16..5480144ba1ce 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -73,39 +73,39 @@ open class EnterPipToOtherOrientation(flicker: FlickerTest) : PipTransition(flic private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - // Launch a portrait only app on the fullscreen stack - testApp.launchViaIntent( + override val thisTransition: FlickerBuilder.() -> Unit = { + teardown { + testApp.exit(wmHelper) + } + transitions { + // Enter PiP, and assert that the PiP is within bounds now that the device is back + // in portrait + broadcastActionTrigger.doAction(ACTION_ENTER_PIP) + // during rotation the status bar becomes invisible and reappears at the end + wmHelper + .StateSyncBuilder() + .withPipShown() + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() + } + } + + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + // Launch a portrait only app on the fullscreen stack + testApp.launchViaIntent( wmHelper, stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString()) - ) - // Launch the PiP activity fixed as landscape - pipApp.launchViaIntent( + ) + // Launch the PiP activity fixed as landscape, but don't enter PiP + pipApp.launchViaIntent( wmHelper, stringExtras = - mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()) - ) - } - teardown { - pipApp.exit(wmHelper) - testApp.exit(wmHelper) - } - transitions { - // Enter PiP, and assert that the PiP is within bounds now that the device is back - // in portrait - broadcastActionTrigger.doAction(ACTION_ENTER_PIP) - // during rotation the status bar becomes invisible and reappears at the end - wmHelper - .StateSyncBuilder() - .withPipShown() - .withNavOrTaskBarVisible() - .withStatusBarVisible() - .waitForAndVerify() - } + mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()) + ) } + } /** * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt index bfd57786e615..95121def07bc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt @@ -26,12 +26,11 @@ import org.junit.Test import org.junit.runners.Parameterized abstract class EnterPipTransition(flicker: FlickerTest) : PipTransition(flicker) { - /** {@inheritDoc} */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { pipApp.launchViaIntent(wmHelper) } - teardown { pipApp.exit(wmHelper) } + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + pipApp.launchViaIntent(wmHelper) } + } /** Checks [pipApp] window remains visible throughout the animation */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt index f1925d8c9d85..95725b64a48a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt @@ -51,11 +51,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class EnterPipViaAppUiButtonTest(flicker: FlickerTest) : EnterPipTransition(flicker) { - - /** {@inheritDoc} */ - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { pipApp.clickEnterPipButton(wmHelper) } - } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.clickEnterPipButton(wmHelper) } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt index 3e0e37dfc997..0b3d16a8087d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt @@ -53,19 +53,16 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ExitPipToAppViaExpandButtonTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { - - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - setup { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } - transitions { - // This will bring PipApp to fullscreen - pipApp.expandPipWindowToApp(wmHelper) - // Wait until the other app is no longer visible - wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify() - } + override val thisTransition: FlickerBuilder.() -> Unit = { + setup { + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) + } + transitions { + // This will bring PipApp to fullscreen + pipApp.expandPipWindowToApp(wmHelper) + // Wait until the other app is no longer visible + wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify() } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt index 603f99541a12..bb2d40becdc9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt @@ -52,19 +52,16 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ExitPipToAppViaIntentTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { - - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - setup { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } - transitions { - // This will bring PipApp to fullscreen - pipApp.exitPipToFullScreenViaIntent(wmHelper) - // Wait until the other app is no longer visible - wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify() - } + override val thisTransition: FlickerBuilder.() -> Unit = { + setup { + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) + } + transitions { + // This will bring PipApp to fullscreen + pipApp.exitPipToFullScreenViaIntent(wmHelper) + // Wait until the other app is no longer visible + wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify() } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt index bbb1c6c2ac63..fd16b6ea6ada 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -56,8 +56,9 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) { - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { transitions { pipApp.doubleClickPipWindow(wmHelper) } } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.doubleClickPipWindow(wmHelper) } + } /** * Checks that the pip app window remains inside the display bounds throughout the whole diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt index d860e00fbfff..253aa4cae5c7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt @@ -35,8 +35,9 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) { - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.25f, 30) } } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.25f, 30) } + } /** Checks that the visible region area of [pipApp] always increases during the animation. */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt index d8d57d219933..094060f86691 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt @@ -56,15 +56,10 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class MovePipDownOnShelfHeightChange(flicker: FlickerTest) : MovePipShelfHeightTransition(flicker) { - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - teardown { - tapl.pressHome() - testApp.exit(wmHelper) - } - transitions { testApp.launchViaIntent(wmHelper) } - } + override val thisTransition: FlickerBuilder.() -> Unit = { + teardown { testApp.exit(wmHelper) } + transitions { testApp.launchViaIntent(wmHelper) } + } /** Checks that the visible region of [pipApp] window always moves down during the animation. */ @Presubmit @Test fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt index 6b061bbb1565..ff51c27bf116 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt @@ -41,23 +41,21 @@ import org.junit.runners.Parameterized open class MovePipOnImeVisibilityChangeTest(flicker: FlickerTest) : PipTransition(flicker) { private val imeApp = ImeAppHelper(instrumentation) - /** {@inheritDoc} */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - setup { - imeApp.launchViaIntent(wmHelper) - setRotation(flicker.scenario.startRotation) - } - teardown { imeApp.exit(wmHelper) } - transitions { - // open the soft keyboard - imeApp.openIME(wmHelper) - createTag(TAG_IME_VISIBLE) + override val thisTransition: FlickerBuilder.() -> Unit = { + setup { + imeApp.launchViaIntent(wmHelper) + setRotation(flicker.scenario.startRotation) + } + teardown { imeApp.exit(wmHelper) } + transitions { + // open the soft keyboard + imeApp.openIME(wmHelper) + createTag(TAG_IME_VISIBLE) - // then close it again - imeApp.closeIME(wmHelper) - } + // then close it again + imeApp.closeIME(wmHelper) } + } /** Ensure the pip window remains visible throughout any keyboard interactions */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt index ae3f87967658..27b061b67a85 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt @@ -57,14 +57,12 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) open class MovePipUpOnShelfHeightChangeTest(flicker: FlickerTest) : MovePipShelfHeightTransition(flicker) { - /** Defines the transition used to run the test */ - override val transition: FlickerBuilder.() -> Unit - get() = - buildTransition() { - setup { testApp.launchViaIntent(wmHelper) } - transitions { tapl.pressHome() } - teardown { testApp.exit(wmHelper) } - } + override val thisTransition: FlickerBuilder.() -> Unit = + { + setup { testApp.launchViaIntent(wmHelper) } + transitions { tapl.pressHome() } + teardown { testApp.exit(wmHelper) } + } /** Checks that the visible region of [pipApp] window always moves up during the animation. */ @Presubmit @Test fun pipWindowMovesUp() = pipWindowMoves(Direction.UP) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt index 4e2a4e700698..9f81ba8eee87 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory -import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule import com.android.server.wm.flicker.testapp.ActivityOptions import org.junit.FixMethodOrder import org.junit.Test @@ -37,28 +36,31 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) { private var isDraggedLeft: Boolean = true - override val transition: FlickerBuilder.() -> Unit - get() = { - val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") - setup { - tapl.setEnableRotation(true) - // Launch the PIP activity and wait for it to enter PiP mode - RemoveAllTasksButHomeRule.removeAllTasksButHome() - pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) } + } - // determine the direction of dragging to test for - isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper) - } - teardown { - // release the primary pointer after dragging without release - pipApp.releasePipAfterDragging() + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + setup { + tapl.setEnableRotation(true) + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - pipApp.exit(wmHelper) - tapl.setEnableRotation(false) - } - transitions { pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) } + // determine the direction of dragging to test for + isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper) + } + } + + override val defaultTeardown: FlickerBuilder.() -> Unit = { + teardown { + // release the primary pointer after dragging without release + pipApp.releasePipAfterDragging() + + pipApp.exit(wmHelper) + tapl.setEnableRotation(false) } + } @Postsubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt index 8eb41b4ac694..60bf5ffdc7af 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt @@ -37,8 +37,9 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @FlakyTest(bugId = 270677470) class PipPinchInTest(flicker: FlickerTest) : PipTransition(flicker) { - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { transitions { pipApp.pinchInPipWindow(wmHelper, 0.4f, 30) } } + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { pipApp.pinchInPipWindow(wmHelper, 0.4f, 30) } + } /** Checks that the visible region area of [pipApp] always decreases during the animation. */ @Postsubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index eb1245b9ab86..17a178f78de3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -56,27 +56,37 @@ abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { } } - /** - * Gets a configuration that handles basic setup and teardown of pip tests and that launches the - * Pip app for test - * - * @param stringExtras Arguments to pass to the PIP launch intent - * @param extraSpec Additional segment of flicker specification - */ - @JvmOverloads - protected open fun buildTransition( - stringExtras: Map<String, String> = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true"), - extraSpec: FlickerBuilder.() -> Unit = {} - ): FlickerBuilder.() -> Unit { - return { - setup { - setRotation(Rotation.ROTATION_0) - removeAllTasksButHome() - pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - } - teardown { pipApp.exit(wmHelper) } + /** Defines the transition used to run the test */ + protected open val thisTransition: FlickerBuilder.() -> Unit = {} - extraSpec(this) + override val transition: FlickerBuilder.() -> Unit + get() = { + defaultSetup(this) + defaultEnterPip(this) + thisTransition(this) + defaultTeardown(this) + } + + /** Defines the default setup steps required by the test */ + protected open val defaultSetup: FlickerBuilder.() -> Unit = { + setup { + setRotation(Rotation.ROTATION_0) + removeAllTasksButHome() + } + } + + /** Defines the default method of entering PiP */ + protected open val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + pipApp.launchViaIntentAndWaitForPip(wmHelper, + stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true")) + } + } + + /** Defines the default teardown required to clean up after the test */ + protected open val defaultTeardown: FlickerBuilder.() -> Unit = { + teardown { + pipApp.exit(wmHelper) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt index 3850c1f6c89a..c618e5a24fdf 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt @@ -50,41 +50,41 @@ open class SetRequestedOrientationWhilePinned(flicker: FlickerTest) : PipTransit private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) - /** {@inheritDoc} */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - // Launch the PiP activity fixed as landscape. - pipApp.launchViaIntent( + override val thisTransition: FlickerBuilder.() -> Unit = { + transitions { + // Launch the activity back into fullscreen and ensure that it is now in landscape + pipApp.launchViaIntent(wmHelper) + // System bar may fade out during fixed rotation. + wmHelper + .StateSyncBuilder() + .withFullScreenApp(pipApp) + .withRotation(Rotation.ROTATION_90) + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() + } + } + + override val defaultEnterPip: FlickerBuilder.() -> Unit = { + setup { + // Launch the PiP activity fixed as landscape. + pipApp.launchViaIntent( wmHelper, stringExtras = - mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()) - ) - // Enter PiP. - broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP) - // System bar may fade out during fixed rotation. - wmHelper + mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()) + ) + // Enter PiP. + broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP) + // System bar may fade out during fixed rotation. + wmHelper .StateSyncBuilder() .withPipShown() .withRotation(Rotation.ROTATION_0) .withNavOrTaskBarVisible() .withStatusBarVisible() .waitForAndVerify() - } - teardown { pipApp.exit(wmHelper) } - transitions { - // Launch the activity back into fullscreen and ensure that it is now in landscape - pipApp.launchViaIntent(wmHelper) - // System bar may fade out during fixed rotation. - wmHelper - .StateSyncBuilder() - .withFullScreenApp(pipApp) - .withRotation(Rotation.ROTATION_90) - .withNavOrTaskBarVisible() - .withStatusBarVisible() - .waitForAndVerify() - } } + } /** * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt index 703784dd8c67..43d6c8f26126 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt @@ -63,14 +63,13 @@ open class ShowPipAndRotateDisplay(flicker: FlickerTest) : PipTransition(flicker private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition { - setup { - testApp.launchViaIntent(wmHelper) - setRotation(flicker.scenario.startRotation) - } - transitions { setRotation(flicker.scenario.endRotation) } + override val thisTransition: FlickerBuilder.() -> Unit = { + setup { + testApp.launchViaIntent(wmHelper) + setRotation(flicker.scenario.startRotation) } + transitions { setRotation(flicker.scenario.endRotation) } + } /** Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 666ef5515f21..dba21b8dd3f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -46,11 +46,13 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask +import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -78,6 +80,7 @@ import org.mockito.Mockito.`when` as whenever class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var shellCommandHandler: ShellCommandHandler @Mock lateinit var shellController: ShellController @Mock lateinit var displayController: DisplayController @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @@ -86,12 +89,14 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var transitions: Transitions @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler + @Mock lateinit var launchAdjacentController: LaunchAdjacentController private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private val shellExecutor = TestShellExecutor() // Mock running tasks are registered here so we can get the list from mock shell task organizer private val runningTasks = mutableListOf<RunningTaskInfo>() @@ -115,6 +120,7 @@ class DesktopTasksControllerTest : ShellTestCase() { return DesktopTasksController( context, shellInit, + shellCommandHandler, shellController, displayController, shellTaskOrganizer, @@ -124,7 +130,8 @@ class DesktopTasksControllerTest : ShellTestCase() { enterDesktopTransitionHandler, exitDesktopTransitionHandler, desktopModeTaskRepository, - TestShellExecutor() + launchAdjacentController, + shellExecutor ) } @@ -348,7 +355,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveTaskToFront_postsWctWithReorderOp() { val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() + setUpFreeformTask() controller.moveTaskToFront(task1) @@ -611,6 +618,27 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue() } + @Test + fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { + val task = setUpFreeformTask() + clearInvocations(launchAdjacentController) + + markTaskVisible(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = false + } + + @Test + fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { + val task = setUpFreeformTask() + markTaskVisible(task) + clearInvocations(launchAdjacentController) + + markTaskHidden(task) + shellExecutor.flushAll() + verify(launchAdjacentController).launchAdjacentEnabled = true + } + private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFreeformTask(displayId) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index ada3455fae18..1dfdbf6514ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -29,6 +29,7 @@ import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.MotionEvent; +import android.view.ViewConfiguration; import androidx.test.filters.SmallTest; @@ -90,6 +91,8 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { private PipDisplayLayoutState mPipDisplayLayoutState; + private PipTouchState mPipTouchState; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -104,8 +107,12 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator); + + mPipTouchState = new PipTouchState(ViewConfiguration.get(mContext), + () -> {}, () -> {}, mMainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(mContext, pipBoundsAlgorithm, - mPipBoundsState, motionHelper, mPipTaskOrganizer, mPipDismissTargetHandler, + mPipBoundsState, motionHelper, mPipTouchState, mPipTaskOrganizer, + mPipDismissTargetHandler, (Rect bounds) -> new Rect(), () -> {}, mPipUiEventLogger, mPhonePipMenuController, mMainExecutor) { @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index fb17d8799bda..981035087e24 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -61,6 +61,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -102,6 +103,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock IconProvider mIconProvider; @Mock StageCoordinator mStageCoordinator; @Mock RecentTasksController mRecentTasks; + @Mock LaunchAdjacentController mLaunchAdjacentController; @Captor ArgumentCaptor<Intent> mIntentCaptor; private ShellController mShellController; @@ -117,7 +119,8 @@ public class SplitScreenControllerTests extends ShellTestCase { mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, - mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator)); + mIconProvider, mRecentTasks, mLaunchAdjacentController, mMainExecutor, + mStageCoordinator)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index 4e446c684d86..ff6f59d8973c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -31,6 +31,7 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -75,11 +76,12 @@ public class SplitTestUtils { DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks) { + ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, + LaunchAdjacentController launchAdjacentController) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, mainExecutor, recentTasks); + transitions, transactionPool, mainExecutor, recentTasks, + launchAdjacentController); // Prepare root task for testing. mRootTask = new TestRunningTaskInfoBuilder().build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 3b05651f884b..0095f65b410a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -70,6 +70,7 @@ import com.android.wm.shell.TransitionInfoBuilder; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -101,6 +102,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private SurfaceSession mSurfaceSession; @Mock private IconProvider mIconProvider; @Mock private ShellExecutor mMainExecutor; + @Mock private LaunchAdjacentController mLaunchAdjacentController; private SplitLayout mSplitLayout; private MainStage mMainStage; private SideStage mSideStage; @@ -130,7 +132,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mMainExecutor, Optional.empty()); + mTransactionPool, mMainExecutor, Optional.empty(), + mLaunchAdjacentController); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 66b6c62f1dd6..91ae1ef8603e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -65,6 +65,7 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -107,6 +108,8 @@ public class StageCoordinatorTests extends ShellTestCase { private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; + @Mock + private LaunchAdjacentController mLaunchAdjacentController; private final Rect mBounds1 = new Rect(10, 20, 30, 40); private final Rect mBounds2 = new Rect(5, 10, 15, 20); @@ -130,7 +133,7 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, Optional.empty())); + mMainExecutor, Optional.empty(), mLaunchAdjacentController)); mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build(); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); diff --git a/native/webview/OWNERS b/native/webview/OWNERS index 580bb0fe8273..7e27dc8831a6 100644 --- a/native/webview/OWNERS +++ b/native/webview/OWNERS @@ -1,4 +1,4 @@ +# Bug component: 76427 boliu@google.com -changwan@google.com -tobiasjs@google.com +ntfschr@google.com torne@google.com diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 539857951ab1..7a6fad4b6d51 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -20,7 +20,7 @@ <string name="app_label">Companion Device Manager</string> <!-- Title of the device association confirmation dialog. --> - <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong>?</string> + <string name="confirmation_title">Allow the app <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong>?</string> <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index cf962d1d94aa..bce86c477e77 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -118,8 +118,10 @@ class CredentialSelectorViewModel( if (entry != null && entry.pendingIntent != null) { Log.d(Constants.LOG_TAG, "Launching provider activity") uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) + val entryIntent = entry.fillInIntent + entryIntent?.putExtra(Constants.IS_AUTO_SELECTED_KEY, uiState.isAutoSelectFlow) val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) - .setFillInIntent(entry.fillInIntent).build() + .setFillInIntent(entryIntent).build() try { launcher.launch(intentSenderRequest) } catch (e: Exception) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt index c6dc5945d886..51ca5971cec4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt @@ -21,5 +21,6 @@ class Constants { const val LOG_TAG = "CredentialSelector" const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS = "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED" + const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED" } -}
\ No newline at end of file +} diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java index 738354f78e30..e1eb36ac276c 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java @@ -16,11 +16,10 @@ package com.android.dynsystem; -import static android.os.AsyncTask.Status.FINISHED; -import static android.os.AsyncTask.Status.PENDING; import static android.os.AsyncTask.Status.RUNNING; import static android.os.image.DynamicSystemClient.ACTION_HIDE_NOTIFICATION; import static android.os.image.DynamicSystemClient.ACTION_NOTIFY_IF_IN_USE; +import static android.os.image.DynamicSystemClient.ACTION_NOTIFY_KEYGUARD_DISMISSED; import static android.os.image.DynamicSystemClient.ACTION_START_INSTALL; import static android.os.image.DynamicSystemClient.CAUSE_ERROR_EXCEPTION; import static android.os.image.DynamicSystemClient.CAUSE_ERROR_INVALID_URL; @@ -234,6 +233,8 @@ public class DynamicSystemInstallationService extends Service executeNotifyIfInUseCommand(); } else if (ACTION_HIDE_NOTIFICATION.equals(action)) { executeHideNotificationCommand(); + } else if (ACTION_NOTIFY_KEYGUARD_DISMISSED.equals(action)) { + executeNotifyKeyguardDismissed(); } return Service.START_NOT_STICKY; @@ -477,6 +478,10 @@ public class DynamicSystemInstallationService extends Service } } + private void executeNotifyKeyguardDismissed() { + postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); + } + private void resetTaskAndStop() { resetTaskAndStop(/* removeNotification= */ false); } diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java index b52272961e4b..7401691cd59e 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/VerificationActivity.java @@ -16,6 +16,7 @@ package com.android.dynsystem; +import static android.os.image.DynamicSystemClient.ACTION_NOTIFY_KEYGUARD_DISMISSED; import static android.os.image.DynamicSystemClient.KEY_KEYGUARD_USE_DEFAULT_STRINGS; import android.app.Activity; @@ -83,11 +84,19 @@ public class VerificationActivity extends Activity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { startInstallationService(); + } else { + notifyKeyguardDismissed(); } finish(); } + private void notifyKeyguardDismissed() { + Intent intent = new Intent(this, DynamicSystemInstallationService.class); + intent.setAction(ACTION_NOTIFY_KEYGUARD_DISMISSED); + startServiceAsUser(intent, UserHandle.SYSTEM); + } + private void startInstallationService() { // retrieve data from calling intent Intent callingIntent = getIntent(); diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index a2118fac4231..c52fde6364a0 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -38,7 +38,11 @@ <!-- Message for updating an existing app [CHAR LIMIT=NONE] --> <string name="install_confirm_question_update">Do you want to update this app?</string> <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] --> - <string name="install_confirm_question_update_owner_reminder">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your phone. App functionality may change.</string> + <string name="install_confirm_question_update_owner_reminder" product="tablet">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your tablet. App functionality may change.</string> + <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_reminder" product="tv">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your TV. App functionality may change.</string> + <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_reminder" product="default">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your phone. App functionality may change.</string> <!-- [CHAR LIMIT=100] --> <string name="install_failed">App not installed.</string> <!-- Reason displayed when installation fails because the package was blocked diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java index 7b17cbdd3a1e..19d74b33e034 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java @@ -16,6 +16,8 @@ package com.android.packageinstaller; +import static android.content.Intent.CATEGORY_LAUNCHER; + import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; import android.app.Activity; @@ -45,6 +47,9 @@ public class DeleteStagedFileOnResult extends Activity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { setResult(resultCode, data); finish(); + if (data != null && data.hasCategory(CATEGORY_LAUNCHER)) { + startActivity(data); + } } @Override diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java index 73c03a57cade..ff991d2f7ee3 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java @@ -17,7 +17,6 @@ package com.android.packageinstaller; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -123,11 +122,7 @@ public class InstallSuccess extends AlertActivity { Button launchButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); if (enabled) { launchButton.setOnClickListener(view -> { - try { - startActivity(mLaunchIntent); - } catch (ActivityNotFoundException | SecurityException e) { - Log.e(LOG_TAG, "Could not start activity", e); - } + setResult(Activity.RESULT_OK, mLaunchIntent); finish(); }); } else { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index d1541569bc55..e071c111d617 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -16,8 +16,9 @@ */ package com.android.packageinstaller; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY; -import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT; +import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.Manifest; @@ -789,7 +790,8 @@ public class PackageInstallerActivity extends AlertActivity { } new Handler(Looper.getMainLooper()).postDelayed(() -> { if (!isDestroyed()) { - startActivity(getIntent().addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT)); + startActivity(getIntent().addFlags( + FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP)); } }, 500); diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 750c156f95bd..c244ca0505ed 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -69,9 +69,6 @@ android_library { "src/**/*.java", "src/**/*.kt", ], - - min_sdk_version: "30", - } // NOTE: Keep this module in sync with ./common.mk diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 89bfa0eb646b..030b70a75a8b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -31,6 +31,8 @@ import com.android.settingslib.spaprivileged.template.common.UserProfilePager /** * The full screen template for an App List page. * + * @param noMoreOptions default false. If true, then do not display more options action button, + * including the "Show System" / "Hide System" action. * @param header the description header appears before all the applications. */ @Composable @@ -38,6 +40,7 @@ fun <T : AppRecord> AppListPage( title: String, listModel: AppListModel<T>, showInstantApps: Boolean = false, + noMoreOptions: Boolean = false, matchAnyUserForAdmin: Boolean = false, primaryUserOnly: Boolean = false, noItemMessage: String? = null, @@ -49,9 +52,11 @@ fun <T : AppRecord> AppListPage( SearchScaffold( title = title, actions = { - MoreOptionsAction { - ShowSystemAction(showSystem.value) { showSystem.value = it } - moreOptions() + if (!noMoreOptions) { + MoreOptionsAction { + ShowSystemAction(showSystem.value) { showSystem.value = it } + moreOptions() + } } }, ) { bottomPadding, searchQuery -> diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt index 06003c0cb8f9..f6f48891030a 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt @@ -63,9 +63,7 @@ class AppListPageTest { fun canShowSystem() { val inputState by setContent() - composeTestRule.onNodeWithContentDescription( - context.getString(R.string.abc_action_menu_overflow_description) - ).performClick() + onMoreOptions().performClick() composeTestRule.onNodeWithText(context.getString(R.string.menu_show_system)).performClick() val state = inputState!!.state @@ -75,20 +73,32 @@ class AppListPageTest { @Test fun afterShowSystem_displayHideSystem() { setContent() - composeTestRule.onNodeWithContentDescription( - context.getString(R.string.abc_action_menu_overflow_description) - ).performClick() + onMoreOptions().performClick() composeTestRule.onNodeWithText(context.getString(R.string.menu_show_system)).performClick() - composeTestRule.onNodeWithContentDescription( - context.getString(R.string.abc_action_menu_overflow_description) - ).performClick() + onMoreOptions().performClick() composeTestRule.onNodeWithText(context.getString(R.string.menu_hide_system)) .assertIsDisplayed() } + @Test + fun noMoreOptions_notDisplayMoreOptions() { + setContent(noMoreOptions = true) + + onMoreOptions().assertDoesNotExist() + } + + @Test + fun noMoreOptions_showSystemIsFalse() { + val inputState by setContent(noMoreOptions = true) + + val state = inputState!!.state + assertThat(state.showSystem.value).isFalse() + } + private fun setContent( + noMoreOptions: Boolean = false, header: @Composable () -> Unit = {}, ): State<AppListInput<TestAppRecord>?> { val appListState = mutableStateOf<AppListInput<TestAppRecord>?>(null) @@ -96,6 +106,7 @@ class AppListPageTest { AppListPage( title = TITLE, listModel = TestAppListModel(), + noMoreOptions = noMoreOptions, header = header, appList = { appListState.value = this }, ) @@ -103,6 +114,11 @@ class AppListPageTest { return appListState } + private fun onMoreOptions() = + composeTestRule.onNodeWithContentDescription( + context.getString(R.string.abc_action_menu_overflow_description) + ) + private companion object { const val TITLE = "Title" } diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index dac7f8d15388..9f884b277b5b 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -241,12 +241,12 @@ <!-- Bluetooth settings. Similar to bluetooth_profile_a2dp_high_quality, but used when the device supports high quality audio but we don't know which codec that will be used. --> <string name="bluetooth_profile_a2dp_high_quality_unknown_codec">HD audio</string> - <!-- Bluetooth settings. The user-visible string that is used whenever referring to the Hearing Aid profile. --> - <string name="bluetooth_profile_hearing_aid">Hearing Aids</string> + <!-- Bluetooth settings. The user-visible string that is used whenever referring to the Hearing aid profile. --> + <string name="bluetooth_profile_hearing_aid">Hearing aids</string> <!-- Bluetooth settings. The user-visible string that is used whenever referring to the LE audio profile. --> <string name="bluetooth_profile_le_audio">LE Audio</string> - <!-- Bluetooth settings. Connection options screen. The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. --> - <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string> + <!-- Bluetooth settings. Connection options screen. The summary for the Hearing aid checkbox preference when hearing aid is connected. --> + <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to hearing aids</string> <!-- Bluetooth settings. Connection options screen. The summary for the LE audio checkbox preference when LE audio is connected. --> <string name="bluetooth_le_audio_profile_summary_connected">Connected to LE audio</string> @@ -287,8 +287,8 @@ for the HID checkbox preference that describes how checking it will set the HID profile as preferred. --> <string name="bluetooth_hid_profile_summary_use_for">Use for input</string> - <!-- Bluetooth settings. Connection options screen. The summary for the Hearing Aid checkbox preference that describes how checking it will set the Hearing Aid profile as preferred. --> - <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for Hearing Aids</string> + <!-- Bluetooth settings. Connection options screen. The summary for the Hearing aid checkbox preference that describes how checking it will set the hearing aid related profile as preferred. --> + <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for hearing aids</string> <!-- Bluetooth settings. Connection options screen. The summary for the LE_AUDIO checkbox preference that describes how checking it will set the LE_AUDIO profile as preferred. --> <string name="bluetooth_le_audio_profile_summary_use_for">Use for LE_AUDIO</string> @@ -827,6 +827,11 @@ <!-- UI debug setting: show touches location summary [CHAR LIMIT=50] --> <string name="show_touches_summary">Show visual feedback for taps</string> + <!-- UI debug setting: show key presses? [CHAR LIMIT=25] --> + <string name="show_key_presses">Show key presses</string> + <!-- UI debug setting: show physical key presses summary [CHAR LIMIT=50] --> + <string name="show_key_presses_summary">Show visual feedback for physical key presses</string> + <!-- UI debug setting: show where surface updates happen? [CHAR LIMIT=25] --> <string name="show_screen_updates">Show surface updates</string> <!-- UI debug setting: show surface updates summary [CHAR LIMIT=50] --> diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp index cfff519705f2..918d696fa481 100644 --- a/packages/SettingsLib/search/Android.bp +++ b/packages/SettingsLib/search/Android.bp @@ -7,8 +7,18 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +java_library { + name: "SettingsLib-search-interface", + visibility: ["//visibility:private"], + srcs: ["interface-src/**/*.java"], + host_supported: true, +} + android_library { name: "SettingsLib-search", + static_libs: [ + "SettingsLib-search-interface", + ], srcs: ["src/**/*.java"], sdk_version: "system_current", @@ -19,12 +29,10 @@ java_plugin { name: "SettingsLib-annotation-processor", processor_class: "com.android.settingslib.search.IndexableProcessor", static_libs: [ + "SettingsLib-search-interface", "javapoet", ], - srcs: [ - "processor-src/**/*.java", - "src/com/android/settingslib/search/SearchIndexable.java", - ], + srcs: ["processor-src/**/*.java"], java_resource_dirs: ["resources"], } diff --git a/packages/SettingsLib/search/common.mk b/packages/SettingsLib/search/common.mk deleted file mode 100644 index 05226db5cb91..000000000000 --- a/packages/SettingsLib/search/common.mk +++ /dev/null @@ -1,10 +0,0 @@ -# Include this file to generate SearchIndexableResourcesImpl - -LOCAL_ANNOTATION_PROCESSORS += \ - SettingsLib-annotation-processor - -LOCAL_ANNOTATION_PROCESSOR_CLASSES += \ - com.android.settingslib.search.IndexableProcessor - -LOCAL_STATIC_JAVA_LIBRARIES += \ - SettingsLib-search diff --git a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexable.java b/packages/SettingsLib/search/interface-src/com/android/settingslib/search/SearchIndexable.java index 638fa3e98138..174f33709b02 100644 --- a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexable.java +++ b/packages/SettingsLib/search/interface-src/com/android/settingslib/search/SearchIndexable.java @@ -27,7 +27,7 @@ public @interface SearchIndexable { /** * Bitfield for the form factors this class should be considered indexable for. * Default is {@link #ALL}. - * + * <p> * TODO: actually use this value somehow */ int forTarget() default ALL; @@ -35,27 +35,27 @@ public @interface SearchIndexable { /** * Indicates that the class should be considered indexable for Mobile. */ - int MOBILE = 1<<0; + int MOBILE = 1 << 0; /** * Indicates that the class should be considered indexable for TV. */ - int TV = 1<<1; + int TV = 1 << 1; /** * Indicates that the class should be considered indexable for Wear. */ - int WEAR = 1<<2; + int WEAR = 1 << 2; /** * Indicates that the class should be considered indexable for Auto. */ - int AUTO = 1<<3; + int AUTO = 1 << 3; /** * Indicates that the class should be considered indexable for ARC++. */ - int ARC = 1<<4; + int ARC = 1 << 4; /** * Indicates that the class should be considered indexable for all targets. diff --git a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java index e92157e7c867..fa43915deb6a 100644 --- a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java +++ b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java @@ -34,6 +34,7 @@ import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; @@ -48,10 +49,11 @@ import javax.tools.Diagnostic.Kind; * subclasses. */ @SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions(IndexableProcessor.PACKAGE_KEY) @SupportedAnnotationTypes({"com.android.settingslib.search.SearchIndexable"}) public class IndexableProcessor extends AbstractProcessor { - private static final String PACKAGE = "com.android.settingslib.search"; + private static final String SETTINGSLIB_SEARCH_PACKAGE = "com.android.settingslib.search"; private static final String CLASS_BASE = "SearchIndexableResourcesBase"; private static final String CLASS_MOBILE = "SearchIndexableResourcesMobile"; private static final String CLASS_TV = "SearchIndexableResourcesTv"; @@ -59,6 +61,9 @@ public class IndexableProcessor extends AbstractProcessor { private static final String CLASS_AUTO = "SearchIndexableResourcesAuto"; private static final String CLASS_ARC = "SearchIndexableResourcesArc"; + static final String PACKAGE_KEY = "com.android.settingslib.search.processor.package"; + + private String mPackage; private Filer mFiler; private Messager mMessager; private boolean mRanOnce; @@ -72,7 +77,8 @@ public class IndexableProcessor extends AbstractProcessor { } mRanOnce = true; - final ClassName searchIndexableData = ClassName.get(PACKAGE, "SearchIndexableData"); + final ClassName searchIndexableData = + ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableData"); final FieldSpec providers = FieldSpec.builder( ParameterizedTypeName.get( @@ -130,7 +136,7 @@ public class IndexableProcessor extends AbstractProcessor { builder = arcConstructorBuilder; } builder.addCode( - "$N(new SearchIndexableData($L.class, $L" + "$N(new com.android.settingslib.search.SearchIndexableData($L.class, $L" + ".SEARCH_INDEX_DATA_PROVIDER));\n", addIndex, className, className); } else { @@ -150,50 +156,51 @@ public class IndexableProcessor extends AbstractProcessor { final TypeSpec baseClass = TypeSpec.classBuilder(CLASS_BASE) .addModifiers(Modifier.PUBLIC) - .addSuperinterface(ClassName.get(PACKAGE, "SearchIndexableResources")) + .addSuperinterface( + ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableResources")) .addField(providers) .addMethod(baseConstructorBuilder.build()) .addMethod(addIndex) .addMethod(getProviderValues) .build(); - final JavaFile searchIndexableResourcesBase = JavaFile.builder(PACKAGE, baseClass).build(); + final JavaFile searchIndexableResourcesBase = JavaFile.builder(mPackage, baseClass).build(); - final JavaFile searchIndexableResourcesMobile = JavaFile.builder(PACKAGE, + final JavaFile searchIndexableResourcesMobile = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_MOBILE) .addModifiers(Modifier.PUBLIC) - .superclass(ClassName.get(PACKAGE, baseClass.name)) + .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(mobileConstructorBuilder.build()) .build()) .build(); - final JavaFile searchIndexableResourcesTv = JavaFile.builder(PACKAGE, + final JavaFile searchIndexableResourcesTv = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_TV) .addModifiers(Modifier.PUBLIC) - .superclass(ClassName.get(PACKAGE, baseClass.name)) + .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(tvConstructorBuilder.build()) .build()) .build(); - final JavaFile searchIndexableResourcesWear = JavaFile.builder(PACKAGE, + final JavaFile searchIndexableResourcesWear = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_WEAR) .addModifiers(Modifier.PUBLIC) - .superclass(ClassName.get(PACKAGE, baseClass.name)) + .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(wearConstructorBuilder.build()) .build()) .build(); - final JavaFile searchIndexableResourcesAuto = JavaFile.builder(PACKAGE, + final JavaFile searchIndexableResourcesAuto = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_AUTO) .addModifiers(Modifier.PUBLIC) - .superclass(ClassName.get(PACKAGE, baseClass.name)) + .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(autoConstructorBuilder.build()) .build()) .build(); - final JavaFile searchIndexableResourcesArc = JavaFile.builder(PACKAGE, + final JavaFile searchIndexableResourcesArc = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_ARC) .addModifiers(Modifier.PUBLIC) - .superclass(ClassName.get(PACKAGE, baseClass.name)) + .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(arcConstructorBuilder.build()) .build()) .build(); @@ -214,6 +221,8 @@ public class IndexableProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); + mPackage = processingEnvironment.getOptions() + .getOrDefault(PACKAGE_KEY, SETTINGSLIB_SEARCH_PACKAGE); mFiler = processingEnvironment.getFiler(); mMessager = processingEnvironment.getMessager(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 8d4aa9a7b25e..c967b568042c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -688,8 +688,12 @@ public class Utils { continue; } for (int complianceWarningType : complianceWarnings) { - if (complianceWarningType != 0) { - return true; + switch (complianceWarningType) { + case UsbPortStatus.COMPLIANCE_WARNING_OTHER: + case UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY: + return true; + default: + break; } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index fe8988385453..6eb2f3834858 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -484,8 +484,9 @@ public class ApplicationsState { if (DEBUG_LOCKING) Log.v(TAG, "getEntry about to acquire lock..."); synchronized (mEntriesMap) { AppEntry entry = null; - if (mEntriesMap.contains(userId)) { - entry = mEntriesMap.get(userId).get(packageName); + HashMap<String, AppEntry> userEntriesMap = mEntriesMap.get(userId); + if (userEntriesMap != null) { + entry = userEntriesMap.get(packageName); } if (entry == null) { ApplicationInfo info = getAppInfoLocked(packageName, userId); @@ -735,8 +736,9 @@ public class ApplicationsState { private AppEntry getEntryLocked(ApplicationInfo info) { int userId = UserHandle.getUserId(info.uid); AppEntry entry = null; - if (mEntriesMap.contains(userId)) { - entry = mEntriesMap.get(userId).get(info.packageName); + HashMap<String, AppEntry> userEntriesMap = mEntriesMap.get(userId); + if (userEntriesMap != null) { + entry = userEntriesMap.get(info.packageName); } if (DEBUG) { Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry); @@ -752,8 +754,11 @@ public class ApplicationsState { Log.i(TAG, "Creating AppEntry for " + info.packageName); } entry = new AppEntry(mContext, info, mCurId++); - mEntriesMap.get(userId).put(info.packageName, entry); - mAppEntries.add(entry); + userEntriesMap = mEntriesMap.get(userId); + if (userEntriesMap != null) { + userEntriesMap.put(info.packageName, entry); + mAppEntries.add(entry); + } } else if (entry.info != info) { entry.info = info; } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 8aacd4d6da54..0bd9384c3ef4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -40,6 +40,7 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TargetApi; import android.app.Notification; @@ -56,7 +57,6 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.DoNotInline; -import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.android.internal.annotations.VisibleForTesting; @@ -70,29 +70,17 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; -/** - * InfoMediaManager provide interface to get InfoMediaDevice list. - */ +/** InfoMediaManager provide interface to get InfoMediaDevice list. */ @RequiresApi(Build.VERSION_CODES.R) -public class InfoMediaManager extends MediaManager { +public abstract class InfoMediaManager extends MediaManager { private static final String TAG = "InfoMediaManager"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - @VisibleForTesting - final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); - @VisibleForTesting - final Executor mExecutor = Executors.newSingleThreadExecutor(); - @VisibleForTesting - MediaRouter2Manager mRouterManager; - @VisibleForTesting - String mPackageName; + protected String mPackageName; private MediaDevice mCurrentConnectedDevice; - private LocalBluetoothManager mBluetoothManager; + private final LocalBluetoothManager mBluetoothManager; private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = new ConcurrentHashMap<>(); @@ -100,7 +88,6 @@ public class InfoMediaManager extends MediaManager { LocalBluetoothManager localBluetoothManager) { super(context, notification); - mRouterManager = MediaRouter2Manager.getInstance(context); mBluetoothManager = localBluetoothManager; if (!TextUtils.isEmpty(packageName)) { mPackageName = packageName; @@ -110,25 +97,87 @@ public class InfoMediaManager extends MediaManager { @Override public void startScan() { mMediaDevices.clear(); - mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); - mRouterManager.registerScanRequest(); + startScanOnRouter(); + updateRouteListingPreference(); + refreshDevices(); + } + + private void updateRouteListingPreference() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !TextUtils.isEmpty(mPackageName)) { RouteListingPreference routeListingPreference = - mRouterManager.getRouteListingPreference(mPackageName); - if (routeListingPreference != null) { - Api34Impl.onRouteListingPreferenceUpdated(null, routeListingPreference, - mPreferenceItemMap); - } + getRouteListingPreference(); + Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, + mPreferenceItemMap); } - refreshDevices(); } @Override - public void stopScan() { - mRouterManager.unregisterCallback(mMediaRouterCallback); - mRouterManager.unregisterScanRequest(); - } + public abstract void stopScan(); + + protected abstract void startScanOnRouter(); + + /** + * Transfer MediaDevice for media without package name. + */ + protected abstract boolean connectDeviceWithoutPackageName(@NonNull MediaDevice device); + + protected abstract void selectRoute( + @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info); + + protected abstract void deselectRoute( + @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info); + + protected abstract void releaseSession(@NonNull RoutingSessionInfo sessionInfo); + + @NonNull + protected abstract List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info); + + @NonNull + protected abstract List<MediaRoute2Info> getDeselectableRoutes( + @NonNull RoutingSessionInfo info); + + @NonNull + protected abstract List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info); + + protected abstract void setSessionVolume(@NonNull RoutingSessionInfo info, int volume); + + @Nullable + protected abstract RouteListingPreference getRouteListingPreference(); + + /** + * Returns the list of currently active {@link RoutingSessionInfo routing sessions} known to the + * system. + */ + @NonNull + protected abstract List<RoutingSessionInfo> getActiveRoutingSessions(); + + @NonNull + protected abstract List<RoutingSessionInfo> getRoutingSessionsForPackage(); + + @NonNull + protected abstract List<MediaRoute2Info> getAllRoutes(); + + @NonNull + protected abstract List<MediaRoute2Info> getAvailableRoutesFromRouter(); + + @NonNull + protected abstract List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName); + + @NonNull + protected abstract ComplexMediaDevice createComplexMediaDevice( + MediaRoute2Info route, RouteListingPreference.Item routeListingPreferenceItem); + + @NonNull + protected abstract InfoMediaDevice createInfoMediaDevice( + MediaRoute2Info route, RouteListingPreference.Item routeListingPreferenceItem); + + @NonNull + protected abstract PhoneMediaDevice createPhoneMediaDevice(MediaRoute2Info route); + + @NonNull + protected abstract BluetoothMediaDevice createBluetoothMediaDevice( + MediaRoute2Info route, CachedBluetoothDevice cachedDevice); /** * Get current device that played media. @@ -139,18 +188,6 @@ public class InfoMediaManager extends MediaManager { } /** - * Transfer MediaDevice for media without package name. - */ - boolean connectDeviceWithoutPackageName(MediaDevice device) { - final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null); - if (info != null) { - mRouterManager.transfer(info, device.mRouteInfo); - return true; - } - return false; - } - - /** * Add a MediaDevice to let it play current media. * * @param device MediaDevice @@ -169,26 +206,21 @@ public class InfoMediaManager extends MediaManager { return false; } - mRouterManager.selectRoute(info, device.mRouteInfo); + selectRoute(device.mRouteInfo, info); return true; } private RoutingSessionInfo getRoutingSessionInfo() { - return getRoutingSessionInfo(mPackageName); - } - - private RoutingSessionInfo getRoutingSessionInfo(String packageName) { - final List<RoutingSessionInfo> sessionInfos = - mRouterManager.getRoutingSessions(packageName); + final List<RoutingSessionInfo> sessionInfos = getRoutingSessionsForPackage(); - if (sessionInfos == null || sessionInfos.isEmpty()) { + if (sessionInfos.isEmpty()) { return null; } return sessionInfos.get(sessionInfos.size() - 1); } boolean isRoutingSessionAvailableForVolumeControl() { - List<RoutingSessionInfo> sessions = mRouterManager.getRoutingSessions(mPackageName); + List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage(); for (RoutingSessionInfo session : sessions) { if (!session.isSystemSession() @@ -203,15 +235,17 @@ public class InfoMediaManager extends MediaManager { boolean preferRouteListingOrdering() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE - && Api34Impl.preferRouteListingOrdering(mRouterManager, mPackageName); + && !TextUtils.isEmpty(mPackageName) + && Api34Impl.preferRouteListingOrdering(getRouteListingPreference()); } @Nullable ComponentName getLinkedItemComponentName() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && TextUtils.isEmpty( + mPackageName)) { return null; } - return Api34Impl.getLinkedItemComponentName(mRouterManager, mPackageName); + return Api34Impl.getLinkedItemComponentName(getRouteListingPreference()); } /** @@ -233,7 +267,7 @@ public class InfoMediaManager extends MediaManager { return false; } - mRouterManager.deselectRoute(info, device.mRouteInfo); + deselectRoute(device.mRouteInfo, info); return true; } @@ -252,11 +286,10 @@ public class InfoMediaManager extends MediaManager { return false; } - mRouterManager.releaseSession(sessionInfo); + releaseSession(sessionInfo); return true; } - /** * Returns the list of {@link MediaDevice media devices} that can be added to the current {@link * RoutingSessionInfo routing session}. @@ -276,9 +309,9 @@ public class InfoMediaManager extends MediaManager { } final List<MediaDevice> deviceList = new ArrayList<>(); - for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) { - deviceList.add(new InfoMediaDevice(mContext, mRouterManager, - route, mPackageName, mPreferenceItemMap.get(route.getId()))); + for (MediaRoute2Info route : getSelectableRoutes(info)) { + deviceList.add( + createInfoMediaDevice(route, mPreferenceItemMap.get(route.getId()))); } return deviceList; } @@ -302,9 +335,9 @@ public class InfoMediaManager extends MediaManager { } final List<MediaDevice> deviceList = new ArrayList<>(); - for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) { - deviceList.add(new InfoMediaDevice(mContext, mRouterManager, - route, mPackageName, mPreferenceItemMap.get(route.getId()))); + for (MediaRoute2Info route : getDeselectableRoutes(info)) { + deviceList.add( + createInfoMediaDevice(route, mPreferenceItemMap.get(route.getId()))); Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); } return deviceList; @@ -329,9 +362,9 @@ public class InfoMediaManager extends MediaManager { } final List<MediaDevice> deviceList = new ArrayList<>(); - for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) { - deviceList.add(new InfoMediaDevice(mContext, mRouterManager, - route, mPackageName, mPreferenceItemMap.get(route.getId()))); + for (MediaRoute2Info route : getSelectedRoutes(info)) { + deviceList.add( + createInfoMediaDevice(route, mPreferenceItemMap.get(route.getId()))); } return deviceList; } @@ -342,7 +375,7 @@ public class InfoMediaManager extends MediaManager { return; } - mRouterManager.setSessionVolume(info, volume); + setSessionVolume(info, volume); } /** @@ -364,7 +397,7 @@ public class InfoMediaManager extends MediaManager { } Log.d(TAG, "adjustSessionVolume() adjust volume: " + volume + ", with : " + mPackageName); - mRouterManager.setSessionVolume(info, volume); + setSessionVolume(info, volume); } /** @@ -431,7 +464,7 @@ public class InfoMediaManager extends MediaManager { } // Disable when there is no transferable route - return mRouterManager.getTransferableRoutes(packageName).isEmpty(); + return getTransferableRoutes(packageName).isEmpty(); } @TargetApi(Build.VERSION_CODES.R) @@ -454,7 +487,7 @@ public class InfoMediaManager extends MediaManager { // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") private void buildAllRoutes() { - for (MediaRoute2Info route : mRouterManager.getAllRoutes()) { + for (MediaRoute2Info route : getAllRoutes()) { if (DEBUG) { Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : " + route.getVolume() + ", type : " + route.getType()); @@ -465,22 +498,10 @@ public class InfoMediaManager extends MediaManager { } } - /** - * Returns the list of currently active {@link RoutingSessionInfo routing sessions} known to the - * system. - */ - @NonNull - List<RoutingSessionInfo> getActiveRoutingSessions() { - List<RoutingSessionInfo> infos = new ArrayList<>(); - infos.add(mRouterManager.getSystemRoutingSession(null)); - infos.addAll(mRouterManager.getRemoteSessions()); - return infos; - } - // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") private synchronized void buildAvailableRoutes() { - for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) { + for (MediaRoute2Info route : getAvailableRoutes()) { if (DEBUG) { Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " + route.getVolume() + ", type : " + route.getType()); @@ -488,18 +509,17 @@ public class InfoMediaManager extends MediaManager { addMediaDevice(route); } } - - private synchronized List<MediaRoute2Info> getAvailableRoutes(String packageName) { + private synchronized List<MediaRoute2Info> getAvailableRoutes() { List<MediaRoute2Info> infos = new ArrayList<>(); - RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName); + RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(); List<MediaRoute2Info> selectedRouteInfos = new ArrayList<>(); if (routingSessionInfo != null) { - selectedRouteInfos = mRouterManager.getSelectedRoutes(routingSessionInfo); + selectedRouteInfos = getSelectedRoutes(routingSessionInfo); infos.addAll(selectedRouteInfos); - infos.addAll(mRouterManager.getSelectableRoutes(routingSessionInfo)); + infos.addAll(getSelectableRoutes(routingSessionInfo)); } final List<MediaRoute2Info> transferableRoutes = - mRouterManager.getTransferableRoutes(packageName); + getTransferableRoutes(mPackageName); for (MediaRoute2Info transferableRoute : transferableRoutes) { boolean alreadyAdded = false; for (MediaRoute2Info mediaRoute2Info : infos) { @@ -514,14 +534,13 @@ public class InfoMediaManager extends MediaManager { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !TextUtils.isEmpty(mPackageName)) { - RouteListingPreference routeListingPreference = - mRouterManager.getRouteListingPreference(mPackageName); + RouteListingPreference routeListingPreference = getRouteListingPreference(); if (routeListingPreference != null) { final List<RouteListingPreference.Item> preferenceRouteListing = Api34Impl.composePreferenceRouteListing( routeListingPreference); infos = Api34Impl.arrangeRouteListByPreference(selectedRouteInfos, - mRouterManager.getAvailableRoutes(packageName), + getAvailableRoutesFromRouter(), preferenceRouteListing); } return Api34Impl.filterDuplicatedIds(infos); @@ -547,8 +566,8 @@ public class InfoMediaManager extends MediaManager { case TYPE_REMOTE_GAME_CONSOLE: case TYPE_REMOTE_CAR: case TYPE_REMOTE_SMARTWATCH: - mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, - mPackageName, mPreferenceItemMap.get(route.getId())); + mediaDevice = + createInfoMediaDevice(route, mPreferenceItemMap.get(route.getId())); break; case TYPE_BUILTIN_SPEAKER: case TYPE_USB_DEVICE: @@ -558,8 +577,7 @@ public class InfoMediaManager extends MediaManager { case TYPE_HDMI: case TYPE_WIRED_HEADSET: case TYPE_WIRED_HEADPHONES: - mediaDevice = - new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName); + mediaDevice = createPhoneMediaDevice(route); break; case TYPE_HEARING_AID: case TYPE_BLUETOOTH_A2DP: @@ -569,13 +587,13 @@ public class InfoMediaManager extends MediaManager { final CachedBluetoothDevice cachedDevice = mBluetoothManager.getCachedDeviceManager().findDevice(device); if (cachedDevice != null) { - mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, - route, mPackageName); + mediaDevice = createBluetoothMediaDevice(route, cachedDevice); } break; case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: - mediaDevice = new ComplexMediaDevice(mContext, mRouterManager, route, - mPackageName, mPreferenceItemMap.get(route.getId())); + mediaDevice = + createComplexMediaDevice( + route, mPreferenceItemMap.get(route.getId())); default: Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); break; @@ -654,16 +672,16 @@ public class InfoMediaManager extends MediaManager { public void onRouteListingPreferenceUpdated( String packageName, RouteListingPreference routeListingPreference) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Api34Impl.onRouteListingPreferenceUpdated(packageName, routeListingPreference, - mPreferenceItemMap); + if (TextUtils.equals(mPackageName, packageName)) { + Api34Impl.onRouteListingPreferenceUpdated( + routeListingPreference, mPreferenceItemMap); refreshDevices(); } } } @RequiresApi(34) - private static class Api34Impl { + static class Api34Impl { @DoNotInline static List<RouteListingPreference.Item> composePreferenceRouteListing( RouteListingPreference routeListingPreference) { @@ -712,19 +730,13 @@ public class InfoMediaManager extends MediaManager { if (sortedInfoList.size() != infolist.size()) { infolist.removeAll(sortedInfoList); sortedInfoList.addAll(infolist.stream().filter( - MediaRoute2Info::isSystemRoute).collect(Collectors.toList())); + MediaRoute2Info::isSystemRoute).toList()); } return sortedInfoList; } @DoNotInline - static boolean preferRouteListingOrdering(MediaRouter2Manager mediaRouter2Manager, - String packageName) { - if (TextUtils.isEmpty(packageName)) { - return false; - } - RouteListingPreference routeListingPreference = - mediaRouter2Manager.getRouteListingPreference(packageName); + static boolean preferRouteListingOrdering(RouteListingPreference routeListingPreference) { return routeListingPreference != null && !routeListingPreference.getUseSystemOrdering(); } @@ -732,26 +744,19 @@ public class InfoMediaManager extends MediaManager { @DoNotInline @Nullable static ComponentName getLinkedItemComponentName( - MediaRouter2Manager mediaRouter2Manager, String packageName) { - if (TextUtils.isEmpty(packageName)) { - return null; - } - RouteListingPreference routeListingPreference = - mediaRouter2Manager.getRouteListingPreference(packageName); + RouteListingPreference routeListingPreference) { return routeListingPreference == null ? null : routeListingPreference.getLinkedItemComponentName(); } @DoNotInline static void onRouteListingPreferenceUpdated( - String packageName, RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap) { preferenceItemMap.clear(); if (routeListingPreference != null) { - routeListingPreference.getItems().forEach((item) -> { - preferenceItemMap.put(item.getRouteId(), item); - }); + routeListingPreference.getItems().forEach((item) -> + preferenceItemMap.put(item.getRouteId(), item)); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 2ce2636c2440..987f61649ba9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -137,7 +137,8 @@ public class LocalMediaManager implements BluetoothCallback { } mInfoMediaManager = - new InfoMediaManager(context, packageName, notification, mLocalBluetoothManager); + new ManagerInfoMediaManager( + context, packageName, notification, mLocalBluetoothManager); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java new file mode 100644 index 000000000000..4f081369f5bf --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java @@ -0,0 +1,187 @@ +/* + * 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.settingslib.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.content.Context; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; +import android.media.RouteListingPreference; +import android.media.RoutingSessionInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Template implementation of {@link InfoMediaManager} using {@link MediaRouter2Manager}. + */ +public class ManagerInfoMediaManager extends InfoMediaManager { + + @VisibleForTesting + /* package */ final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); + @VisibleForTesting + /* package */ MediaRouter2Manager mRouterManager; + + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + + public ManagerInfoMediaManager( + Context context, + String packageName, + Notification notification, + LocalBluetoothManager localBluetoothManager) { + super(context, packageName, notification, localBluetoothManager); + + mRouterManager = MediaRouter2Manager.getInstance(context); + } + + @Override + protected void startScanOnRouter() { + mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); + mRouterManager.registerScanRequest(); + } + + @Override + public void stopScan() { + mRouterManager.unregisterCallback(mMediaRouterCallback); + mRouterManager.unregisterScanRequest(); + } + + @Override + protected boolean connectDeviceWithoutPackageName(@NonNull MediaDevice device) { + final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null); + if (info != null) { + mRouterManager.transfer(info, device.mRouteInfo); + return true; + } + return false; + } + + @Override + protected void selectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) { + mRouterManager.selectRoute(info, route); + } + + @Override + protected void deselectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) { + mRouterManager.deselectRoute(info, route); + } + + @Override + protected void releaseSession(@NonNull RoutingSessionInfo sessionInfo) { + mRouterManager.releaseSession(sessionInfo); + } + + @Override + @NonNull + protected List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info) { + return mRouterManager.getSelectableRoutes(info); + } + + @Override + @NonNull + protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) { + return mRouterManager.getDeselectableRoutes(info); + } + + @Override + @NonNull + protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) { + return mRouterManager.getSelectedRoutes(info); + } + + @Override + protected void setSessionVolume(@NonNull RoutingSessionInfo info, int volume) { + mRouterManager.setSessionVolume(info, volume); + } + + @Override + @Nullable + protected RouteListingPreference getRouteListingPreference() { + return mRouterManager.getRouteListingPreference(mPackageName); + } + + @Override + @NonNull + protected List<RoutingSessionInfo> getRoutingSessionsForPackage() { + return mRouterManager.getRoutingSessions(mPackageName); + } + + @Override + @NonNull + protected List<RoutingSessionInfo> getActiveRoutingSessions() { + List<RoutingSessionInfo> infos = new ArrayList<>(); + infos.add(mRouterManager.getSystemRoutingSession(null)); + infos.addAll(mRouterManager.getRemoteSessions()); + return infos; + } + + @Override + @NonNull + protected List<MediaRoute2Info> getAllRoutes() { + return mRouterManager.getAllRoutes(); + } + + @Override + @NonNull + protected List<MediaRoute2Info> getAvailableRoutesFromRouter() { + return mRouterManager.getAvailableRoutes(mPackageName); + } + + @Override + @NonNull + protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) { + return mRouterManager.getTransferableRoutes(packageName); + } + + @Override + @NonNull + protected ComplexMediaDevice createComplexMediaDevice( + MediaRoute2Info route, RouteListingPreference.Item routeListingPreferenceItem) { + return new ComplexMediaDevice( + mContext, mRouterManager, route, mPackageName, routeListingPreferenceItem); + } + + @Override + @NonNull + protected InfoMediaDevice createInfoMediaDevice( + MediaRoute2Info route, RouteListingPreference.Item routeListingPreferenceItem) { + return new InfoMediaDevice( + mContext, mRouterManager, route, mPackageName, routeListingPreferenceItem); + } + + @Override + @NonNull + protected PhoneMediaDevice createPhoneMediaDevice(MediaRoute2Info route) { + return new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName); + } + + @Override + @NonNull + protected BluetoothMediaDevice createBluetoothMediaDevice( + MediaRoute2Info route, CachedBluetoothDevice cachedDevice) { + return new BluetoothMediaDevice( + mContext, cachedDevice, mRouterManager, route, mPackageName); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java index 0637e5d27f57..4a913c87bddf 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java @@ -455,12 +455,24 @@ public class UtilsTest { } @Test - public void containsIncompatibleChargers_returnTrue() { - setupIncompatibleCharging(); + public void containsIncompatibleChargers_complianeWarningOther_returnTrue() { + setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_OTHER); + assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isTrue(); + } + + @Test + public void containsIncompatibleChargers_complianeWarningDebug_returnTrue() { + setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY); assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isTrue(); } @Test + public void containsIncompatibleChargers_unexpectedWarningType_returnFalse() { + setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_BC_1_2); + assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isFalse(); + } + + @Test public void containsIncompatibleChargers_emptyComplianceWarnings_returnFalse() { setupIncompatibleCharging(); when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]); @@ -494,12 +506,17 @@ public class UtilsTest { } private void setupIncompatibleCharging() { + setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_OTHER); + } + + private void setupIncompatibleCharging(int complianceWarningType) { final List<UsbPort> usbPorts = new ArrayList<>(); usbPorts.add(mUsbPort); when(mUsbManager.getPorts()).thenReturn(usbPorts); when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus); when(mUsbPort.supportsComplianceWarnings()).thenReturn(true); when(mUsbPortStatus.isConnected()).thenReturn(true); - when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1}); + when(mUsbPortStatus.getComplianceWarnings()) + .thenReturn(new int[]{complianceWarningType}); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index 1d081d7214cc..34d8148f418f 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -804,7 +804,7 @@ public class ApplicationsStateRoboTest { } @Test - public void getEntry_validUserId_shouldReturnEntry() { + public void getEntry_hasCache_shouldReturnCacheEntry() { mApplicationsState.mEntriesMap.put(/* userId= */ 0, new HashMap<>()); addApp(PKG_1, /* id= */ 1); @@ -813,10 +813,13 @@ public class ApplicationsStateRoboTest { } @Test - public void getEntry_invalidUserId_shouldReturnNull() { - mApplicationsState.mEntriesMap.put(/* userId= */ 0, new HashMap<>()); - addApp(PKG_1, /* id= */ 1); + public void getEntry_hasNoCache_shouldReturnEntry() { + mApplicationsState.mEntriesMap.clear(); + ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0); + mApplicationsState.mApplications.add(appInfo); + mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false); - assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ -1)).isNull(); + assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName) + .isEqualTo(PKG_1); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index 4d45e55d41c8..ce1744dd415e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -73,6 +73,7 @@ import java.util.Set; public class InfoMediaManagerTest { private static final String TEST_PACKAGE_NAME = "com.test.packagename"; + private static final String TEST_PACKAGE_NAME_2 = "com.test.packagename2"; private static final String TEST_ID = "test_id"; private static final String TEST_ID_1 = "test_id_1"; private static final String TEST_ID_2 = "test_id_2"; @@ -95,7 +96,7 @@ public class InfoMediaManagerTest { @Mock private ComponentName mComponentName; - private InfoMediaManager mInfoMediaManager; + private ManagerInfoMediaManager mInfoMediaManager; private Context mContext; private ShadowRouter2Manager mShadowRouter2Manager; @@ -107,7 +108,8 @@ public class InfoMediaManagerTest { doReturn(mMediaSessionManager).when(mContext).getSystemService( Context.MEDIA_SESSION_SERVICE); mInfoMediaManager = - new InfoMediaManager(mContext, TEST_PACKAGE_NAME, null, mLocalBluetoothManager); + new ManagerInfoMediaManager( + mContext, TEST_PACKAGE_NAME, null, mLocalBluetoothManager); mShadowRouter2Manager = ShadowRouter2Manager.getShadow(); mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext); } @@ -308,7 +310,54 @@ public class InfoMediaManagerTest { } @Test - public void onRouteChanged_getAvailableRoutesWithPrefernceListExit_ordersRoutes() { + public void onRouteChanged_getAvailableRoutesWithPreferenceListExit_ordersRoutes() { + RouteListingPreference routeListingPreference = setUpPreferenceList(TEST_PACKAGE_NAME); + setUpSelectedRoutes(TEST_PACKAGE_NAME); + + final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); + final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); + routingSessionInfos.add(sessionInfo); + + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); + when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); + + setAvailableRoutesList(TEST_PACKAGE_NAME); + + mInfoMediaManager.mRouterManager = mRouterManager; + mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME, + routeListingPreference); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); + + assertThat(mInfoMediaManager.mMediaDevices).hasSize(3); + assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); + assertThat(mInfoMediaManager.mMediaDevices.get(1).getId()).isEqualTo(TEST_ID_4); + assertThat(mInfoMediaManager.mMediaDevices.get(1).isSuggestedDevice()).isTrue(); + assertThat(mInfoMediaManager.mMediaDevices.get(2).getId()).isEqualTo(TEST_ID_3); + } + + @Test + public void onRouteChanged_preferenceListUpdateWithDifferentPkg_notOrdersRoutes() { + RouteListingPreference routeListingPreference = setUpPreferenceList(TEST_PACKAGE_NAME_2); + setUpSelectedRoutes(TEST_PACKAGE_NAME); + + final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); + final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); + routingSessionInfos.add(sessionInfo); + + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); + when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); + + setAvailableRoutesList(TEST_PACKAGE_NAME); + mInfoMediaManager.mRouterManager = mRouterManager; + mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME_2, + routeListingPreference); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); + + assertThat(mInfoMediaManager.mMediaDevices).hasSize(1); + assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); + } + + private RouteListingPreference setUpPreferenceList(String packageName) { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.UPSIDE_DOWN_CAKE); final List<RouteListingPreference.Item> preferenceItemList = new ArrayList<>(); @@ -324,57 +373,40 @@ public class InfoMediaManagerTest { RouteListingPreference routeListingPreference = new RouteListingPreference.Builder().setItems( preferenceItemList).setUseSystemOrdering(false).build(); - when(mRouterManager.getRouteListingPreference(TEST_PACKAGE_NAME)) + when(mRouterManager.getRouteListingPreference(packageName)) .thenReturn(routeListingPreference); + return routeListingPreference; + } + private void setUpSelectedRoutes(String packageName) { final List<MediaRoute2Info> selectedRoutes = new ArrayList<>(); final MediaRoute2Info info = mock(MediaRoute2Info.class); when(info.getId()).thenReturn(TEST_ID); - when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(info.getClientPackageName()).thenReturn(packageName); when(info.isSystemRoute()).thenReturn(true); selectedRoutes.add(info); when(mRouterManager.getSelectedRoutes(any())).thenReturn(selectedRoutes); - - final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); - final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); - routingSessionInfos.add(sessionInfo); - - when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); - when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); - - setAvailableRoutesList(); - - mInfoMediaManager.mRouterManager = mRouterManager; - mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME, - routeListingPreference); - mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); - - assertThat(mInfoMediaManager.mMediaDevices).hasSize(3); - assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); - assertThat(mInfoMediaManager.mMediaDevices.get(1).getId()).isEqualTo(TEST_ID_4); - assertThat(mInfoMediaManager.mMediaDevices.get(1).isSuggestedDevice()).isTrue(); - assertThat(mInfoMediaManager.mMediaDevices.get(2).getId()).isEqualTo(TEST_ID_3); } - private List<MediaRoute2Info> setAvailableRoutesList() { + private List<MediaRoute2Info> setAvailableRoutesList(String packageName) { final List<MediaRoute2Info> availableRoutes = new ArrayList<>(); final MediaRoute2Info availableInfo1 = mock(MediaRoute2Info.class); when(availableInfo1.getId()).thenReturn(TEST_ID_2); - when(availableInfo1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo1.getClientPackageName()).thenReturn(packageName); when(availableInfo1.getType()).thenReturn(TYPE_REMOTE_TV); availableRoutes.add(availableInfo1); final MediaRoute2Info availableInfo2 = mock(MediaRoute2Info.class); when(availableInfo2.getId()).thenReturn(TEST_ID_3); - when(availableInfo2.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo2.getClientPackageName()).thenReturn(packageName); availableRoutes.add(availableInfo2); final MediaRoute2Info availableInfo3 = mock(MediaRoute2Info.class); when(availableInfo3.getId()).thenReturn(TEST_ID_4); - when(availableInfo3.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo3.getClientPackageName()).thenReturn(packageName); availableRoutes.add(availableInfo3); - when(mRouterManager.getAvailableRoutes(TEST_PACKAGE_NAME)).thenReturn( + when(mRouterManager.getAvailableRoutes(packageName)).thenReturn( availableRoutes); return availableRoutes; diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 476c820a79a7..b7b86ef18299 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -817,7 +817,8 @@ <service android:name=".dreams.DreamOverlayService" android:enabled="false" - android:exported="true" /> + android:exported="true" + android:singleUser="true" /> <activity android:name=".keyguard.WorkLockActivity" android:label="@string/accessibility_desc_work_lock" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt index 83e44b69812b..7e1bfb921ca9 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt @@ -18,6 +18,7 @@ package com.android.systemui.animation import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis +import android.util.Log import android.util.LruCache import android.util.MathUtils import android.util.MathUtils.abs @@ -114,6 +115,9 @@ class FontInterpolator { tmpInterpKey.set(start, end, progress) val cachedFont = interpCache[tmpInterpKey] if (cachedFont != null) { + if (DEBUG) { + Log.d(LOG_TAG, "[$progress] Interp. cache hit for $tmpInterpKey") + } return cachedFont } @@ -159,6 +163,9 @@ class FontInterpolator { val axesCachedFont = verFontCache[tmpVarFontKey] if (axesCachedFont != null) { interpCache.put(InterpKey(start, end, progress), axesCachedFont) + if (DEBUG) { + Log.d(LOG_TAG, "[$progress] Axis cache hit for $tmpVarFontKey") + } return axesCachedFont } @@ -168,6 +175,9 @@ class FontInterpolator { val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build() interpCache.put(InterpKey(start, end, progress), newFont) verFontCache.put(VarFontKey(start, newAxes), newFont) + if (DEBUG) { + Log.d(LOG_TAG, "[$progress] Cache MISS for $tmpInterpKey / $tmpVarFontKey") + } return newFont } @@ -233,6 +243,8 @@ class FontInterpolator { (v.coerceIn(min, max) / step).toInt() * step companion object { + private const val LOG_TAG = "FontInterpolator" + private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) private val EMPTY_AXES = arrayOf<FontVariationAxis>() // Returns true if given two font instance can be interpolated. diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 00108940e6ec..16ddf0c36d9d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -26,6 +26,7 @@ import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis import android.text.Layout import android.util.LruCache +import kotlin.math.roundToInt private const val DEFAULT_ANIMATION_DURATION: Long = 300 private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 @@ -63,9 +64,9 @@ class TypefaceVariantCacheImpl( return it } - return TypefaceVariantCache - .createVariantTypeface(baseTypeface, fvar) - .also { cache.put(fvar, it) } + return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also { + cache.put(fvar, it) + } } } @@ -74,7 +75,6 @@ class TypefaceVariantCacheImpl( * * Currently this class can provide text style animation for text weight and text size. For example * the simple view that draws text with animating text size is like as follows: - * * <pre> <code> * ``` * class SimpleTextAnimation : View { @@ -97,6 +97,7 @@ class TypefaceVariantCacheImpl( */ class TextAnimator( layout: Layout, + numberOfAnimationSteps: Int? = null, // Only do this number of discrete animation steps. private val invalidateCallback: () -> Unit, ) { var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface) @@ -112,7 +113,8 @@ class TextAnimator( ValueAnimator.ofFloat(1f).apply { duration = DEFAULT_ANIMATION_DURATION addUpdateListener { - textInterpolator.progress = it.animatedValue as Float + textInterpolator.progress = + calculateProgress(it.animatedValue as Float, numberOfAnimationSteps) invalidateCallback() } addListener( @@ -123,6 +125,17 @@ class TextAnimator( ) } + private fun calculateProgress(animProgress: Float, numberOfAnimationSteps: Int?): Float { + if (numberOfAnimationSteps != null) { + // This clamps the progress to the nearest value of "numberOfAnimationSteps" + // discrete values between 0 and 1f. + return (animProgress * numberOfAnimationSteps).roundToInt() / + numberOfAnimationSteps.toFloat() + } + + return animProgress + } + sealed class PositionedGlyph { /** Mutable X coordinate of the glyph position relative from drawing offset. */ diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt index 89871fa7d875..2cd587ffbc45 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -56,7 +56,19 @@ data class TurbulenceNoiseAnimationConfig( val easeOutDuration: Float = DEFAULT_EASING_DURATION_IN_MILLIS, val pixelDensity: Float = 1f, val blendMode: BlendMode = DEFAULT_BLEND_MODE, - val onAnimationEnd: Runnable? = null + val onAnimationEnd: Runnable? = null, + /** + * Variants in noise. Higher number means more contrast; lower number means less contrast but + * make the noise dimmed. You may want to increase the [lumaMatteBlendFactor] to compensate. + * Expected range [0, 1]. + */ + val lumaMatteBlendFactor: Float = DEFAULT_LUMA_MATTE_BLEND_FACTOR, + /** + * Offset for the overall brightness in noise. Higher number makes the noise brighter. You may + * want to use this if you have made the noise softer using [lumaMatteBlendFactor]. Expected + * range [0, 1]. + */ + val lumaMatteOverallBrightness: Float = DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS ) { companion object { const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec @@ -66,6 +78,8 @@ data class TurbulenceNoiseAnimationConfig( const val DEFAULT_NOISE_SPEED_Z = 0.3f const val DEFAULT_OPACITY = 150 // full opacity is 255. const val DEFAULT_COLOR = Color.WHITE + const val DEFAULT_LUMA_MATTE_BLEND_FACTOR = 1f + const val DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS = 0f const val DEFAULT_BACKGROUND_COLOR = Color.BLACK val DEFAULT_BLEND_MODE = BlendMode.SRC_OVER } diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt index d1ba7c4de35c..d3c57c91405a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -37,6 +37,8 @@ class TurbulenceNoiseShader(useFractal: Boolean = false) : uniform float in_opacity; uniform float in_pixelDensity; uniform float in_inverseLuma; + uniform half in_lumaMatteBlendFactor; + uniform half in_lumaMatteOverallBrightness; layout(color) uniform vec4 in_color; layout(color) uniform vec4 in_backgroundColor; """ @@ -48,18 +50,21 @@ class TurbulenceNoiseShader(useFractal: Boolean = false) : uv.x *= in_aspectRatio; vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - float luma = abs(in_inverseLuma - simplex3d(noiseP)) * in_opacity; + // Bring it to [0, 1] range. + float luma = (simplex3d(noiseP) * in_inverseLuma) * 0.5 + 0.5; + luma = saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) + * in_opacity; vec3 mask = maskLuminosity(in_color.rgb, luma); vec3 color = in_backgroundColor.rgb + mask * 0.6; - // Add dither with triangle distribution to avoid color banding. Ok to dither in the + // Add dither with triangle distribution to avoid color banding. Dither in the // shader here as we are in gamma space. float dither = triangleNoise(p * in_pixelDensity) / 255.; // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to // multiply rgb with a to get the correct result. - color = (color + dither.rrr) * in_color.a; - return vec4(color, in_color.a); + color = (color + dither.rrr) * in_opacity; + return vec4(color, in_opacity); } """ @@ -70,12 +75,15 @@ class TurbulenceNoiseShader(useFractal: Boolean = false) : uv.x *= in_aspectRatio; vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - float luma = abs(in_inverseLuma - simplex3d_fractal(noiseP)) * in_opacity; + // Bring it to [0, 1] range. + float luma = (simplex3d_fractal(noiseP) * in_inverseLuma) * 0.5 + 0.5; + luma = saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) + * in_opacity; vec3 mask = maskLuminosity(in_color.rgb, luma); vec3 color = in_backgroundColor.rgb + mask * 0.6; // Skip dithering. - return vec4(color * in_color.a, in_color.a); + return vec4(color * in_opacity, in_opacity); } """ @@ -125,6 +133,28 @@ class TurbulenceNoiseShader(useFractal: Boolean = false) : } /** + * Sets blend and brightness factors of the luma matte. + * + * @param lumaMatteBlendFactor increases or decreases the amount of variance in noise. Setting + * this a lower number removes variations. I.e. the turbulence noise will look more blended. + * Expected input range is [0, 1]. more dimmed. + * @param lumaMatteOverallBrightness adds the overall brightness of the turbulence noise. + * Expected input range is [0, 1]. + * + * Example usage: You may want to apply a small number to [lumaMatteBlendFactor], such as 0.2, + * which makes the noise look softer. However it makes the overall noise look dim, so you want + * offset something like 0.3 for [lumaMatteOverallBrightness] to bring back its overall + * brightness. + */ + fun setLumaMatteFactors( + lumaMatteBlendFactor: Float = 1f, + lumaMatteOverallBrightness: Float = 0f + ) { + setFloatUniform("in_lumaMatteBlendFactor", lumaMatteBlendFactor) + setFloatUniform("in_lumaMatteOverallBrightness", lumaMatteOverallBrightness) + } + + /** * Sets whether to inverse the luminosity of the noise. * * By default noise will be used as a luma matte as is. This means that you will see color in @@ -132,7 +162,7 @@ class TurbulenceNoiseShader(useFractal: Boolean = false) : * true. */ fun setInverseNoiseLuminosity(inverse: Boolean) { - setFloatUniform("in_inverseLuma", if (inverse) 1f else 0f) + setFloatUniform("in_inverseLuma", if (inverse) -1f else 1f) } /** Current noise movements in x, y, and z axes. */ 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 c3e84787d4fb..43d6504fce84 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 @@ -215,10 +215,12 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex noiseConfig = config with(turbulenceNoiseShader) { setGridCount(config.gridCount) - setColor(ColorUtils.setAlphaComponent(config.color, config.opacity)) + setColor(config.color) setBackgroundColor(config.backgroundColor) setSize(config.width, config.height) setPixelDensity(config.pixelDensity) + setInverseNoiseLuminosity(inverse = false) + setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness) } paint.blendMode = config.blendMode } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 465b73e6de19..648ef03895cd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -74,7 +74,8 @@ class AnimatableClockView @JvmOverloads constructor( private var onTextAnimatorInitialized: Runnable? = null @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = - { layout, invalidateCb -> TextAnimator(layout, invalidateCb) } + { layout, invalidateCb -> + TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) } @VisibleForTesting var isAnimationEnabled: Boolean = true @VisibleForTesting var timeOverrideInMillis: Long? = null @@ -567,6 +568,7 @@ class AnimatableClockView @JvmOverloads constructor( private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500 private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000 private const val COLOR_ANIM_DURATION: Long = 400 + private const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 // Constants for the animation private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index 2baeaf67df93..9cc87fde122f 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -133,6 +133,9 @@ public interface ActivityStarter { boolean willAnimateOnKeyguard, @Nullable String customMessage); + /** Whether we should animate an activity launch. */ + boolean shouldAnimateLaunch(boolean isActivityIntent); + interface Callback { void onActivityStarted(int resultCode); } diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml index a8f0cc3a1d92..4a9d41fae1d5 100644 --- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml +++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml @@ -14,20 +14,6 @@ Copyright (C) 2015 The Android Open Source Project limitations under the License. --> <inset xmlns:android="http://schemas.android.com/apk/res/android" - android:insetLeft="3dp" - android:insetRight="3dp"> - <vector android:width="18dp" - android:height="18dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:fillColor="#FFFFFFFF" - android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4C10,21.1 10.9,22 12,22z"/> - <path - android:fillColor="#FFFFFFFF" - android:pathData="M16,16L2.81,2.81L1.39,4.22l4.85,4.85C6.09,9.68 6,10.33 6,11v6H4v2h12.17l3.61,3.61l1.41,-1.41L16,16zM8,17c0,0 0.01,-6.11 0.01,-6.16L14.17,17H8z"/> - <path - android:fillColor="#FFFFFFFF" - android:pathData="M12,6.5c2.49,0 4,2.02 4,4.5v2.17l2,2V11c0,-3.07 -1.63,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5S10.5,3.17 10.5,4v0.68C9.72,4.86 9.05,5.2 8.46,5.63L9.93,7.1C10.51,6.73 11.2,6.5 12,6.5z"/> - </vector> -</inset> + android:insetLeft="3dp" + android:insetRight="3dp" + android:drawable="@drawable/ic_speaker_mute" /> diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml index 81691898dfe5..efc661a6e974 100644 --- a/packages/SystemUI/res/layout/auth_biometric_contents.xml +++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml @@ -55,13 +55,7 @@ android:layout_height="wrap_content" android:layout_gravity="center"> - <com.airbnb.lottie.LottieAnimationView - android:id="@+id/biometric_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" /> + <include layout="@layout/auth_biometric_icon"/> <com.airbnb.lottie.LottieAnimationView android:id="@+id/biometric_icon_overlay" diff --git a/packages/SystemUI/res/layout/auth_biometric_icon.xml b/packages/SystemUI/res/layout/auth_biometric_icon.xml new file mode 100644 index 000000000000..b2df63dab700 --- /dev/null +++ b/packages/SystemUI/res/layout/auth_biometric_icon.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + + +<com.airbnb.lottie.LottieAnimationView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/biometric_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:contentDescription="@null" + android:scaleType="fitXY"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json new file mode 100644 index 000000000000..b1d6a270bc67 --- /dev/null +++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json @@ -0,0 +1 @@ +{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c619d462a5f6..9108fa615e88 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -353,7 +353,7 @@ <!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]--> <string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string> <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]--> - <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face.</string> + <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face</string> <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]--> <string name="biometric_dialog_tap_confirm_with_face_alt_1">Unlocked by face. Press to continue.</string> <!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]--> @@ -613,8 +613,8 @@ <string name="quick_settings_bluetooth_secondary_label_headset">Headset</string> <!-- QuickSettings: Bluetooth secondary label for an input/IO device being connected [CHAR LIMIT=20]--> <string name="quick_settings_bluetooth_secondary_label_input">Input</string> - <!-- QuickSettings: Bluetooth secondary label for a Hearing Aids device being connected [CHAR LIMIT=20]--> - <string name="quick_settings_bluetooth_secondary_label_hearing_aids">Hearing Aids</string> + <!-- QuickSettings: Bluetooth secondary label for a Hearing aids device being connected [CHAR LIMIT=20]--> + <string name="quick_settings_bluetooth_secondary_label_hearing_aids">Hearing aids</string> <!-- QuickSettings: Bluetooth secondary label shown when bluetooth is being enabled [CHAR LIMIT=NONE] --> <string name="quick_settings_bluetooth_secondary_label_transient">Turning on…</string> <!-- QuickSettings: Brightness [CHAR LIMIT=NONE] --> @@ -2726,8 +2726,8 @@ <string name="media_output_broadcast_last_update_error">Can\u2019t save.</string> <!-- The hint message when Broadcast code is less than 4 characters [CHAR LIMIT=60] --> <string name="media_output_broadcast_code_hint_no_less_than_min">Use at least 4 characters</string> - <!-- The hint message when Broadcast code is more than 16 characters [CHAR LIMIT=60] --> - <string name="media_output_broadcast_code_hint_no_more_than_max">Use fewer than 16 characters</string> + <!-- The hint message when Broadcast edit is more than 16/254 characters [CHAR LIMIT=60] --> + <string name="media_output_broadcast_edit_hint_no_more_than_max">Use fewer than <xliff:g id="length" example="16">%1$d</xliff:g> characters</string> <!-- Label for clip data when copying the build number off QS [CHAR LIMIT=NONE]--> <string name="build_number_clip_data_label">Build number</string> diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt index f96d1e3d7fef..070a45170d04 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -62,6 +62,15 @@ open class ViewScreenshotTestRule( private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false override fun apply(base: Statement, description: Description): Statement { + if (isRobolectric) { + // In robolectric mode, we enable NATIVE graphics and unpack font and icu files. + // We need to use reflection, as this library is only needed and therefore + // only available in deviceless mode. + val nativeLoaderClassName = "org.robolectric.nativeruntime.DefaultNativeRuntimeLoader" + val defaultNativeRuntimeLoader = Class.forName(nativeLoaderClassName) + System.setProperty("robolectric.graphicsMode", "NATIVE") + defaultNativeRuntimeLoader.getMethod("injectAndLoad").invoke(null) + } val ruleToApply = if (isRobolectric) roboRule else delegateRule return ruleToApply.apply(base, description) } @@ -69,6 +78,7 @@ open class ViewScreenshotTestRule( protected fun takeScreenshot( mode: Mode = Mode.WrapContent, viewProvider: (ComponentActivity) -> View, + beforeScreenshot: (ComponentActivity) -> Unit = {} ): Bitmap { activityRule.scenario.onActivity { activity -> // Make sure that the activity draws full screen and fits the whole display instead of @@ -94,6 +104,7 @@ open class ViewScreenshotTestRule( val content = activity.requireViewById<ViewGroup>(android.R.id.content) assertEquals(1, content.childCount) contentView = content.getChildAt(0) + beforeScreenshot(activity) } return if (isRobolectric) { @@ -111,9 +122,10 @@ open class ViewScreenshotTestRule( fun screenshotTest( goldenIdentifier: String, mode: Mode = Mode.WrapContent, - viewProvider: (ComponentActivity) -> View, + beforeScreenshot: (ComponentActivity) -> Unit = {}, + viewProvider: (ComponentActivity) -> View ) { - val bitmap = takeScreenshot(mode, viewProvider) + val bitmap = takeScreenshot(mode, viewProvider, beforeScreenshot) screenshotRule.assertBitmapAgainstGolden( bitmap, goldenIdentifier, diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt index 0ca2f7a36311..0fbeb1a054a7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt @@ -202,8 +202,6 @@ constructor( fun calculateScreenLocation(sampledView: View): RectF? { - if (!sampledView.isLaidOut) return null - val screenLocation = tmpScreenLocation /** * The method getLocationOnScreen is used to obtain the view coordinates relative to its @@ -219,6 +217,10 @@ constructor( samplingBounds.right = left + sampledView.width samplingBounds.bottom = top + sampledView.height + // ensure never go out of bounds + if (samplingBounds.right > displaySize.x) samplingBounds.right = displaySize.x + if (samplingBounds.bottom > displaySize.y) samplingBounds.bottom = displaySize.y + return RectF(samplingBounds) } diff --git a/packages/SystemUI/src/com/android/systemui/DejankUtils.java b/packages/SystemUI/src/com/android/systemui/DejankUtils.java index 3fce55f6e515..146d12c3e621 100644 --- a/packages/SystemUI/src/com/android/systemui/DejankUtils.java +++ b/packages/SystemUI/src/com/android/systemui/DejankUtils.java @@ -29,13 +29,17 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.StrictMode; import android.os.SystemProperties; +import android.os.Trace; import android.view.Choreographer; +import android.view.View; +import android.view.ViewRootImpl; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.util.Assert; import java.util.ArrayList; import java.util.HashSet; +import java.util.Random; import java.util.Stack; import java.util.function.Supplier; @@ -43,12 +47,14 @@ import java.util.function.Supplier; * Utility class for methods used to dejank the UI. */ public class DejankUtils { + private static final String TRACK_NAME = "DejankUtils"; public static final boolean STRICT_MODE_ENABLED = Build.IS_ENG || SystemProperties.getBoolean("persist.sysui.strictmode", false); private static final Choreographer sChoreographer = Choreographer.getInstance(); private static final Handler sHandler = new Handler(); private static final ArrayList<Runnable> sPendingRunnables = new ArrayList<>(); + private static final Random sRandom = new Random(); private static Stack<String> sBlockingIpcs = new Stack<>(); private static boolean sTemporarilyIgnoreStrictMode; private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>(); @@ -254,4 +260,30 @@ public class DejankUtils { public static void setImmediate(boolean immediate) { sImmediate = immediate; } + + /** + * Calls notifyRendererOfExpensiveFrame on the ViewRootImpl after performing null checks. + */ + public static void notifyRendererOfExpensiveFrame(View view, String reason) { + if (view == null) return; + notifyRendererOfExpensiveFrame(view.getViewRootImpl(), reason); + } + + /** + * Calls notifyRendererOfExpensiveFrame on the ViewRootImpl after performing null checks. + */ + public static void notifyRendererOfExpensiveFrame(ViewRootImpl viewRoot, String reason) { + if (viewRoot == null) return; + if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) { + int cookie = sRandom.nextInt(); + Trace.asyncTraceForTrackBegin( + Trace.TRACE_TAG_APP, + TRACK_NAME, + "notifyRendererOfExpensiveFrame (" + reason + ")", + cookie); + DejankUtils.postAfterTraversal( + () -> Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, cookie)); + } + viewRoot.notifyRendererOfExpensiveFrame(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index c30a2146436c..8af92ce4bcb1 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -548,6 +548,10 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { if (!cancelled) { updateSwipeProgressFromOffset(animView, canBeDismissed); resetSwipeOfView(animView); + // Clear the snapped view after success, assuming it's not being swiped now + if (animView == mTouchedView && !mIsSwiping) { + mTouchedView = null; + } } onChildSnappedBack(animView, targetLeft); }); @@ -813,13 +817,39 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { } } - public void resetSwipeState() { - View swipedView = getSwipedView(); + private void resetSwipeState() { + resetSwipeStates(/* resetAll= */ false); + } + + public void resetTouchState() { + resetSwipeStates(/* resetAll= */ true); + } + + /** This method resets the swipe state, and if `resetAll` is true, also resets the snap state */ + private void resetSwipeStates(boolean resetAll) { + final View touchedView = mTouchedView; + final boolean wasSnapping = mSnappingChild; + final boolean wasSwiping = mIsSwiping; mTouchedView = null; mIsSwiping = false; - if (swipedView != null) { - snapChildIfNeeded(swipedView, false, 0); - onChildSnappedBack(swipedView, 0); + // If we were swiping, then we resetting swipe requires resetting everything. + resetAll |= wasSwiping; + if (resetAll) { + mSnappingChild = false; + } + if (touchedView == null) return; // No view to reset visually + // When snap needs to be reset, first thing is to cancel any translation animation + final boolean snapNeedsReset = resetAll && wasSnapping; + if (snapNeedsReset) { + cancelTranslateAnimation(touchedView); + } + // actually reset the view to default state + if (resetAll) { + snapChildIfNeeded(touchedView, false, 0); + } + // report if a swipe or snap was reset. + if (wasSwiping || snapNeedsReset) { + onChildSnappedBack(touchedView, 0); } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 50e03992df49..ba3c8602c6ff 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -30,8 +30,10 @@ import com.android.internal.os.BinderInternal; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; +import com.android.systemui.dump.LogBufferEulogizer; import com.android.systemui.dump.LogBufferFreezer; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; +import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager; import com.android.systemui.statusbar.policy.BatteryStateNotifier; import java.io.FileDescriptor; @@ -44,22 +46,29 @@ public class SystemUIService extends Service { private final Handler mMainHandler; private final DumpHandler mDumpHandler; private final BroadcastDispatcher mBroadcastDispatcher; + private final LogBufferEulogizer mLogBufferEulogizer; private final LogBufferFreezer mLogBufferFreezer; private final BatteryStateNotifier mBatteryStateNotifier; + private final UncaughtExceptionPreHandlerManager mUncaughtExceptionPreHandlerManager; + @Inject public SystemUIService( @Main Handler mainHandler, DumpHandler dumpHandler, BroadcastDispatcher broadcastDispatcher, + LogBufferEulogizer logBufferEulogizer, LogBufferFreezer logBufferFreezer, - BatteryStateNotifier batteryStateNotifier) { + BatteryStateNotifier batteryStateNotifier, + UncaughtExceptionPreHandlerManager uncaughtExceptionPreHandlerManager) { super(); mMainHandler = mainHandler; mDumpHandler = dumpHandler; mBroadcastDispatcher = broadcastDispatcher; + mLogBufferEulogizer = logBufferEulogizer; mLogBufferFreezer = logBufferFreezer; mBatteryStateNotifier = batteryStateNotifier; + mUncaughtExceptionPreHandlerManager = uncaughtExceptionPreHandlerManager; } @Override @@ -71,7 +80,13 @@ public class SystemUIService extends Service { // Finish initializing dump logic mLogBufferFreezer.attach(mBroadcastDispatcher); - mDumpHandler.init(); + + // Attempt to dump all LogBuffers for any uncaught exception + mUncaughtExceptionPreHandlerManager.registerHandler((thread, throwable) -> { + if (throwable instanceof Exception) { + mLogBufferEulogizer.record(((Exception) throwable)); + } + }); // If configured, set up a battery notification if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 4158390ec953..eebc1f06c516 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -485,13 +485,11 @@ public class SystemActions implements CoreStartable { } private void handleNotifications() { - mCentralSurfacesOptionalLazy.get().ifPresent( - CentralSurfaces::animateExpandNotificationsPanel); + mShadeController.animateExpandShade(); } private void handleQuickSettings() { - mCentralSurfacesOptionalLazy.get().ifPresent( - centralSurfaces -> centralSurfaces.animateExpandSettingsPanel(null)); + mShadeController.animateExpandQs(); } private void handlePowerDialog() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt index 682888fc39b5..95610aec3562 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt @@ -21,6 +21,7 @@ import android.content.Context import com.airbnb.lottie.LottieAnimationView import com.android.systemui.R import com.android.systemui.biometrics.AuthBiometricView.BiometricState +import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION @@ -47,13 +48,16 @@ open class AuthBiometricFingerprintAndFaceIconController( @BiometricState oldState: Int, @BiometricState newState: Int ): Int? = when (newState) { + STATE_AUTHENTICATED -> { + if (oldState == STATE_PENDING_CONFIRMATION) { + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + } else { + super.getAnimationForTransition(oldState, newState) + } + } STATE_PENDING_CONFIRMATION -> { if (oldState == STATE_ERROR || oldState == STATE_HELP) { R.raw.fingerprint_dialogue_error_to_unlock_lottie - } else if (oldState == STATE_PENDING_CONFIRMATION) { - // TODO(jbolinger): missing asset for this transition - // (unlocked icon to success checkmark) - R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie } else { R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 69ce78ce30a8..cd8f04d18500 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -961,6 +961,10 @@ public abstract class AuthBiometricView extends LinearLayout implements AuthBiom return Utils.isDeviceCredentialAllowed(mPromptInfo); } + public LottieAnimationView getIconView() { + return mIconView; + } + @AuthDialog.DialogSize int getSize() { return mSize; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 43a3b9958ee5..7f706859abb3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -614,7 +614,11 @@ public class AuthContainerView extends LinearLayout return ((AuthBiometricFingerprintView) view).isUdfps(); } if (view instanceof BiometricPromptLayout) { - return ((BiometricPromptLayout) view).isUdfps(); + // this will force the prompt to align itself on the edge of the screen + // instead of centering (temporary workaround to prevent small implicit view + // from breaking due to the way gravity / margins are set in the legacy + // AuthPanelController + return true; } return false; @@ -638,12 +642,12 @@ public class AuthContainerView extends LinearLayout case Surface.ROTATION_90: mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); - setScrollViewGravity(Gravity.BOTTOM | Gravity.RIGHT); + setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); break; case Surface.ROTATION_270: mPanelController.setPosition(AuthPanelController.POSITION_LEFT); - setScrollViewGravity(Gravity.BOTTOM | Gravity.LEFT); + setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); break; case Surface.ROTATION_180: diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 57f1928fe545..b2ffea3d050c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -201,7 +201,9 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, final TaskStackListener mTaskStackListener = new TaskStackListener() { @Override public void onTaskStackChanged() { - mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground); + if (!isOwnerInForeground()) { + mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground); + } } }; @@ -239,33 +241,37 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } + private boolean isOwnerInForeground() { + final String clientPackage = mCurrentDialog.getOpPackageName(); + final List<ActivityManager.RunningTaskInfo> runningTasks = + mActivityTaskManager.getTasks(1); + if (!runningTasks.isEmpty()) { + final String topPackage = runningTasks.get(0).topActivity.getPackageName(); + if (!topPackage.contentEquals(clientPackage) + && !Utils.isSystem(mContext, clientPackage)) { + Log.w(TAG, "Evicting client due to: " + topPackage); + return false; + } + } + return true; + } + private void cancelIfOwnerIsNotInForeground() { mExecution.assertIsMainThread(); if (mCurrentDialog != null) { try { - final String clientPackage = mCurrentDialog.getOpPackageName(); - Log.w(TAG, "Task stack changed, current client: " + clientPackage); - final List<ActivityManager.RunningTaskInfo> runningTasks = - mActivityTaskManager.getTasks(1); - if (!runningTasks.isEmpty()) { - final String topPackage = runningTasks.get(0).topActivity.getPackageName(); - if (!topPackage.contentEquals(clientPackage) - && !Utils.isSystem(mContext, clientPackage)) { - Log.e(TAG, "Evicting client due to: " + topPackage); - mCurrentDialog.dismissWithoutCallback(true /* animate */); - mCurrentDialog = null; - - for (Callback cb : mCallbacks) { - cb.onBiometricPromptDismissed(); - } + mCurrentDialog.dismissWithoutCallback(true /* animate */); + mCurrentDialog = null; - if (mReceiver != null) { - mReceiver.onDialogDismissed( - BiometricPrompt.DISMISSED_REASON_USER_CANCEL, - null /* credentialAttestation */); - mReceiver = null; - } - } + for (Callback cb : mCallbacks) { + cb.onBiometricPromptDismissed(); + } + + if (mReceiver != null) { + mReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_USER_CANCEL, + null /* credentialAttestation */); + mReceiver = null; } } catch (RemoteException e) { Log.e(TAG, "Remote exception", e); @@ -1253,10 +1259,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, cb.onBiometricPromptShown(); } mCurrentDialog = newDialog; - mCurrentDialog.show(mWindowManager, savedState); - if (!promptInfo.isAllowBackgroundAuthentication()) { - mHandler.post(this::cancelIfOwnerIsNotInForeground); + if (!promptInfo.isAllowBackgroundAuthentication() && !isOwnerInForeground()) { + cancelIfOwnerIsNotInForeground(); + } else { + mCurrentDialog.show(mWindowManager, savedState); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java index acdde3404ab5..167067e7d7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -114,7 +114,13 @@ public class AuthPanelController extends ViewOutlineProvider { } private int getTopBound(@Position int position) { - return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin); + switch (position) { + case POSITION_LEFT: + case POSITION_RIGHT: + return Math.max((mContainerHeight - mContentHeight) / 2, mMargin); + default: + return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin); + } } public void setContainerDimensions(int containerWidth, int containerHeight) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index baaa96efb5f0..d48b9c339d15 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -35,6 +35,7 @@ import android.os.Handler import android.util.Log import android.util.RotationUtils import android.view.Display +import android.view.DisplayInfo import android.view.Gravity import android.view.LayoutInflater import android.view.Surface @@ -58,6 +59,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.util.boundsOnScreen import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.traceSection import java.io.PrintWriter @@ -129,6 +131,8 @@ constructor( } @VisibleForTesting var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT + private val displayInfo = DisplayInfo() + private val overlayViewParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, @@ -214,6 +218,23 @@ constructor( for (requestSource in requests) { pw.println(" $requestSource.name") } + + pw.println("overlayView:") + pw.println(" width=${overlayView?.width}") + pw.println(" height=${overlayView?.height}") + pw.println(" boundsOnScreen=${overlayView?.boundsOnScreen}") + + pw.println("displayStateInteractor:") + pw.println(" isInRearDisplayMode=${displayStateInteractor?.isInRearDisplayMode?.value}") + + pw.println("sensorProps:") + pw.println(" displayId=${displayInfo.uniqueId}") + pw.println(" sensorType=${sensorProps?.sensorType}") + pw.println(" location=${sensorProps?.getLocation(displayInfo.uniqueId)}") + + pw.println("overlayOffsets=$overlayOffsets") + pw.println("isReverseDefaultRotation=$isReverseDefaultRotation") + pw.println("currentRotation=${displayInfo.rotation}") } private fun onOrientationChanged(@BiometricOverlayConstants.ShowReason reason: Int) { @@ -226,6 +247,8 @@ constructor( val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) overlayView = view val display = context.display!! + // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation + display.getDisplayInfo(displayInfo) val offsets = sensorProps.getLocation(display.uniqueId).let { location -> if (location == null) { @@ -239,12 +262,12 @@ constructor( view.rotation = display.asSideFpsAnimationRotation( offsets.isYAligned(), - getRotationFromDefault(display.rotation) + getRotationFromDefault(displayInfo.rotation) ) lottie.setAnimation( display.asSideFpsAnimation( offsets.isYAligned(), - getRotationFromDefault(display.rotation) + getRotationFromDefault(displayInfo.rotation) ) ) lottie.addLottieOnCompositionLoadedListener { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index c935aa290e21..26b6f2a7a3cc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -78,6 +78,7 @@ constructor( sendFoldStateUpdate(isFolded) } } + sendFoldStateUpdate(false) screenSizeFoldProvider.registerCallback(callback, mainExecutor) awaitClose { screenSizeFoldProvider.unregisterCallback(callback) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt new file mode 100644 index 000000000000..bd0907e588ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt @@ -0,0 +1,47 @@ +/* + * 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.biometrics.ui.binder + +import android.view.DisplayInfo +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.android.systemui.biometrics.AuthBiometricFingerprintView +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.launch + +/** Sub-binder for [AuthBiometricFingerprintView.mIconView]. */ +object AuthBiometricFingerprintIconViewBinder { + + /** + * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel]. + */ + @JvmStatic + fun bind(view: LottieAnimationView, viewModel: AuthBiometricFingerprintViewModel) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + val displayInfo = DisplayInfo() + view.context.display?.getDisplayInfo(displayInfo) + viewModel.setRotation(displayInfo.rotation) + viewModel.onConfigurationChanged(view.context.resources.configuration) + launch { viewModel.iconAsset.collect { iconAsset -> view.setAnimation(iconAsset) } } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt index ae0cf3771ed3..9c1bcec2f396 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt @@ -17,31 +17,18 @@ package com.android.systemui.biometrics.ui.binder -import android.view.Surface -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.biometrics.AuthBiometricFingerprintView import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.launch object AuthBiometricFingerprintViewBinder { - /** Binds a [AuthBiometricFingerprintView] to a [AuthBiometricFingerprintViewModel]. */ + /** + * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel]. + */ @JvmStatic fun bind(view: AuthBiometricFingerprintView, viewModel: AuthBiometricFingerprintViewModel) { - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.onConfigurationChanged(view.context.resources.configuration) - viewModel.setRotation(view.context.display?.orientation ?: Surface.ROTATION_0) - launch { - viewModel.iconAsset.collect { iconAsset -> - if (view.isSfps) { - view.updateIconViewAnimation(iconAsset) - } - } - } - } + if (view.isSfps) { + AuthBiometricFingerprintIconViewBinder.bind(view.getIconView(), viewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index e4c4e9aedb56..1dffa80a084f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -19,8 +19,11 @@ package com.android.systemui.biometrics.ui.binder import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator +import android.view.Surface import android.view.View import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView import androidx.core.animation.addListener @@ -52,7 +55,9 @@ object BiometricViewSizeBinder { panelViewController: AuthPanelController, jankListener: BiometricJankListener, ) { - val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! + val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java)) + val accessibilityManager = + requireNotNull(view.context.getSystemService(AccessibilityManager::class.java)) fun notifyAccessibilityChanged() { Utils.notifyAccessibilityContentChanged(accessibilityManager, view) } @@ -102,15 +107,26 @@ object BiometricViewSizeBinder { when { size.isSmall -> { iconHolderView.alpha = 1f + val bottomInset = + windowManager.maximumWindowMetrics.windowInsets + .getInsets(WindowInsets.Type.navigationBars()) + .bottom iconHolderView.y = - view.height - iconHolderView.height - iconPadding + if (view.isLandscape()) { + (view.height - iconHolderView.height - bottomInset) / 2f + } else { + view.height - + iconHolderView.height - + iconPadding - + bottomInset + } val newHeight = - iconHolderView.height + 2 * iconPadding.toInt() - + iconHolderView.height + (2 * iconPadding.toInt()) - iconHolderView.paddingTop - iconHolderView.paddingBottom panelViewController.updateForContentDimensions( width, - newHeight, + newHeight + bottomInset, 0, /* animateDurationMs */ ) } @@ -181,6 +197,11 @@ object BiometricViewSizeBinder { } } +private fun View.isLandscape(): Boolean { + val r = context.display.rotation + return r == Surface.ROTATION_90 || r == Surface.ROTATION_270 +} + private fun TextView.showTextOrHide(forceHide: Boolean = false) { visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 1da7900e985b..df2a749d986d 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -283,6 +283,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + mWindow.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); mWindow.requestFeature(Window.FEATURE_NO_TITLE); // Hide all insets when the dream is showing mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index b141db15ea0f..c2421dcbc6ca 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -19,7 +19,6 @@ package com.android.systemui.dreams; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_ENABLED; import android.service.dreams.DreamService; -import android.util.Log; import androidx.annotation.NonNull; @@ -52,7 +51,6 @@ import javax.inject.Named; public class DreamOverlayStateController implements CallbackController<DreamOverlayStateController.Callback> { private static final String TAG = "DreamOverlayStateCtlr"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0; public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1; @@ -110,13 +108,17 @@ public class DreamOverlayStateController implements private final int mSupportedTypes; + private final DreamLogger mLogger; + @VisibleForTesting @Inject public DreamOverlayStateController(@Main Executor executor, @Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + DreamLogger dreamLogger) { mExecutor = executor; mOverlayEnabled = overlayEnabled; + mLogger = dreamLogger; mFeatureFlags = featureFlags; if (mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)) { mSupportedTypes = Complication.COMPLICATION_TYPE_NONE @@ -124,9 +126,7 @@ public class DreamOverlayStateController implements } else { mSupportedTypes = Complication.COMPLICATION_TYPE_NONE; } - if (DEBUG) { - Log.d(TAG, "Dream overlay enabled:" + mOverlayEnabled); - } + mLogger.d(TAG, "Dream overlay enabled: " + mOverlayEnabled); } /** @@ -134,18 +134,14 @@ public class DreamOverlayStateController implements */ public void addComplication(Complication complication) { if (!mOverlayEnabled) { - if (DEBUG) { - Log.d(TAG, - "Ignoring adding complication due to overlay disabled:" + complication); - } + mLogger.d(TAG, + "Ignoring adding complication due to overlay disabled: " + complication); return; } mExecutor.execute(() -> { if (mComplications.add(complication)) { - if (DEBUG) { - Log.d(TAG, "addComplication: added " + complication); - } + mLogger.d(TAG, "Added dream complication: " + complication); mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); @@ -156,18 +152,14 @@ public class DreamOverlayStateController implements */ public void removeComplication(Complication complication) { if (!mOverlayEnabled) { - if (DEBUG) { - Log.d(TAG, - "Ignoring removing complication due to overlay disabled:" + complication); - } + mLogger.d(TAG, + "Ignoring removing complication due to overlay disabled: " + complication); return; } mExecutor.execute(() -> { if (mComplications.remove(complication)) { - if (DEBUG) { - Log.d(TAG, "removeComplication: removed " + complication); - } + mLogger.d(TAG, "Removed dream complication: " + complication); mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); @@ -313,6 +305,7 @@ public class DreamOverlayStateController implements * @param active {@code true} if overlay is active, {@code false} otherwise. */ public void setOverlayActive(boolean active) { + mLogger.d(TAG, "Dream overlay active: " + active); modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_ACTIVE); } @@ -321,6 +314,8 @@ public class DreamOverlayStateController implements * @param active {@code true} if low light mode is active, {@code false} otherwise. */ public void setLowLightActive(boolean active) { + mLogger.d(TAG, "Low light mode active: " + active); + if (isLowLightActive() && !active) { // Notify that we're exiting low light only on the transition from active to not active. mCallbacks.forEach(Callback::onExitLowLight); @@ -351,6 +346,7 @@ public class DreamOverlayStateController implements * @param hasAttention {@code true} if has the user's attention, {@code false} otherwise. */ public void setHasAssistantAttention(boolean hasAttention) { + mLogger.d(TAG, "Dream overlay has Assistant attention: " + hasAttention); modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION); } @@ -359,6 +355,7 @@ public class DreamOverlayStateController implements * @param visible {@code true} if the status bar is visible, {@code false} otherwise. */ public void setDreamOverlayStatusBarVisible(boolean visible) { + mLogger.d(TAG, "Dream overlay status bar visible: " + visible); modifyState( visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE); } @@ -376,6 +373,7 @@ public class DreamOverlayStateController implements */ public void setAvailableComplicationTypes(@Complication.ComplicationType int types) { mExecutor.execute(() -> { + mLogger.d(TAG, "Available complication types: " + types); mAvailableComplicationTypes = types; mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged); }); @@ -393,6 +391,7 @@ public class DreamOverlayStateController implements */ public void setShouldShowComplications(boolean shouldShowComplications) { mExecutor.execute(() -> { + mLogger.d(TAG, "Should show complications: " + shouldShowComplications); mShouldShowComplications = shouldShowComplications; mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged); }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index 7c1bfeda30b2..1865c38632e5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -141,6 +141,20 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); } + protected static String getLoggableStatusIconType(@StatusIconType int type) { + return switch (type) { + case STATUS_ICON_NOTIFICATIONS -> "notifications"; + case STATUS_ICON_WIFI_UNAVAILABLE -> "wifi_unavailable"; + case STATUS_ICON_ALARM_SET -> "alarm_set"; + case STATUS_ICON_CAMERA_DISABLED -> "camera_disabled"; + case STATUS_ICON_MIC_DISABLED -> "mic_disabled"; + case STATUS_ICON_MIC_CAMERA_DISABLED -> "mic_camera_disabled"; + case STATUS_ICON_PRIORITY_MODE_ON -> "priority_mode_on"; + case STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE -> "assistant_attention_active"; + default -> type + "(unknown)"; + }; + } + void showIcon(@StatusIconType int iconType, boolean show, @Nullable String contentDescription) { View icon = mStatusIcons.get(iconType); if (icon == null) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java index c954f98ad36e..3a284083e844 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java @@ -61,6 +61,8 @@ import javax.inject.Inject; */ @DreamOverlayComponent.DreamOverlayScope public class DreamOverlayStatusBarViewController extends ViewController<DreamOverlayStatusBarView> { + private static final String TAG = "DreamStatusBarCtrl"; + private final ConnectivityManager mConnectivityManager; private final TouchInsetManager.TouchInsetSession mTouchInsetSession; private final NextAlarmController mNextAlarmController; @@ -78,6 +80,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve private final Executor mMainExecutor; private final List<DreamOverlayStatusBarItemsProvider.StatusBarItem> mExtraStatusBarItems = new ArrayList<>(); + private final DreamLogger mLogger; private boolean mIsAttached; @@ -157,7 +160,8 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve StatusBarWindowStateController statusBarWindowStateController, DreamOverlayStatusBarItemsProvider statusBarItemsProvider, DreamOverlayStateController dreamOverlayStateController, - UserTracker userTracker) { + UserTracker userTracker, + DreamLogger dreamLogger) { super(view); mResources = resources; mMainExecutor = mainExecutor; @@ -173,6 +177,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve mZenModeController = zenModeController; mDreamOverlayStateController = dreamOverlayStateController; mUserTracker = userTracker; + mLogger = dreamLogger; // Register to receive show/hide updates for the system status bar. Our custom status bar // needs to hide when the system status bar is showing to ovoid overlapping status bars. @@ -341,6 +346,8 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve @Nullable String contentDescription) { mMainExecutor.execute(() -> { if (mIsAttached) { + mLogger.d(TAG, (show ? "Showing" : "Hiding") + " dream status bar item: " + + DreamOverlayStatusBarView.getLoggableStatusIconType(iconType)); mView.showIcon(iconType, show, contentDescription); } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt index 78e132ff6397..4b297a3d321d 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt @@ -33,10 +33,10 @@ import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_DREAM import com.android.systemui.smartspace.SmartspacePrecondition import com.android.systemui.smartspace.SmartspaceTargetFilter -import com.android.systemui.smartspace.dagger.SmartspaceModule import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN import com.android.systemui.smartspace.dagger.SmartspaceViewComponent import com.android.systemui.util.concurrency.Execution import java.util.Optional @@ -58,7 +58,7 @@ class DreamSmartspaceController @Inject constructor( @Named(DREAM_SMARTSPACE_TARGET_FILTER) private val optionalTargetFilter: Optional<SmartspaceTargetFilter>, @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>, - @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN) + @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN) optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>, ) { companion object { diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 4b03fd334cb5..75284fc18149 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -20,12 +20,16 @@ import android.content.Context import android.os.SystemClock import android.os.Trace import com.android.systemui.CoreStartable +import com.android.systemui.ProtoDumpable import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL +import com.android.systemui.dump.DumpsysEntry.DumpableEntry +import com.android.systemui.dump.DumpsysEntry.LogBufferEntry +import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.log.LogBuffer -import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.android.systemui.log.table.TableLogBuffer import com.google.protobuf.nano.MessageNano import java.io.BufferedOutputStream import java.io.FileDescriptor @@ -39,11 +43,11 @@ import javax.inject.Provider * * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections - * contains all [LogBuffer]s (due to their length). + * contains all [LogBuffer]s and [TableLogBuffer]s (due to their length). * - * The CRITICAL and NORMAL sections can be found within a bug report by searching for - * "SERVICE com.android.systemui/.SystemUIService" and - * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively. + * The CRITICAL and NORMAL sections can be found within a bug report by searching for "SERVICE + * com.android.systemui/.SystemUIService" and "SERVICE + * com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively. * * Finally, some or all of the dump can be triggered on-demand via adb (see below). * @@ -72,6 +76,7 @@ import javax.inject.Provider * # To dump all dumpables or all buffers: * $ <invocation> dumpables * $ <invocation> buffers + * $ <invocation> tables * * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a * # bug report: @@ -83,37 +88,26 @@ import javax.inject.Provider * $ <invocation> --help * ``` */ -class DumpHandler @Inject constructor( +class DumpHandler +@Inject +constructor( private val context: Context, private val dumpManager: DumpManager, private val logBufferEulogizer: LogBufferEulogizer, private val startables: MutableMap<Class<*>, Provider<CoreStartable>>, - private val uncaughtExceptionPreHandlerManager: UncaughtExceptionPreHandlerManager ) { - /** - * Registers an uncaught exception handler - */ - fun init() { - uncaughtExceptionPreHandlerManager.registerHandler { _, e -> - if (e is Exception) { - logBufferEulogizer.record(e) - } - } - } - - /** - * Dump the diagnostics! Behavior can be controlled via [args]. - */ + /** Dump the diagnostics! Behavior can be controlled via [args]. */ fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { Trace.beginSection("DumpManager#dump()") val start = SystemClock.uptimeMillis() - val parsedArgs = try { - parseArgs(args) - } catch (e: ArgParseException) { - pw.println(e.message) - return - } + val parsedArgs = + try { + parseArgs(args) + } catch (e: ArgParseException) { + pw.println(e.message) + return + } when { parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) @@ -134,6 +128,7 @@ class DumpHandler @Inject constructor( "bugreport-normal" -> dumpNormal(pw, args) "dumpables" -> dumpDumpables(pw, args) "buffers" -> dumpBuffers(pw, args) + "tables" -> dumpTables(pw, args) "config" -> dumpConfig(pw) "help" -> dumpHelp(pw) else -> { @@ -147,44 +142,65 @@ class DumpHandler @Inject constructor( } private fun dumpCritical(pw: PrintWriter, args: ParsedArgs) { - dumpManager.dumpCritical(pw, args.rawArgs) + val targets = dumpManager.getDumpables() + for (target in targets) { + if (target.priority == DumpPriority.CRITICAL) { + dumpDumpable(target, pw, args.rawArgs) + } + } dumpConfig(pw) } private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) { - dumpManager.dumpNormal(pw, args.rawArgs, args.tailLength) - logBufferEulogizer.readEulogyIfPresent(pw) - } + val targets = dumpManager.getDumpables() + for (target in targets) { + if (target.priority == DumpPriority.NORMAL) { + dumpDumpable(target, pw, args.rawArgs) + } + } - private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) { - if (args.listOnly) { - dumpManager.listDumpables(pw) - } else { - dumpManager.dumpDumpables(pw, args.rawArgs) + val buffers = dumpManager.getLogBuffers() + for (buffer in buffers) { + dumpBuffer(buffer, pw, args.tailLength) } + + val tableBuffers = dumpManager.getTableLogBuffers() + for (table in tableBuffers) { + dumpTableBuffer(table, pw, args.rawArgs) + } + + logBufferEulogizer.readEulogyIfPresent(pw) } - private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) { - if (args.listOnly) { - dumpManager.listBuffers(pw) - } else { - dumpManager.dumpBuffers(pw, args.tailLength) + private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getDumpables().listOrDumpEntries(pw, args) + + private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getLogBuffers().listOrDumpEntries(pw, args) + + private fun dumpTables(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getTableLogBuffers().listOrDumpEntries(pw, args) + + private fun listTargetNames(targets: Collection<DumpsysEntry>, pw: PrintWriter) { + for (target in targets) { + pw.println(target.name) } } - private fun dumpProtoTargets( - targets: List<String>, - fd: FileDescriptor, - args: ParsedArgs - ) { + private fun dumpProtoTargets(targets: List<String>, fd: FileDescriptor, args: ParsedArgs) { val systemUIProto = SystemUIProtoDump() + val dumpables = dumpManager.getDumpables() if (targets.isNotEmpty()) { for (target in targets) { - dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs) + findBestProtoTargetMatch(dumpables, target)?.dumpProto(systemUIProto, args.rawArgs) } } else { - dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs) + // Dump all protos + for (dumpable in dumpables) { + (dumpable.dumpable as? ProtoDumpable)?.dumpProto(systemUIProto, args.rawArgs) + } } + val buffer = BufferedOutputStream(FileOutputStream(fd)) buffer.use { it.write(MessageNano.toByteArray(systemUIProto)) @@ -192,36 +208,70 @@ class DumpHandler @Inject constructor( } } - private fun dumpTargets( - targets: List<String>, - pw: PrintWriter, - args: ParsedArgs - ) { + // Attempts to dump the target list to the given PrintWriter. Since the arguments come in as + // a list of strings, we use the [findBestTargetMatch] method to determine the most-correct + // target with the given search string. + private fun dumpTargets(targets: List<String>, pw: PrintWriter, args: ParsedArgs) { if (targets.isNotEmpty()) { - for (target in targets) { - dumpManager.dumpTarget(target, pw, args.rawArgs, args.tailLength) + val dumpables = dumpManager.getDumpables() + val buffers = dumpManager.getLogBuffers() + val tableBuffers = dumpManager.getTableLogBuffers() + + targets.forEach { target -> + findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args) } } else { if (args.listOnly) { + val dumpables = dumpManager.getDumpables() + val buffers = dumpManager.getLogBuffers() + pw.println("Dumpables:") - dumpManager.listDumpables(pw) + listTargetNames(dumpables, pw) pw.println() pw.println("Buffers:") - dumpManager.listBuffers(pw) + listTargetNames(buffers, pw) } else { pw.println("Nothing to dump :(") } } } + private fun findTargetInCollection( + target: String, + dumpables: Collection<DumpableEntry>, + logBuffers: Collection<LogBufferEntry>, + tableBuffers: Collection<TableLogBufferEntry>, + ) = + sequence { + findBestTargetMatch(dumpables, target)?.let { yield(it) } + findBestTargetMatch(logBuffers, target)?.let { yield(it) } + findBestTargetMatch(tableBuffers, target)?.let { yield(it) } + } + .sortedBy { it.name } + .minByOrNull { it.name.length } + + private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter, args: Array<String>) { + pw.preamble(entry) + entry.dumpable.dump(pw, args) + } + + private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter, tailLength: Int) { + pw.preamble(entry) + entry.buffer.dump(pw, tailLength) + } + + private fun dumpTableBuffer(buffer: TableLogBufferEntry, pw: PrintWriter, args: Array<String>) { + pw.preamble(buffer) + buffer.table.dump(pw, args) + } + private fun dumpConfig(pw: PrintWriter) { pw.println("SystemUiServiceComponents configuration:") pw.print("vendor component: ") pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent)) - val services: MutableList<String> = startables.keys - .map({ cls: Class<*> -> cls.simpleName }) - .toMutableList() + val services: MutableList<String> = + startables.keys.map({ cls: Class<*> -> cls.simpleName }).toMutableList() services.add(context.resources.getString(R.string.config_systemUIVendorServiceComponent)) dumpServiceList(pw, "global", services.toTypedArray()) @@ -265,6 +315,7 @@ class DumpHandler @Inject constructor( pw.println("Special commands:") pw.println("$ <invocation> dumpables") pw.println("$ <invocation> buffers") + pw.println("$ <invocation> tables") pw.println("$ <invocation> bugreport-critical") pw.println("$ <invocation> bugreport-normal") pw.println("$ <invocation> config") @@ -274,6 +325,7 @@ class DumpHandler @Inject constructor( pw.println("$ <invocation> --list") pw.println("$ <invocation> dumpables --list") pw.println("$ <invocation> buffers --list") + pw.println("$ <invocation> tables --list") pw.println() pw.println("Show only the most recent N lines of buffers") @@ -291,24 +343,26 @@ class DumpHandler @Inject constructor( iterator.remove() when (arg) { PRIORITY_ARG -> { - pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) { - if (PRIORITY_OPTIONS.contains(it)) { - it - } else { - throw IllegalArgumentException() + pArgs.dumpPriority = + readArgument(iterator, PRIORITY_ARG) { + if (PRIORITY_OPTIONS.contains(it)) { + it + } else { + throw IllegalArgumentException() + } } - } } PROTO -> pArgs.proto = true - "-t", "--tail" -> { - pArgs.tailLength = readArgument(iterator, arg) { - it.toInt() - } + "-t", + "--tail" -> { + pArgs.tailLength = readArgument(iterator, arg) { it.toInt() } } - "-l", "--list" -> { + "-l", + "--list" -> { pArgs.listOnly = true } - "-h", "--help" -> { + "-h", + "--help" -> { pArgs.command = "help" } // This flag is passed as part of the proto dump in Bug reports, we can ignore @@ -345,29 +399,131 @@ class DumpHandler @Inject constructor( } } + private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) = + when (this) { + is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs) + is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength) + is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs) + } + + private fun Collection<DumpsysEntry>.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) = + if (args.listOnly) { + listTargetNames(this, pw) + } else { + forEach { it.dump(pw, args) } + } + companion object { const val PRIORITY_ARG = "--dump-priority" const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_NORMAL = "NORMAL" const val PROTO = "--proto" + + /** + * Important: do not change this divider without updating any bug report processing tools + * (e.g. ABT), since this divider is used to determine boundaries for bug report views + */ + const val DUMPSYS_DUMPABLE_DIVIDER = + "----------------------------------------------------------------------------" + + /** + * Important: do not change this divider without updating any bug report processing tools + * (e.g. ABT), since this divider is used to determine boundaries for bug report views + */ + const val DUMPSYS_BUFFER_DIVIDER = + "============================================================================" + + private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) = + c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length } + + private fun findBestProtoTargetMatch( + c: Collection<DumpableEntry>, + target: String + ): ProtoDumpable? = + c.asSequence() + .filter { it.name.endsWith(target) } + .filter { it.dumpable is ProtoDumpable } + .minByOrNull { it.name.length } + ?.dumpable as? ProtoDumpable + + private fun PrintWriter.preamble(entry: DumpsysEntry) = + when (entry) { + // Historically TableLogBuffer was not separate from dumpables, so they have the + // same header + is DumpableEntry, + is TableLogBufferEntry -> { + println() + println(entry.name) + println(DUMPSYS_DUMPABLE_DIVIDER) + } + is LogBufferEntry -> { + println() + println() + println("BUFFER ${entry.name}:") + println(DUMPSYS_BUFFER_DIVIDER) + } + } + + /** + * Zero-arg utility to write a [DumpableEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.dumpable.dump(pw, arrayOf()) + } + + /** + * Zero-arg utility to write a [LogBufferEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.buffer.dump(pw, 0) + } + + /** + * Zero-arg utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpTableBuffer(entry: TableLogBufferEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.table.dump(pw, arrayOf()) + } + + /** + * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + fun DumpsysEntry.dump(pw: PrintWriter) { + when (this) { + is DumpableEntry -> dumpDumpable(this, pw) + is LogBufferEntry -> dumpBuffer(this, pw) + is TableLogBufferEntry -> dumpTableBuffer(this, pw) + } + } + + /** Format [entries] in a dumpsys-appropriate way, using [pw] */ + fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) { + entries.forEach { it.dump(pw) } + } } } private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL) -private val COMMANDS = arrayOf( +private val COMMANDS = + arrayOf( "bugreport-critical", "bugreport-normal", "buffers", "dumpables", + "tables", "config", "help" -) + ) -private class ParsedArgs( - val rawArgs: Array<String>, - val nonFlagArgs: List<String> -) { +private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) { var dumpPriority: String? = null var tailLength: Int = 0 var command: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index 2d57633e47a8..c924df6da263 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,9 +18,11 @@ package com.android.systemui.dump import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable -import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.dump.DumpsysEntry.DumpableEntry +import com.android.systemui.dump.DumpsysEntry.LogBufferEntry +import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry import com.android.systemui.log.LogBuffer -import java.io.PrintWriter +import com.android.systemui.log.table.TableLogBuffer import java.util.TreeMap import javax.inject.Inject import javax.inject.Singleton @@ -37,8 +39,9 @@ import javax.inject.Singleton @Singleton open class DumpManager @Inject constructor() { // NOTE: Using TreeMap ensures that iteration is in a predictable & alphabetical order. - private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = TreeMap() - private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = TreeMap() + private val dumpables: MutableMap<String, DumpableEntry> = TreeMap() + private val buffers: MutableMap<String, LogBufferEntry> = TreeMap() + private val tableLogBuffers: MutableMap<String, TableLogBufferEntry> = TreeMap() /** See [registerCriticalDumpable]. */ fun registerCriticalDumpable(module: Dumpable) { @@ -77,14 +80,14 @@ open class DumpManager @Inject constructor() { * Register a dumpable to be called during a bug report. * * @param name The name to register the dumpable under. This is typically the qualified class - * name of the thing being dumped (getClass().getName()), but can be anything as long as it - * doesn't clash with an existing registration. + * name of the thing being dumped (getClass().getName()), but can be anything as long as it + * doesn't clash with an existing registration. * @param priority the priority level of this dumpable, which affects at what point in the bug - * report this gets dump. By default, the dumpable will be called during the CRITICAL section of - * the bug report, so don't dump an excessive amount of stuff here. + * report this gets dump. By default, the dumpable will be called during the CRITICAL section + * of the bug report, so don't dump an excessive amount of stuff here. * * TODO(b/259973758): Replace all calls to this method with calls to [registerCriticalDumpable] - * or [registerNormalDumpable] instead. + * or [registerNormalDumpable] instead. */ @Synchronized @JvmOverloads @@ -98,7 +101,7 @@ open class DumpManager @Inject constructor() { throw IllegalArgumentException("'$name' is already registered") } - dumpables[name] = RegisteredDumpable(name, module, priority) + dumpables[name] = DumpableEntry(module, name, priority) } /** @@ -110,217 +113,62 @@ open class DumpManager @Inject constructor() { registerDumpable(module::class.java.simpleName, module) } - /** - * Unregisters a previously-registered dumpable. - */ + /** Unregisters a previously-registered dumpable. */ @Synchronized fun unregisterDumpable(name: String) { dumpables.remove(name) } - /** - * Register a [LogBuffer] to be dumped during a bug report. - */ + /** Register a [LogBuffer] to be dumped during a bug report. */ @Synchronized fun registerBuffer(name: String, buffer: LogBuffer) { if (!canAssignToNameLocked(name, buffer)) { throw IllegalArgumentException("'$name' is already registered") } - // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of - // data. - buffers[name] = RegisteredDumpable(name, buffer, DumpPriority.NORMAL) - } - - /** - * Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends - * with [target]. - */ - @Synchronized - fun dumpTarget( - target: String, - pw: PrintWriter, - args: Array<String>, - tailLength: Int, - ) { - sequence { - findBestTargetMatch(dumpables, target)?.let { - yield(it.name to { dumpDumpable(it, pw, args) }) - } - findBestTargetMatch(buffers, target)?.let { - yield(it.name to { dumpBuffer(it, pw, tailLength) }) - } - }.sortedBy { it.first }.minByOrNull { it.first.length }?.second?.invoke() - } - - @Synchronized - fun dumpProtoTarget( - target: String, - protoDump: SystemUIProtoDump, - args: Array<String> - ) { - findBestProtoTargetMatch(dumpables, target)?.let { - dumpProtoDumpable(it, protoDump, args) - } + buffers[name] = LogBufferEntry(buffer, name) } + /** Register a [TableLogBuffer] to be dumped during a bugreport */ @Synchronized - fun dumpProtoDumpables( - systemUIProtoDump: SystemUIProtoDump, - args: Array<String> - ) { - for (dumpable in dumpables.values) { - if (dumpable.dumpable is ProtoDumpable) { - dumpProtoDumpable( - dumpable.dumpable, - systemUIProtoDump, - args - ) - } - } - } - - /** - * Dumps all registered dumpables with critical priority to [pw] - */ - @Synchronized - fun dumpCritical(pw: PrintWriter, args: Array<String>) { - for (dumpable in dumpables.values) { - if (dumpable.priority == DumpPriority.CRITICAL) { - dumpDumpable(dumpable, pw, args) - } - } - } - - /** - * To [pw], dumps (1) all registered dumpables with normal priority; and (2) all [LogBuffer]s. - */ - @Synchronized - fun dumpNormal(pw: PrintWriter, args: Array<String>, tailLength: Int = 0) { - for (dumpable in dumpables.values) { - if (dumpable.priority == DumpPriority.NORMAL) { - dumpDumpable(dumpable, pw, args) - } - } - - for (buffer in buffers.values) { - dumpBuffer(buffer, pw, tailLength) + fun registerTableLogBuffer(name: String, buffer: TableLogBuffer) { + if (!canAssignToNameLocked(name, buffer)) { + throw IllegalArgumentException("'$name' is already registered") } - } - /** - * Dump all the instances of [Dumpable]. - */ - @Synchronized - fun dumpDumpables(pw: PrintWriter, args: Array<String>) { - for (module in dumpables.values) { - dumpDumpable(module, pw, args) - } + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of + // data. + tableLogBuffers[name] = TableLogBufferEntry(buffer, name) } - /** - * Dumps the names of all registered dumpables (one per line) - */ - @Synchronized - fun listDumpables(pw: PrintWriter) { - for (module in dumpables.values) { - pw.println(module.name) - } - } + @Synchronized fun getDumpables(): Collection<DumpableEntry> = dumpables.values.toList() - /** - * Dumps all registered [LogBuffer]s to [pw] - */ - @Synchronized - fun dumpBuffers(pw: PrintWriter, tailLength: Int) { - for (buffer in buffers.values) { - dumpBuffer(buffer, pw, tailLength) - } - } + @Synchronized fun getLogBuffers(): Collection<LogBufferEntry> = buffers.values.toList() - /** - * Dumps the names of all registered buffers (one per line) - */ @Synchronized - fun listBuffers(pw: PrintWriter) { - for (buffer in buffers.values) { - pw.println(buffer.name) - } - } + fun getTableLogBuffers(): Collection<TableLogBufferEntry> = tableLogBuffers.values.toList() @Synchronized fun freezeBuffers() { for (buffer in buffers.values) { - buffer.dumpable.freeze() + buffer.buffer.freeze() } } @Synchronized fun unfreezeBuffers() { for (buffer in buffers.values) { - buffer.dumpable.unfreeze() + buffer.buffer.unfreeze() } } - private fun dumpDumpable( - dumpable: RegisteredDumpable<Dumpable>, - pw: PrintWriter, - args: Array<String> - ) { - pw.println() - pw.println("${dumpable.name}:") - pw.println("----------------------------------------------------------------------------") - dumpable.dumpable.dump(pw, args) - } - - private fun dumpBuffer( - buffer: RegisteredDumpable<LogBuffer>, - pw: PrintWriter, - tailLength: Int - ) { - pw.println() - pw.println() - pw.println("BUFFER ${buffer.name}:") - pw.println("============================================================================") - buffer.dumpable.dump(pw, tailLength) - } - - private fun dumpProtoDumpable( - protoDumpable: ProtoDumpable, - systemUIProtoDump: SystemUIProtoDump, - args: Array<String> - ) { - protoDumpable.dumpProto(systemUIProtoDump, args) - } - private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean { - val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable + val existingDumpable = + dumpables[name]?.dumpable ?: buffers[name]?.buffer ?: tableLogBuffers[name]?.table return existingDumpable == null || newDumpable == existingDumpable } - - private fun <V : Any> findBestTargetMatch(map: Map<String, V>, target: String): V? = map - .asSequence() - .filter { it.key.endsWith(target) } - .minByOrNull { it.key.length } - ?.value - - private fun findBestProtoTargetMatch( - map: Map<String, RegisteredDumpable<Dumpable>>, - target: String - ): ProtoDumpable? = map - .asSequence() - .filter { it.key.endsWith(target) } - .filter { it.value.dumpable is ProtoDumpable } - .minByOrNull { it.key.length } - ?.value?.dumpable as? ProtoDumpable } -private data class RegisteredDumpable<T>( - val name: String, - val dumpable: T, - val priority: DumpPriority, -) - /** * The priority level for a given dumpable, which affects at what point in the bug report this gets * dumped. diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt new file mode 100644 index 000000000000..cd3e1bb7acac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt @@ -0,0 +1,62 @@ +/* + * 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.dump + +import com.android.systemui.Dumpable +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.table.TableLogBuffer + +/** + * A DumpsysEntry is a named, registered entry tracked by [DumpManager] which can be addressed and + * used both in a bugreport / dumpsys invocation or in an individual CLI implementation. + * + * The idea here is that we define every type that [DumpManager] knows about and defines the minimum + * shared interface between each type. So far, just [name] and [priority]. This way, [DumpManager] + * can just store them in separate maps and do the minimal amount of work to discriminate between + * them. + * + * Individual consumers can request these participants in a list via the relevant get* methods on + * [DumpManager] + */ +sealed interface DumpsysEntry { + val name: String + val priority: DumpPriority + + data class DumpableEntry( + val dumpable: Dumpable, + override val name: String, + override val priority: DumpPriority, + ) : DumpsysEntry + + data class LogBufferEntry( + val buffer: LogBuffer, + override val name: String, + ) : DumpsysEntry { + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of + // data. + override val priority: DumpPriority = DumpPriority.NORMAL + } + + data class TableLogBufferEntry( + val table: TableLogBuffer, + override val name: String, + ) : DumpsysEntry { + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of + // data. + override val priority: DumpPriority = DumpPriority.NORMAL + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt index 2d5c9ae2e641..25b90bebf0d1 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt @@ -20,6 +20,7 @@ import android.content.Context import android.icu.text.SimpleDateFormat import android.util.Log import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpHandler.Companion.dumpEntries import com.android.systemui.log.LogBuffer import com.android.systemui.util.io.Files import com.android.systemui.util.time.SystemClock @@ -48,20 +49,21 @@ class LogBufferEulogizer( private val files: Files, private val logPath: Path, private val minWriteGap: Long, - private val maxLogAgeToDump: Long + private val maxLogAgeToDump: Long, ) { - @Inject constructor( + @Inject + constructor( context: Context, dumpManager: DumpManager, systemClock: SystemClock, - files: Files + files: Files, ) : this( dumpManager, systemClock, files, Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"), MIN_WRITE_GAP, - MAX_AGE_TO_DUMP + MAX_AGE_TO_DUMP, ) /** @@ -91,7 +93,8 @@ class LogBufferEulogizer( pw.println() pw.println("Dump triggered by exception:") reason.printStackTrace(pw) - dumpManager.dumpBuffers(pw, 0) + val buffers = dumpManager.getLogBuffers() + dumpEntries(buffers, pw) duration = systemClock.uptimeMillis() - start pw.println() pw.println("Buffer eulogy took ${duration}ms") @@ -105,16 +108,17 @@ class LogBufferEulogizer( return reason } - /** - * If a eulogy file is present, writes its contents to [pw]. - */ + /** If a eulogy file is present, writes its contents to [pw]. */ fun readEulogyIfPresent(pw: PrintWriter) { try { val millisSinceLastWrite = getMillisSinceLastWrite(logPath) if (millisSinceLastWrite > maxLogAgeToDump) { - Log.i(TAG, "Not eulogizing buffers; they are " + + Log.i( + TAG, + "Not eulogizing buffers; they are " + TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) + - " hours old") + " hours old" + ) return } @@ -122,9 +126,7 @@ class LogBufferEulogizer( pw.println() pw.println() pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============") - s.forEach { line -> - pw.println(line) - } + s.forEach { line -> pw.println(line) } } } catch (e: IOException) { // File doesn't exist, okay @@ -134,12 +136,13 @@ class LogBufferEulogizer( } private fun getMillisSinceLastWrite(path: Path): Long { - val stats = try { - files.readAttributes(path, BasicFileAttributes::class.java) - } catch (e: IOException) { - // File doesn't exist - null - } + val stats = + try { + files.readAttributes(path, BasicFileAttributes::class.java) + } catch (e: IOException) { + // File doesn't exist + null + } return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0) } } @@ -147,4 +150,4 @@ class LogBufferEulogizer( private const val TAG = "BufferEulogizer" private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5) private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48) -private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
\ No newline at end of file +private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 280710755ff6..c71775b19e90 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -125,6 +125,7 @@ import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; import com.android.systemui.scrim.ScrimDrawable; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -250,6 +251,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene protected Handler mMainHandler; private int mSmallestScreenWidthDp; private final Optional<CentralSurfaces> mCentralSurfacesOptional; + private final ShadeController mShadeController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DialogLaunchAnimator mDialogLaunchAnimator; @@ -360,6 +362,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene @Main Handler handler, PackageManager packageManager, Optional<CentralSurfaces> centralSurfacesOptional, + ShadeController shadeController, KeyguardUpdateMonitor keyguardUpdateMonitor, DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; @@ -392,6 +395,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMainHandler = handler; mSmallestScreenWidthDp = resources.getConfiguration().smallestScreenWidthDp; mCentralSurfacesOptional = centralSurfacesOptional; + mShadeController = shadeController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mDialogLaunchAnimator = dialogLaunchAnimator; @@ -700,7 +704,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mAdapter, mOverflowAdapter, mSysuiColorExtractor, mStatusBarService, mLightBarController, mNotificationShadeWindowController, this::onRefresh, mKeyguardShowing, - mPowerAdapter, mUiEventLogger, mCentralSurfacesOptional, mKeyguardUpdateMonitor, + mPowerAdapter, mUiEventLogger, mCentralSurfacesOptional, + mShadeController, + mKeyguardUpdateMonitor, mLockPatternUtils); dialog.setOnDismissListener(this); @@ -2205,6 +2211,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private UiEventLogger mUiEventLogger; private GestureDetector mGestureDetector; private Optional<CentralSurfaces> mCentralSurfacesOptional; + private final ShadeController mShadeController; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private LockPatternUtils mLockPatternUtils; private float mWindowDimAmount; @@ -2278,6 +2285,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Runnable onRefreshCallback, boolean keyguardShowing, MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger, Optional<CentralSurfaces> centralSurfacesOptional, + ShadeController shadeController, KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils) { // We set dismissOnDeviceLock to false because we have a custom broadcast receiver to @@ -2295,6 +2303,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mKeyguardShowing = keyguardShowing; mUiEventLogger = uiEventLogger; mCentralSurfacesOptional = centralSurfacesOptional; + mShadeController = shadeController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mGestureDetector = new GestureDetector(mContext, mGestureListener); @@ -2342,12 +2351,10 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mUiEventLogger.log(GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE); if (mCentralSurfacesOptional.map(CentralSurfaces::isKeyguardShowing).orElse(false)) { // match existing lockscreen behavior to open QS when swiping from status bar - mCentralSurfacesOptional.ifPresent( - centralSurfaces -> centralSurfaces.animateExpandSettingsPanel(null)); + mShadeController.animateExpandQs(); } else { // otherwise, swiping down should expand notification shade - mCentralSurfacesOptional.ifPresent( - centralSurfaces -> centralSurfaces.animateExpandNotificationsPanel()); + mShadeController.animateExpandShade(); } dismiss(); } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java index 8125d70a6ab3..68dc1b3dc7d7 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java @@ -98,7 +98,7 @@ public class ShutdownUi { window.setBackgroundDrawable(background); window.setWindowAnimations(com.android.systemui.R.style.Animation_ShutdownUi); - d.setContentView(getShutdownDialogContent()); + d.setContentView(getShutdownDialogContent(isReboot)); d.setCancelable(false); int color; @@ -129,7 +129,12 @@ public class ShutdownUi { d.show(); } - public int getShutdownDialogContent() { + /** + * Returns the layout resource to use for UI while shutting down. + * @param isReboot Whether this is a reboot or a shutdown. + * @return + */ + public int getShutdownDialogContent(boolean isReboot) { return R.layout.shutdown_dialog; } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 5a8c2253b185..94227bccfced 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -19,6 +19,7 @@ package com.android.systemui.keyguard; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; @@ -182,7 +183,8 @@ public class KeyguardService extends Service { // Wrap Keyguard going away animation. // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy). - public static IRemoteTransition wrap(IRemoteAnimationRunner runner) { + public static IRemoteTransition wrap(final KeyguardViewMediator keyguardViewMediator, + final IRemoteAnimationRunner runner, final boolean lockscreenLiveWallpaperEnabled) { return new IRemoteTransition.Stub() { private final ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = new ArrayMap<>(); @@ -213,7 +215,9 @@ public class KeyguardService extends Service { } } initAlphaForAnimationTargets(t, apps); - initAlphaForAnimationTargets(t, wallpapers); + if (lockscreenLiveWallpaperEnabled) { + initAlphaForAnimationTargets(t, wallpapers); + } t.apply(); mFinishCallback = finishCallback; runner.onAnimationStart( @@ -236,6 +240,12 @@ public class KeyguardService extends Service { SurfaceControl.Transaction candidateT, IBinder currentTransition, IRemoteTransitionFinishedCallback candidateFinishCallback) throws RemoteException { + if ((candidateInfo.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) { + keyguardViewMediator.setPendingLock(true); + keyguardViewMediator.cancelKeyguardExitAnimation(); + return; + } + try { synchronized (mLeashMap) { runner.onAnimationCancelled(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index c706363c9454..68e72c58972b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator +import android.app.WallpaperManager import android.content.Context import android.graphics.Matrix import android.graphics.Rect @@ -50,6 +51,7 @@ import com.android.systemui.shared.system.smartspace.SmartspaceState import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject @@ -148,7 +150,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>, private val statusBarStateController: SysuiStatusBarStateController, private val notificationShadeWindowController: NotificationShadeWindowController, - private val powerManager: PowerManager + private val powerManager: PowerManager, + private val wallpaperManager: WallpaperManager ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() { interface KeyguardUnlockAnimationListener { @@ -171,7 +174,7 @@ class KeyguardUnlockAnimationController @Inject constructor( @JvmDefault fun onUnlockAnimationStarted( playingCannedAnimation: Boolean, - fromWakeAndUnlock: Boolean, + isWakeAndUnlockNotFromDream: Boolean, unlockAnimationStartDelay: Long, unlockAnimationDuration: Long ) {} @@ -204,6 +207,12 @@ class KeyguardUnlockAnimationController @Inject constructor( var playingCannedUnlockAnimation = false /** + * Whether we reached the swipe gesture threshold to dismiss keyguard, or restore it, once + * and should ignore any future changes to the dismiss amount before the animation finishes. + */ + var dismissAmountThresholdsReached = false + + /** * Remote callback provided by Launcher that allows us to control the Launcher's unlock * animation and smartspace. * @@ -582,10 +591,13 @@ class KeyguardUnlockAnimationController @Inject constructor( playCannedUnlockAnimation() } + // Notify if waking from AOD only + val isWakeAndUnlockNotFromDream = biometricUnlockControllerLazy.get().isWakeAndUnlock && + biometricUnlockControllerLazy.get().mode != MODE_WAKE_AND_UNLOCK_FROM_DREAM listeners.forEach { it.onUnlockAnimationStarted( playingCannedUnlockAnimation /* playingCannedAnimation */, - biometricUnlockControllerLazy.get().isWakeAndUnlock /* isWakeAndUnlock */, + isWakeAndUnlockNotFromDream /* isWakeAndUnlockNotFromDream */, CANNED_UNLOCK_START_DELAY /* unlockStartDelay */, LAUNCHER_ICONS_ANIMATION_DURATION_MS /* unlockAnimationDuration */) } @@ -647,6 +659,7 @@ class KeyguardUnlockAnimationController @Inject constructor( @VisibleForTesting fun unlockToLauncherWithInWindowAnimations() { + surfaceBehindAlpha = 1f setSurfaceBehindAppearAmount(1f, wallpapers = false) try { @@ -686,8 +699,10 @@ class KeyguardUnlockAnimationController @Inject constructor( return@postDelayed } - if (wallpaperTargets != null) { - fadeInWallpaper() + if ((wallpaperTargets?.isNotEmpty() == true) && + wallpaperManager.isLockscreenLiveWallpaperEnabled()) { + fadeInWallpaper() + hideKeyguardViewAfterRemoteAnimation() } else { keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation( false /* cancelled */) @@ -758,6 +773,10 @@ class KeyguardUnlockAnimationController @Inject constructor( return } + if (dismissAmountThresholdsReached) { + return + } + if (!keyguardStateController.isShowing) { return } @@ -789,6 +808,11 @@ class KeyguardUnlockAnimationController @Inject constructor( return } + // no-op if we alreaddy reached a threshold. + if (dismissAmountThresholdsReached) { + return + } + // no-op if animation is not requested yet. if (!keyguardViewMediator.get().requestedShowSurfaceBehindKeyguard() || !keyguardViewMediator.get().isAnimatingBetweenKeyguardAndSurfaceBehindOrWillBe) { @@ -803,6 +827,7 @@ class KeyguardUnlockAnimationController @Inject constructor( !keyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture && dismissAmount >= DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD)) { setSurfaceBehindAppearAmount(1f) + dismissAmountThresholdsReached = true keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation( false /* cancelled */) } @@ -937,6 +962,7 @@ class KeyguardUnlockAnimationController @Inject constructor( wallpaperTargets = null playingCannedUnlockAnimation = false + dismissAmountThresholdsReached = false willUnlockWithInWindowLauncherAnimations = false willUnlockWithSmartspaceTransition = false @@ -961,7 +987,7 @@ class KeyguardUnlockAnimationController @Inject constructor( 0 /* fadeOutDuration */ ) } else { - Log.e(TAG, "#hideKeyguardViewAfterRemoteAnimation called when keyguard view is not " + + Log.i(TAG, "#hideKeyguardViewAfterRemoteAnimation called when keyguard view is not " + "showing. Ignoring...") } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index fd00f858a33d..835a491bf92a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -47,6 +47,7 @@ import android.app.BroadcastOptions; import android.app.IActivityTaskManager; import android.app.PendingIntent; import android.app.StatusBarManager; +import android.app.WallpaperManager; import android.app.WindowConfiguration; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; @@ -282,6 +283,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private AlarmManager mAlarmManager; private AudioManager mAudioManager; private StatusBarManager mStatusBarManager; + private WallpaperManager mWallpaperManager; private final IStatusBarService mStatusBarService; private final IBinder mStatusBarDisableToken = new Binder(); private final UserTracker mUserTracker; @@ -1364,11 +1366,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(false /* showing */, true /* forceCallbacks */); } + boolean isLLwpEnabled = getWallpaperManager().isLockscreenLiveWallpaperEnabled(); mKeyguardTransitions.register( - KeyguardService.wrap(getExitAnimationRunner()), - KeyguardService.wrap(getOccludeAnimationRunner()), - KeyguardService.wrap(getOccludeByDreamAnimationRunner()), - KeyguardService.wrap(getUnoccludeAnimationRunner())); + KeyguardService.wrap(this, getExitAnimationRunner(), isLLwpEnabled), + KeyguardService.wrap(this, getOccludeAnimationRunner(), isLLwpEnabled), + KeyguardService.wrap(this, getOccludeByDreamAnimationRunner(), isLLwpEnabled), + KeyguardService.wrap(this, getUnoccludeAnimationRunner(), isLLwpEnabled)); final ContentResolver cr = mContext.getContentResolver(); @@ -1414,6 +1417,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mWorkLockController = new WorkLockActivityController(mContext, mUserTracker); } + // TODO(b/273443374) remove, temporary util to get a feature flag + private WallpaperManager getWallpaperManager() { + if (mWallpaperManager == null) { + mWallpaperManager = mContext.getSystemService(WallpaperManager.class); + } + return mWallpaperManager; + } + @Override public void start() { synchronized (this) { @@ -1921,19 +1932,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } public IRemoteAnimationRunner getExitAnimationRunner() { - return mExitAnimationRunner; + return validatingRemoteAnimationRunner(mExitAnimationRunner); } public IRemoteAnimationRunner getOccludeAnimationRunner() { - return mOccludeAnimationRunner; + return validatingRemoteAnimationRunner(mOccludeAnimationRunner); } public IRemoteAnimationRunner getOccludeByDreamAnimationRunner() { - return mOccludeByDreamAnimationRunner; + return validatingRemoteAnimationRunner(mOccludeByDreamAnimationRunner); } public IRemoteAnimationRunner getUnoccludeAnimationRunner() { - return mUnoccludeAnimationRunner; + return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner); } public boolean isHiding() { @@ -1962,7 +1973,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, startKeyguardExitAnimation(0, 0); } - mPowerGestureIntercepted = mUpdateMonitor.isSecureCameraLaunchedOverKeyguard(); + mPowerGestureIntercepted = + isOccluded && mUpdateMonitor.isSecureCameraLaunchedOverKeyguard(); if (mOccluded != isOccluded) { mOccluded = isOccluded; @@ -2909,6 +2921,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // re-locking. We should just end the surface-behind animation without exiting the // keyguard. The pending lock will be handled by onFinishedGoingToSleep(). finishSurfaceBehindRemoteAnimation(true); + maybeHandlePendingLock(); } else { Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. " + "No pending lock, we should end up unlocked with the app/launcher visible."); @@ -3264,8 +3277,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * Cancel the keyguard exit animation, usually because we were swiping to unlock but WM starts * a new remote animation before finishing the keyguard exit animation. - * - * This will dismiss the keyguard. */ public void cancelKeyguardExitAnimation() { Trace.beginSection("KeyguardViewMediator#cancelKeyguardExitAnimation"); @@ -3438,11 +3449,15 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - private void setPendingLock(boolean hasPendingLock) { + public void setPendingLock(boolean hasPendingLock) { mPendingLock = hasPendingLock; Trace.traceCounter(Trace.TRACE_TAG_APP, "pendingLock", mPendingLock ? 1 : 0); } + private boolean isViewRootReady() { + return mKeyguardViewControllerLazy.get().getViewRootImpl() != null; + } + public void addStateMonitorCallback(IKeyguardStateCallback callback) { synchronized (this) { mKeyguardStateCallbacks.add(callback); @@ -3545,4 +3560,27 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_OCCLUSION); } } + + private IRemoteAnimationRunner validatingRemoteAnimationRunner(IRemoteAnimationRunner wrapped) { + return new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationCancelled() throws RemoteException { + wrapped.onAnimationCancelled(); + } + + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) + throws RemoteException { + if (!isViewRootReady()) { + Log.w(TAG, "Skipping remote animation - view root not ready"); + return; + } + + wrapped.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback); + } + }; + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt index c0a5a51a910d..4dc52ff46707 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt @@ -41,6 +41,8 @@ class BouncerMessageView : LinearLayout { super.onFinishInflate() primaryMessageView = findViewById(R.id.bouncer_primary_message_area) secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area) + primaryMessageView?.disable() + secondaryMessageView?.disable() } fun init(factory: KeyguardMessageAreaController.Factory) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index 44e74e7e339b..9db3c22dff84 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -63,7 +63,7 @@ constructor( val hasCards = response?.walletCards?.isNotEmpty() == true trySendWithFailureLogging( state( - isFeatureEnabled = walletController.isWalletEnabled, + isFeatureEnabled = isWalletAvailable(), hasCard = hasCards, tileIcon = walletController.walletClient.tileIcon, ), @@ -100,7 +100,7 @@ constructor( return when { !walletController.walletClient.isWalletServiceAvailable -> KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice - !walletController.isWalletEnabled || queryCards().isEmpty() -> { + !isWalletAvailable() || queryCards().isEmpty() -> { KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( instructions = listOf( @@ -146,6 +146,11 @@ constructor( } } + private fun isWalletAvailable() = + with(walletController.walletClient) { + isWalletServiceAvailable && isWalletFeatureAvailable + } + private fun state( isFeatureEnabled: Boolean, hasCard: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 100bc596103d..c94aa1151b84 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -47,6 +47,11 @@ import kotlinx.coroutines.flow.filter * To create or modify logic that controls when and how transitions get created, look at * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on * this repository. + * + * To print all transitions to logcat to help with debugging, run this command: + * adb shell settings put global systemui/buffer/KeyguardLog VERBOSE + * + * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag. */ interface KeyguardTransitionRepository { /** diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 19e112487c46..1e2f71f01c35 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -61,7 +61,7 @@ constructor( bgDispatcher, coroutineScope, ) - dumpManager.registerNormalDumpable(name, tableBuffer) + dumpManager.registerTableLogBuffer(name, tableBuffer) tableBuffer.init() return tableBuffer } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt index ff763d81f950..f908481d3912 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt @@ -18,8 +18,8 @@ package com.android.systemui.media.controls.pipeline import android.content.Context import com.android.settingslib.bluetooth.LocalBluetoothManager -import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.LocalMediaManager +import com.android.settingslib.media.ManagerInfoMediaManager import javax.inject.Inject /** Factory to create [LocalMediaManager] objects. */ @@ -31,7 +31,7 @@ constructor( ) { /** Creates a [LocalMediaManager] for the given package. */ fun create(packageName: String): LocalMediaManager { - return InfoMediaManager(context, packageName, null, localBluetoothManager).run { + return ManagerInfoMediaManager(context, packageName, null, localBluetoothManager).run { LocalMediaManager(context, localBluetoothManager, this, packageName) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 516fbf5ca12c..14386c1c0fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -76,7 +76,6 @@ import androidx.constraintlayout.widget.ConstraintSet; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.internal.widget.CachingIconView; @@ -1211,24 +1210,24 @@ public class MediaControlPanel { private TurbulenceNoiseAnimationConfig createTurbulenceNoiseAnimation() { return new TurbulenceNoiseAnimationConfig( - TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT, + /* gridCount= */ 2.14f, TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, - /* noiseMoveSpeedX= */ 0f, + /* noiseMoveSpeedX= */ 0.42f, /* noiseMoveSpeedY= */ 0f, TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), - // We want to add (BlendMode.PLUS) the turbulence noise on top of the album art. - // Thus, set the background color with alpha 0. - /* backgroundColor= */ ColorUtils.setAlphaComponent(Color.BLACK, 0), - TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY, + /* backgroundColor= */ Color.BLACK, + /* opacity= */ 51, /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(), /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(), TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS, - /* easeInDuration= */ 2500f, - /* easeOutDuration= */ 2500f, + /* easeInDuration= */ 1350f, + /* easeOutDuration= */ 1350f, this.getContext().getResources().getDisplayMetrics().density, - BlendMode.PLUS, - /* onAnimationEnd= */ null + BlendMode.SCREEN, + /* onAnimationEnd= */ null, + /* lumaMatteBlendFactor= */ 0.26f, + /* lumaMatteOverallBrightness= */ 0.09f ); } private void clearButton(final ImageButton button) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index abf0932c8407..b4578e97eda2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -41,6 +41,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.drawable.IconCompat; +import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.media.BluetoothMediaDevice; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.qrcode.QrCodeGenerator; @@ -58,6 +59,17 @@ import com.google.zxing.WriterException; public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { private static final String TAG = "MediaOutputBroadcastDialog"; + static final int METADATA_BROADCAST_NAME = 0; + static final int METADATA_BROADCAST_CODE = 1; + + private static final int MAX_BROADCAST_INFO_UPDATE = 3; + @VisibleForTesting + static final int BROADCAST_CODE_MAX_LENGTH = 16; + @VisibleForTesting + static final int BROADCAST_CODE_MIN_LENGTH = 4; + @VisibleForTesting + static final int BROADCAST_NAME_MAX_LENGTH = 254; + private ViewStub mBroadcastInfoArea; private ImageView mBroadcastQrCodeView; private ImageView mBroadcastNotify; @@ -67,14 +79,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { private ImageView mBroadcastCodeEye; private Boolean mIsPasswordHide = true; private ImageView mBroadcastCodeEdit; - private AlertDialog mAlertDialog; + @VisibleForTesting + AlertDialog mAlertDialog; private TextView mBroadcastErrorMessage; private int mRetryCount = 0; private String mCurrentBroadcastName; private String mCurrentBroadcastCode; private boolean mIsStopbyUpdateBroadcastCode = false; + private boolean mIsLeBroadcastAssistantCallbackRegistered; - private TextWatcher mTextWatcher = new TextWatcher() { + private TextWatcher mBroadcastCodeTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Do nothing @@ -102,7 +116,9 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { R.string.media_output_broadcast_code_hint_no_less_than_min); } else if (breakBroadcastCodeRuleTextLengthMoreThanMax) { mBroadcastErrorMessage.setText( - R.string.media_output_broadcast_code_hint_no_more_than_max); + mContext.getResources().getString( + R.string.media_output_broadcast_edit_hint_no_more_than_max, + BROADCAST_CODE_MAX_LENGTH)); } mBroadcastErrorMessage.setVisibility(breakRule ? View.VISIBLE : View.INVISIBLE); @@ -113,7 +129,40 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } }; - private boolean mIsLeBroadcastAssistantCallbackRegistered; + private TextWatcher mBroadcastNameTextWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(Editable s) { + if (mAlertDialog == null || mBroadcastErrorMessage == null) { + return; + } + boolean breakBroadcastNameRuleTextLengthMoreThanMax = + s.length() > BROADCAST_NAME_MAX_LENGTH; + boolean breakRule = breakBroadcastNameRuleTextLengthMoreThanMax || (s.length() == 0); + + if (breakBroadcastNameRuleTextLengthMoreThanMax) { + mBroadcastErrorMessage.setText( + mContext.getResources().getString( + R.string.media_output_broadcast_edit_hint_no_more_than_max, + BROADCAST_NAME_MAX_LENGTH)); + } + mBroadcastErrorMessage.setVisibility( + breakBroadcastNameRuleTextLengthMoreThanMax ? View.VISIBLE : View.INVISIBLE); + Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (positiveBtn != null) { + positiveBtn.setEnabled(breakRule ? false : true); + } + } + }; private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = new BluetoothLeBroadcastAssistant.Callback() { @@ -186,13 +235,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } }; - static final int METADATA_BROADCAST_NAME = 0; - static final int METADATA_BROADCAST_CODE = 1; - - private static final int MAX_BROADCAST_INFO_UPDATE = 3; - private static final int BROADCAST_CODE_MAX_LENGTH = 16; - private static final int BROADCAST_CODE_MIN_LENGTH = 4; - MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender, MediaOutputController mediaOutputController) { super(context, broadcastSender, mediaOutputController); @@ -391,13 +433,12 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { R.layout.media_output_broadcast_update_dialog, null); final EditText editText = layout.requireViewById(R.id.broadcast_edit_text); editText.setText(editString); - if (isBroadcastCode) { - editText.addTextChangedListener(mTextWatcher); - } + editText.addTextChangedListener( + isBroadcastCode ? mBroadcastCodeTextWatcher : mBroadcastNameTextWatcher); mBroadcastErrorMessage = layout.requireViewById(R.id.broadcast_error_message); mAlertDialog = new Builder(mContext) .setTitle(isBroadcastCode ? R.string.media_output_broadcast_code - : R.string.media_output_broadcast_name) + : R.string.media_output_broadcast_name) .setView(layout) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.media_output_broadcast_dialog_save, @@ -420,7 +461,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { return mMediaOutputController.getBroadcastMetadata(); } - private void updateBroadcastInfo(boolean isBroadcastCode, String updatedString) { + @VisibleForTesting + void updateBroadcastInfo(boolean isBroadcastCode, String updatedString) { Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); if (positiveBtn != null) { positiveBtn.setEnabled(false); @@ -523,16 +565,33 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } private void handleUpdateFailedUi() { - final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - mBroadcastErrorMessage.setVisibility(View.VISIBLE); + if (mAlertDialog == null) { + Log.d(TAG, "handleUpdateFailedUi: mAlertDialog is null"); + return; + } + int errorMessageStringId = -1; + boolean enablePositiveBtn = false; if (mRetryCount < MAX_BROADCAST_INFO_UPDATE) { - if (positiveBtn != null) { - positiveBtn.setEnabled(true); - } - mBroadcastErrorMessage.setText(R.string.media_output_broadcast_update_error); + enablePositiveBtn = true; + errorMessageStringId = R.string.media_output_broadcast_update_error; } else { mRetryCount = 0; - mBroadcastErrorMessage.setText(R.string.media_output_broadcast_last_update_error); + errorMessageStringId = R.string.media_output_broadcast_last_update_error; } + + // update UI + final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (positiveBtn != null && enablePositiveBtn) { + positiveBtn.setEnabled(true); + } + if (mBroadcastErrorMessage != null) { + mBroadcastErrorMessage.setVisibility(View.VISIBLE); + mBroadcastErrorMessage.setText(errorMessageStringId); + } + } + + @VisibleForTesting + int getRetryCount() { + return mRetryCount; } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index cc75478ef506..25899e5ae178 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -76,6 +76,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.ManagerInfoMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; @@ -194,7 +195,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mKeyGuardManager = keyGuardManager; mFeatureFlags = featureFlags; mUserTracker = userTracker; - InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); + InfoMediaManager imm = new ManagerInfoMediaManager(mContext, packageName, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); mDialogLaunchAnimator = dialogLaunchAnimator; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index b0fb349083e6..682335e0b419 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -76,7 +76,6 @@ import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.Log; import android.view.Display; -import android.view.DisplayCutout; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.InsetsFrameProvider; @@ -1730,9 +1729,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements tappableElementProvider.setInsetsSize(Insets.NONE); } - final DisplayCutout cutout = userContext.getDisplay().getCutout(); - final int safeInsetsLeft = cutout != null ? cutout.getSafeInsetLeft() : 0; - final int safeInsetsRight = cutout != null ? cutout.getSafeInsetRight() : 0; final int gestureHeight = userContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_gesture_height); final boolean handlingGesture = mEdgeBackGestureHandler.isHandlingGestures(); @@ -1742,19 +1738,23 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mandatoryGestureProvider.setInsetsSize(Insets.of(0, 0, 0, gestureHeight)); } final int gestureInsetsLeft = handlingGesture - ? mEdgeBackGestureHandler.getEdgeWidthLeft() + safeInsetsLeft : 0; + ? mEdgeBackGestureHandler.getEdgeWidthLeft() : 0; final int gestureInsetsRight = handlingGesture - ? mEdgeBackGestureHandler.getEdgeWidthRight() + safeInsetsRight : 0; + ? mEdgeBackGestureHandler.getEdgeWidthRight() : 0; return new InsetsFrameProvider[] { navBarProvider, tappableElementProvider, mandatoryGestureProvider, new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.systemGestures()) .setSource(InsetsFrameProvider.SOURCE_DISPLAY) - .setInsetsSize(Insets.of(gestureInsetsLeft, 0, 0, 0)), + .setInsetsSize(Insets.of(gestureInsetsLeft, 0, 0, 0)) + .setMinimalInsetsSizeInDisplayCutoutSafe( + Insets.of(gestureInsetsLeft, 0, 0, 0)), new InsetsFrameProvider(mInsetsSourceOwner, 1, WindowInsets.Type.systemGestures()) .setSource(InsetsFrameProvider.SOURCE_DISPLAY) .setInsetsSize(Insets.of(0, 0, gestureInsetsRight, 0)) + .setMinimalInsetsSizeInDisplayCutoutSafe( + Insets.of(0, 0, gestureInsetsRight, 0)) }; } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt index c804df8fa555..a256b59ac076 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt @@ -157,17 +157,17 @@ class BackPanel( arrowPaint.color = Utils.getColorAttrDefaultColor(context, if (isDeviceInNightTheme) { - com.android.internal.R.attr.colorAccentPrimary + com.android.internal.R.attr.materialColorOnSecondaryContainer } else { - com.android.internal.R.attr.textColorPrimary + com.android.internal.R.attr.materialColorOnSecondaryFixed } ) arrowBackgroundPaint.color = Utils.getColorAttrDefaultColor(context, if (isDeviceInNightTheme) { - com.android.internal.R.attr.materialColorOnSecondary + com.android.internal.R.attr.materialColorSecondaryContainer } else { - com.android.internal.R.attr.colorAccentSecondary + com.android.internal.R.attr.materialColorSecondaryFixedDim } ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 59b94b7c4bd4..d2568ac79105 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -52,8 +52,8 @@ import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepositor import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.phone.AutoTileManager; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.settings.SecureSettings; @@ -66,7 +66,6 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Predicate; @@ -108,7 +107,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P private AutoTileManager mAutoTiles; private final ArrayList<QSFactory> mQsFactories = new ArrayList<>(); private int mCurrentUser; - private final Optional<CentralSurfaces> mCentralSurfacesOptional; + private final ShadeController mShadeController; private Context mUserContext; private UserTracker mUserTracker; private SecureSettings mSecureSettings; @@ -129,7 +128,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, - Optional<CentralSurfaces> centralSurfacesOptional, + ShadeController shadeController, QSLogger qsLogger, UserTracker userTracker, SecureSettings secureSettings, @@ -148,7 +147,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mUserFileManager = userFileManager; mFeatureFlags = featureFlags; - mCentralSurfacesOptional = centralSurfacesOptional; + mShadeController = shadeController; mQsFactories.add(defaultFactory); pluginManager.addPluginListener(this, QSFactory.class, true); @@ -209,17 +208,17 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P @Override public void collapsePanels() { - mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateCollapsePanels); + mShadeController.postAnimateCollapseShade(); } @Override public void forceCollapsePanels() { - mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateForceCollapsePanels); + mShadeController.postAnimateForceCollapseShade(); } @Override public void openPanels() { - mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateOpenPanels); + mShadeController.postAnimateExpandQs(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt index 260caa767a5e..fa6de8dcdafe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt @@ -16,8 +16,7 @@ package com.android.systemui.qs.pipeline.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.phone.CentralSurfaces -import java.util.Optional +import com.android.systemui.shade.ShadeController import javax.inject.Inject /** Encapsulates business logic for interacting with the QS panel. */ @@ -37,17 +36,17 @@ interface PanelInteractor { class PanelInteractorImpl @Inject constructor( - private val centralSurfaces: Optional<CentralSurfaces>, + private val shadeController: ShadeController, ) : PanelInteractor { override fun collapsePanels() { - centralSurfaces.ifPresent { it.postAnimateCollapsePanels() } + shadeController.postAnimateCollapseShade() } override fun forceCollapsePanels() { - centralSurfaces.ifPresent { it.postAnimateForceCollapsePanels() } + shadeController.postAnimateForceCollapseShade() } override fun openPanels() { - centralSurfaces.ifPresent { it.postAnimateOpenPanels() } + shadeController.postAnimateExpandQs() } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index bb38b30339ef..e7dde6617964 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -360,7 +360,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void toggleNotificationPanel() { verifyCallerAndClearCallingIdentityPostMain("toggleNotificationPanel", () -> - mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::togglePanel)); + mCommandQueue.togglePanel()); } private boolean verifyCaller(String reason) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index e1ac0fd1fd16..2c4555a2378a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -427,6 +427,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList Log.e(TAG, "stopRecording called, but there was an error when ending" + "recording"); exception.printStackTrace(); + createErrorNotification(); } catch (Throwable throwable) { // Something unexpected happen, SystemUI will crash but let's delete // the temporary files anyway diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 3227ef47f733..bd1b7ca7916a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -137,7 +137,12 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Since Quick Share target recommendation does not rely on image URL, it is // queried and surfaced before image compress/export. Action intent would not be // used, because it does not contain image URL. - queryQuickShareAction(image, mParams.owner); + Notification.Action quickShare = + queryQuickShareAction(mScreenshotId, image, mParams.owner, null); + if (quickShare != null) { + mQuickShareData.quickShareAction = quickShare; + mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); + } } // Call synchronously here since already on a background thread. @@ -176,9 +181,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { smartActionsEnabled); mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri, smartActionsEnabled); - mImageData.quickShareAction = createQuickShareAction(mContext, - mQuickShareData.quickShareAction, uri); - mImageData.subject = getSubjectString(); + mImageData.quickShareAction = createQuickShareAction( + mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image, + mParams.owner); + mImageData.subject = getSubjectString(mImageTime); mParams.mActionsReadyListener.onActionsReady(mImageData); if (DEBUG_CALLBACK) { @@ -251,7 +257,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}), new ClipData.Item(uri)); sharingIntent.setClipData(clipdata); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString()); + sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(mImageTime)); sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); @@ -417,60 +423,73 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } /** - * Populate image uri into intent of Quick Share action. + * Wrap the quickshare intent and populate the fillin intent with the URI */ @VisibleForTesting - private Notification.Action createQuickShareAction(Context context, Notification.Action action, - Uri uri) { - if (action == null) { + Notification.Action createQuickShareAction( + Notification.Action quickShare, String screenshotId, Uri uri, long imageTime, + Bitmap image, UserHandle user) { + if (quickShare == null) { return null; + } else if (quickShare.actionIntent.isImmutable()) { + Notification.Action quickShareWithUri = + queryQuickShareAction(screenshotId, image, user, uri); + if (quickShareWithUri == null + || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) { + return null; + } + quickShare = quickShareWithUri; } - // Populate image URI into Quick Share chip intent - Intent sharingIntent = action.actionIntent.getIntent(); - sharingIntent.setType("image/png"); - sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); - String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); - String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - // Include URI in ClipData also, so that grantPermission picks it up. - // We don't use setData here because some apps interpret this as "to:". - ClipData clipdata = new ClipData(new ClipDescription("content", - new String[]{"image/png"}), - new ClipData.Item(uri)); - sharingIntent.setClipData(clipdata); - sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PendingIntent updatedPendingIntent = PendingIntent.getActivity( - context, 0, sharingIntent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions. - Bundle extras = action.getExtras(); + + Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class) + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent) + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT_FILLIN, + createFillInIntent(uri, imageTime)) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + Bundle extras = quickShare.getExtras(); String actionType = extras.getString( ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - Intent intent = new Intent(context, SmartActionsReceiver.class) - .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); // We only query for quick share actions when smart actions are enabled, so we can assert // that it's true here. - addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); - PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, - mRandom.nextInt(), - intent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - return new Notification.Action.Builder(action.getIcon(), action.title, - broadcastIntent).setContextual(true).addExtras(extras).build(); + addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */); + PendingIntent broadcastIntent = + PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title, + broadcastIntent) + .setContextual(true) + .addExtras(extras) + .build(); + } + + private Intent createFillInIntent(Uri uri, long imageTime) { + Intent fillIn = new Intent(); + fillIn.setType("image/png"); + fillIn.putExtra(Intent.EXTRA_STREAM, uri); + fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime)); + // Include URI in ClipData also, so that grantPermission picks it up. + // We don't use setData here because some apps interpret this as "to:". + ClipData clipData = new ClipData( + new ClipDescription("content", new String[]{"image/png"}), + new ClipData.Item(uri)); + fillIn.setClipData(clipData); + fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return fillIn; } /** * Query and surface Quick Share chip if it is available. Action intent would not be used, * because it does not contain image URL which would be populated in {@link - * #createQuickShareAction(Context, Notification.Action, Uri)} + * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)} */ - private void queryQuickShareAction(Bitmap image, UserHandle user) { + + @VisibleForTesting + Notification.Action queryQuickShareAction( + String screenshotId, Bitmap image, UserHandle user, Uri uri) { CompletableFuture<List<Notification.Action>> quickShareActionsFuture = mScreenshotSmartActions.getSmartActionsFuture( - mScreenshotId, null, image, mSmartActionsProvider, + screenshotId, uri, image, mSmartActionsProvider, ScreenshotSmartActionType.QUICK_SHARE_ACTION, true /* smartActionsEnabled */, user); int timeoutMs = DeviceConfig.getInt( @@ -479,17 +498,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 500); List<Notification.Action> quickShareActions = mScreenshotSmartActions.getSmartActions( - mScreenshotId, quickShareActionsFuture, timeoutMs, + screenshotId, quickShareActionsFuture, timeoutMs, mSmartActionsProvider, ScreenshotSmartActionType.QUICK_SHARE_ACTION); if (!quickShareActions.isEmpty()) { - mQuickShareData.quickShareAction = quickShareActions.get(0); - mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); + return quickShareActions.get(0); } + return null; } - private String getSubjectString() { - String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); + private static String getSubjectString(long imageTime) { + String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime)); return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 77a65b22a7f4..b59106efb769 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -246,6 +246,7 @@ public class ScreenshotController { static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition"; static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; + static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin"; static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java index 68b46d2b7525..ca713feafe80 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java @@ -30,7 +30,6 @@ import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -61,7 +60,6 @@ public class ScreenshotSmartActions { screenshotNotificationSmartActionsProviderProvider; } - @VisibleForTesting CompletableFuture<List<Notification.Action>> getSmartActionsFuture( String screenshotId, Uri screenshotUri, Bitmap image, ScreenshotNotificationSmartActionsProvider smartActionsProvider, @@ -83,7 +81,7 @@ public class ScreenshotSmartActions { if (image.getConfig() != Bitmap.Config.HARDWARE) { if (DEBUG_ACTIONS) { Log.d(TAG, String.format("Bitmap expected: Hardware, Bitmap found: %s. " - + "Returning empty list.", image.getConfig())); + + "Returning empty list.", image.getConfig())); } return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -112,7 +110,6 @@ public class ScreenshotSmartActions { return smartActionsFuture; } - @VisibleForTesting List<Notification.Action> getSmartActions(String screenshotId, CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs, ScreenshotNotificationSmartActionsProvider smartActionsProvider, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java index 45af1874e9db..9761f5931193 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java @@ -18,6 +18,7 @@ package com.android.systemui.screenshot; import static com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS; import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT_FILLIN; import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_TYPE; import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; @@ -46,7 +47,9 @@ public class SmartActionsReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); + PendingIntent pendingIntent = + intent.getParcelableExtra(EXTRA_ACTION_INTENT, PendingIntent.class); + Intent fillIn = intent.getParcelableExtra(EXTRA_ACTION_INTENT_FILLIN, Intent.class); String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); if (DEBUG_ACTIONS) { Log.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); @@ -54,7 +57,7 @@ public class SmartActionsReceiver extends BroadcastReceiver { ActivityOptions opts = ActivityOptions.makeBasic(); try { - pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); + pendingIntent.send(context, 0, fillIn, null, null, null, opts.toBundle()); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Pending intent canceled", e); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 127c415a339a..452fc3904c32 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -224,6 +224,8 @@ import com.android.systemui.util.Utils; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.animation.FlingAnimationUtils; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -234,7 +236,6 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Provider; -import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; @CentralSurfacesComponent.CentralSurfacesScope @@ -939,10 +940,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onUnlockAnimationStarted( boolean playingCannedAnimation, - boolean isWakeAndUnlock, + boolean isWakeAndUnlockNotFromDream, long startDelay, long unlockAnimationDuration) { - unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay); + unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlockNotFromDream, + startDelay); } }); mAlternateBouncerInteractor = alternateBouncerInteractor; @@ -957,7 +959,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void unlockAnimationStarted( boolean playingCannedAnimation, - boolean isWakeAndUnlock, + boolean isWakeAndUnlockNotFromDream, long unlockAnimationStartDelay) { // Disable blurs while we're unlocking so that panel expansion does not // cause blurring. This will eventually be re-enabled by the panel view on @@ -965,7 +967,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // unlock gesture, and we don't want that to cause blurring either. mDepthController.setBlursDisabledForUnlock(mTracking); - if (playingCannedAnimation && !isWakeAndUnlock) { + if (playingCannedAnimation && !isWakeAndUnlockNotFromDream) { // Hide the panel so it's not in the way or the surface behind the // keyguard, which will be appearing. If we're wake and unlocking, the // lock screen is hidden instantly so should not be flung away. @@ -2016,6 +2018,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } updateExpansionAndVisibility(); mNotificationStackScrollLayoutController.setPanelFlinging(false); + // expandImmediate should be always reset at the end of animation + mQsController.setExpandImmediate(false); } private boolean isInContentBounds(float x, float y) { @@ -3454,6 +3458,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @VisibleForTesting void notifyExpandingStarted() { if (!mExpanding) { + DejankUtils.notifyRendererOfExpensiveFrame(mView, "notifyExpandingStarted"); mExpanding = true; mIsExpandingOrCollapsing = true; mQsController.onExpandingStarted(mQsController.getFullyExpanded()); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index c42c2f4fa15a..6480164cdaf5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -63,6 +63,7 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.SystemBarUtils; import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.classifier.Classifier; @@ -958,6 +959,7 @@ public class QuickSettingsController implements Dumpable { // TODO (b/265193930): remove dependency on NPVC mPanelViewControllerLazy.get().cancelHeightAnimator(); // end + DejankUtils.notifyRendererOfExpensiveFrame(mPanelView, "onExpansionStarted"); // Reset scroll position and apply that position to the expanded height. float height = mExpansionHeight; diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java index d0a3cbbf0b02..29c4acc792ca 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java @@ -38,23 +38,38 @@ public interface ShadeController { /** Collapse the shade instantly with no animation. */ void instantCollapseShade(); - /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */ void animateCollapseShade(); - /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */ void animateCollapseShade(int flags); - /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */ void animateCollapseShadeForced(); - /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ - void animateCollapseShadeDelayed(); + /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */ + void animateCollapseShadeForcedDelayed(); /** * Collapse the shade animated, showing the bouncer when on {@link StatusBarState#KEYGUARD} or * dismissing status bar when on {@link StatusBarState#SHADE}. */ - void animateCollapsePanels(int flags, boolean force, boolean delayed, float speedUpFactor); + void animateCollapseShade(int flags, boolean force, boolean delayed, float speedUpFactor); + + /** Expand the shade with an animation. */ + void animateExpandShade(); + + /** Expand the shade with quick settings expanded with an animation. */ + void animateExpandQs(); + + /** Posts a request to collapse the shade. */ + void postAnimateCollapseShade(); + + /** Posts a request to force collapse the shade. */ + void postAnimateForceCollapseShade(); + + /** Posts a request to expand the shade to quick settings. */ + void postAnimateExpandQs(); /** * If the shade is not fully expanded, collapse it animated. @@ -115,6 +130,9 @@ public interface ShadeController { */ void collapseShade(boolean animate); + /** Calls #collapseShade if already on the main thread. If not, posts a call to it. */ + void collapseOnMainThread(); + /** Makes shade expanded but not visible. */ void makeExpandedInvisible(); @@ -127,8 +145,11 @@ public interface ShadeController { /** Handle status bar touch event. */ void onStatusBarTouch(MotionEvent event); - /** Called when the shade finishes collapsing. */ - void onClosingFinished(); + /** Called when a launch animation was cancelled. */ + void onLaunchAnimationCancelled(boolean isLaunchForActivity); + + /** Called when a launch animation ends. */ + void onLaunchAnimationEnd(boolean launchIsFullScreen); /** Sets the listener for when the visibility of the shade changes. */ void setVisibilityListener(ShadeVisibilityListener listener); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index d00dab633014..7942b588866a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -17,6 +17,7 @@ package com.android.systemui.shade; import android.content.ComponentCallbacks2; +import android.os.Looper; import android.util.Log; import android.view.MotionEvent; import android.view.ViewTreeObserver; @@ -25,6 +26,7 @@ import android.view.WindowManagerGlobal; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationPresenter; @@ -32,12 +34,14 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; import dagger.Lazy; import java.util.ArrayList; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -51,11 +55,13 @@ public final class ShadeControllerImpl implements ShadeController { private final int mDisplayId; private final CommandQueue mCommandQueue; + private final Executor mMainExecutor; private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarStateController mStatusBarStateController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final StatusBarWindowController mStatusBarWindowController; + private final DeviceProvisionedController mDeviceProvisionedController; private final Lazy<AssistManager> mAssistManagerLazy; private final Lazy<NotificationGutsManager> mGutsManager; @@ -72,18 +78,22 @@ public final class ShadeControllerImpl implements ShadeController { @Inject public ShadeControllerImpl( CommandQueue commandQueue, + @Main Executor mainExecutor, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, StatusBarWindowController statusBarWindowController, + DeviceProvisionedController deviceProvisionedController, NotificationShadeWindowController notificationShadeWindowController, WindowManager windowManager, Lazy<AssistManager> assistManagerLazy, Lazy<NotificationGutsManager> gutsManager ) { mCommandQueue = commandQueue; + mMainExecutor = mainExecutor; mStatusBarStateController = statusBarStateController; mStatusBarWindowController = statusBarWindowController; + mDeviceProvisionedController = deviceProvisionedController; mGutsManager = gutsManager; mNotificationShadeWindowController = notificationShadeWindowController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; @@ -107,21 +117,21 @@ public final class ShadeControllerImpl implements ShadeController { @Override public void animateCollapseShade(int flags) { - animateCollapsePanels(flags, false, false, 1.0f); + animateCollapseShade(flags, false, false, 1.0f); } @Override public void animateCollapseShadeForced() { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true, false, 1.0f); + animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE, true, false, 1.0f); } @Override - public void animateCollapseShadeDelayed() { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true, true, 1.0f); + public void animateCollapseShadeForcedDelayed() { + animateCollapseShade(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true, true, 1.0f); } @Override - public void animateCollapsePanels(int flags, boolean force, boolean delayed, + public void animateCollapseShade(int flags, boolean force, boolean delayed, float speedUpFactor) { if (!force && mStatusBarStateController.getState() != StatusBarState.SHADE) { runPostCollapseRunnables(); @@ -143,6 +153,25 @@ public final class ShadeControllerImpl implements ShadeController { } @Override + public void animateExpandShade() { + if (!mCommandQueue.panelsEnabled()) { + return; + } + mNotificationPanelViewController.expandToNotifications(); + } + + @Override + public void animateExpandQs() { + if (!mCommandQueue.panelsEnabled()) { + return; + } + // Settings are not available in setup + if (!mDeviceProvisionedController.isCurrentUserSetup()) return; + + mNotificationPanelViewController.expandToQs(); + } + + @Override public boolean closeShadeIfOpen() { if (!mNotificationPanelViewController.isFullyCollapsed()) { mCommandQueue.animateCollapsePanels( @@ -167,6 +196,20 @@ public final class ShadeControllerImpl implements ShadeController { public boolean isExpandingOrCollapsing() { return mNotificationPanelViewController.isExpandingOrCollapsing(); } + @Override + public void postAnimateCollapseShade() { + mMainExecutor.execute(this::animateCollapseShade); + } + + @Override + public void postAnimateForceCollapseShade() { + mMainExecutor.execute(this::animateCollapseShadeForced); + } + + @Override + public void postAnimateExpandQs() { + mMainExecutor.execute(this::animateExpandQs); + } @Override public void postOnShadeExpanded(Runnable executable) { @@ -202,7 +245,7 @@ public final class ShadeControllerImpl implements ShadeController { public boolean collapseShade() { if (!mNotificationPanelViewController.isFullyCollapsed()) { // close the shade if it was open - animateCollapseShadeDelayed(); + animateCollapseShadeForcedDelayed(); notifyVisibilityChanged(false); return true; @@ -227,6 +270,15 @@ public final class ShadeControllerImpl implements ShadeController { } @Override + public void collapseOnMainThread() { + if (Looper.getMainLooper().isCurrentThread()) { + collapseShade(); + } else { + mMainExecutor.execute(this::collapseShade); + } + } + + @Override public void onStatusBarTouch(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { if (mExpandedVisible) { @@ -235,8 +287,7 @@ public final class ShadeControllerImpl implements ShadeController { } } - @Override - public void onClosingFinished() { + private void onClosingFinished() { runPostCollapseRunnables(); if (!mPresenter.isPresenterFullyCollapsed()) { // if we set it not to be focusable when collapsing, we have to undo it when we aborted @@ -246,6 +297,27 @@ public final class ShadeControllerImpl implements ShadeController { } @Override + public void onLaunchAnimationCancelled(boolean isLaunchForActivity) { + if (mPresenter.isPresenterFullyCollapsed() + && !mPresenter.isCollapsing() + && isLaunchForActivity) { + onClosingFinished(); + } else { + collapseShade(true /* animate */); + } + } + + @Override + public void onLaunchAnimationEnd(boolean launchIsFullScreen) { + if (!mPresenter.isCollapsing()) { + onClosingFinished(); + } + if (launchIsFullScreen) { + instantCollapseShade(); + } + } + + @Override public void instantCollapseShade() { mNotificationPanelViewController.instantCollapse(); runPostCollapseRunnables(); diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt index 641131e4dcc1..03be88fc31d9 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt @@ -34,6 +34,11 @@ abstract class SmartspaceModule { const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin" /** + * The BcSmartspaceDataPlugin for the standalone weather on dream. + */ + const val DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN = "dream_weather_smartspace_data_plugin" + + /** * The dream smartspace target filter. */ const val DREAM_SMARTSPACE_TARGET_FILTER = "dream_smartspace_target_filter" @@ -62,6 +67,10 @@ abstract class SmartspaceModule { @Named(DREAM_SMARTSPACE_DATA_PLUGIN) abstract fun optionalDreamsBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin? + @BindsOptionalOf + @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN) + abstract fun optionalDreamWeatherSmartspaceDataPlugin(): BcSmartspaceDataPlugin? + @Binds @Named(DREAM_SMARTSPACE_PRECONDITION) abstract fun bindSmartspacePrecondition( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index ae4e19550f8c..a0cbdf3e3e01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -83,7 +83,8 @@ open class BlurUtils @Inject constructor( return } if (lastAppliedBlur == 0 && radius != 0) { - Trace.asyncTraceForTrackBegin(TRACE_TAG_APP, TRACK_NAME, EARLY_WAKEUP_SLICE_NAME, 0) + Trace.asyncTraceForTrackBegin( + TRACE_TAG_APP, TRACK_NAME, "eEarlyWakeup (prepareBlur)", 0) earlyWakeupEnabled = true createTransaction().use { it.setEarlyWakeupStart() @@ -110,7 +111,7 @@ open class BlurUtils @Inject constructor( Trace.asyncTraceForTrackBegin( TRACE_TAG_APP, TRACK_NAME, - EARLY_WAKEUP_SLICE_NAME, + "eEarlyWakeup (applyBlur)", 0 ) it.setEarlyWakeupStart() @@ -159,6 +160,5 @@ open class BlurUtils @Inject constructor( companion object { const val TRACK_NAME = "BlurUtils" - const val EARLY_WAKEUP_SLICE_NAME = "eEarlyWakeup" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 06f43f1eeaa5..39181449aaa0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -56,7 +56,6 @@ import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.WindowManager.KeyboardShortcutsReceiver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; import android.widget.EditText; @@ -337,6 +336,12 @@ public final class KeyboardShortcutListSearch { mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift"); mModifierNames.put(KeyEvent.META_META_ON, "Meta"); mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl"); @@ -411,27 +416,45 @@ public final class KeyboardShortcutListSearch { mKeyCharacterMap = mBackupKeyCharacterMap; } + private boolean mAppShortcutsReceived; + private boolean mImeShortcutsReceived; + @VisibleForTesting void showKeyboardShortcuts(int deviceId) { retrieveKeyCharacterMap(deviceId); - mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() { - @Override - public void onKeyboardShortcutsReceived( - final List<KeyboardShortcutGroup> result) { - // Add specific app shortcuts - if (result.isEmpty()) { - mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false); - } else { - mSpecificAppGroup = reMapToKeyboardShortcutMultiMappingGroup(result); - mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true); - } - mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup); - mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup); - mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup); - mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup); - showKeyboardShortcutSearchList(mFullShortsGroup); + mAppShortcutsReceived = false; + mImeShortcutsReceived = false; + mWindowManager.requestAppKeyboardShortcuts(result -> { + // Add specific app shortcuts + if (result.isEmpty()) { + mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false); + } else { + mSpecificAppGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result)); + mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true); + } + mAppShortcutsReceived = true; + if (mImeShortcutsReceived) { + mergeAndShowKeyboardShortcutsGroups(); } }, deviceId); + mWindowManager.requestImeKeyboardShortcuts(result -> { + // Add specific Ime shortcuts + if (!result.isEmpty()) { + mInputGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result)); + } + mImeShortcutsReceived = true; + if (mAppShortcutsReceived) { + mergeAndShowKeyboardShortcutsGroups(); + } + }, deviceId); + } + + private void mergeAndShowKeyboardShortcutsGroups() { + mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup); + mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup); + mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup); + mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup); + showKeyboardShortcutSearchList(mFullShortsGroup); } // The original data structure is only for 1-to-1 shortcut mapping, so remap the old diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index 43fbc7cbae03..a3fd82e9b140 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -56,7 +56,6 @@ import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.WindowManager.KeyboardShortcutsReceiver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.LinearLayout; @@ -129,6 +128,9 @@ public final class KeyboardShortcuts { private KeyCharacterMap mKeyCharacterMap; private KeyCharacterMap mBackupKeyCharacterMap; + @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null; + @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null; + @VisibleForTesting KeyboardShortcuts(Context context, WindowManager windowManager) { this.mContext = new ContextThemeWrapper( @@ -324,6 +326,12 @@ public final class KeyboardShortcuts { mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift"); mModifierNames.put(KeyEvent.META_META_ON, "Meta"); mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl"); @@ -382,18 +390,36 @@ public final class KeyboardShortcuts { @VisibleForTesting void showKeyboardShortcuts(int deviceId) { retrieveKeyCharacterMap(deviceId); - mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() { - @Override - public void onKeyboardShortcutsReceived( - final List<KeyboardShortcutGroup> result) { - result.add(getSystemShortcuts()); - final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts(); - if (appShortcuts != null) { - result.add(appShortcuts); - } - showKeyboardShortcutsDialog(result); - } - }, deviceId); + mReceivedAppShortcutGroups = null; + mReceivedImeShortcutGroups = null; + mWindowManager.requestAppKeyboardShortcuts( + result -> { + mReceivedAppShortcutGroups = result; + maybeMergeAndShowKeyboardShortcuts(); + }, deviceId); + mWindowManager.requestImeKeyboardShortcuts( + result -> { + mReceivedImeShortcutGroups = result; + maybeMergeAndShowKeyboardShortcuts(); + }, deviceId); + } + + private void maybeMergeAndShowKeyboardShortcuts() { + if (mReceivedAppShortcutGroups == null || mReceivedImeShortcutGroups == null) { + return; + } + List<KeyboardShortcutGroup> shortcutGroups = mReceivedAppShortcutGroups; + shortcutGroups.addAll(mReceivedImeShortcutGroups); + mReceivedAppShortcutGroups = null; + mReceivedImeShortcutGroups = null; + + final KeyboardShortcutGroup defaultAppShortcuts = + getDefaultApplicationShortcuts(); + if (defaultAppShortcuts != null) { + shortcutGroups.add(defaultAppShortcuts); + } + shortcutGroups.add(getSystemShortcuts()); + showKeyboardShortcutsDialog(shortcutGroups); } private void dismissKeyboardShortcuts() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index 0a18f2d89d87..56ea703668d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -188,7 +188,9 @@ constructor( if (animationState.value == ANIMATING_OUT) { coroutineScope.launch { withTimeout(DISAPPEAR_ANIMATION_DURATION) { - animationState.first { it == SHOWING_PERSISTENT_DOT || it == ANIMATION_QUEUED } + animationState.first { + it == SHOWING_PERSISTENT_DOT || it == IDLE || it == ANIMATION_QUEUED + } notifyHidePersistentDot() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 518825cea5e0..cf3903860e94 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -137,6 +137,19 @@ constructor( updateTextColorFromWallpaper() statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount) + + if (regionSamplingEnabled && (!regionSamplers.containsKey(v))) { + var regionSampler = RegionSampler( + v as View, + uiExecutor, + bgExecutor, + regionSamplingEnabled, + isLockscreen = true, + ) { updateTextColorFromRegionSampler() } + initializeTextColors(regionSampler) + regionSamplers[v] = regionSampler + regionSampler.startRegionSampler() + } } override fun onViewDetachedFromWindow(v: View) { @@ -171,23 +184,6 @@ constructor( val filteredTargets = targets.filter(::filterSmartspaceTarget) plugin?.onTargetsAvailable(filteredTargets) - if (!isRegionSamplersCreated) { - for (v in smartspaceViews) { - if (regionSamplingEnabled) { - var regionSampler = RegionSampler( - v as View, - uiExecutor, - bgExecutor, - regionSamplingEnabled, - isLockscreen = true, - ) { updateTextColorFromRegionSampler() } - initializeTextColors(regionSampler) - regionSamplers[v] = regionSampler - regionSampler.startRegionSampler() - } - } - isRegionSamplersCreated = true - } } private val userTrackerCallback = object : UserTracker.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 2fa070ca20b5..706594cee81d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -28,12 +28,9 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges -import com.android.systemui.statusbar.notification.NotifPipelineFlags 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 @@ -50,30 +47,29 @@ import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import java.io.PrintWriter import javax.inject.Inject -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.yield /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section - * headers on the lockscreen. + * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the + * lockscreen. */ @CoordinatorScope class KeyguardCoordinator @@ -86,7 +82,6 @@ constructor( private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val logger: KeyguardCoordinatorLogger, - private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val secureSettings: SecureSettings, @@ -95,6 +90,8 @@ constructor( ) : Coordinator, Dumpable { private val unseenNotifications = mutableSetOf<NotificationEntry>() + private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) + private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { @@ -109,79 +106,131 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) - scope.launch { trackUnseenNotificationsWhileUnlocked() } + scope.launch { trackSeenNotifications() } scope.launch { invalidateWhenUnseenSettingChanges() } dumpManager.registerDumpable(this) } - private suspend fun trackUnseenNotificationsWhileUnlocked() { - // Whether or not we're actively tracking unseen notifications to mark them as seen when - // appropriate. - val isTrackingUnseen: Flow<Boolean> = - keyguardRepository.isKeyguardShowing - // transformLatest so that we can cancel listening to keyguard transitions once - // isKeyguardShowing changes (after a successful transition to the keyguard). - .transformLatest { isShowing -> - if (isShowing) { - // If the keyguard is showing, we're not tracking unseen. - emit(false) - } else { - // If the keyguard stops showing, then start tracking unseen notifications. - emit(true) - // If the screen is turning off, stop tracking, but if that transition is - // cancelled, then start again. - emitAll( - keyguardTransitionRepository.transitions.map { step -> - !step.isScreenTurningOff - } - ) - } - } - // Prevent double emit of `false` caused by transition to AOD, followed by keyguard - // showing + private suspend fun trackSeenNotifications() { + // Whether or not keyguard is visible (or occluded). + val isKeyguardPresent: Flow<Boolean> = + keyguardTransitionRepository.transitions + .map { step -> step.to != KeyguardState.GONE } .distinctUntilChanged() .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } - // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is - // showing again - var clearUnseenOnBeginTracking = false - isTrackingUnseen.collectLatest { trackingUnseen -> - if (!trackingUnseen) { - // Wait for the user to spend enough time on the lock screen before clearing unseen - // set when unlocked - awaitTimeSpentNotDozing(SEEN_TIMEOUT) - clearUnseenOnBeginTracking = true - logger.logSeenOnLockscreen() + // Separately track seen notifications while the device is locked, applying once the device + // is unlocked. + val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() + + // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. + isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean -> + if (isKeyguardPresent) { + // Keyguard is not gone, notifications need to be visible for a certain threshold + // before being marked as seen + trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) } else { - if (clearUnseenOnBeginTracking) { - clearUnseenOnBeginTracking = false - logger.logAllMarkedSeenOnUnlock() - unseenNotifications.clear() + // Mark all seen-while-locked notifications as seen for real. + if (notificationsSeenWhileLocked.isNotEmpty()) { + unseenNotifications.removeAll(notificationsSeenWhileLocked) + logger.logAllMarkedSeenOnUnlock( + seenCount = notificationsSeenWhileLocked.size, + remainingUnseenCount = unseenNotifications.size + ) + notificationsSeenWhileLocked.clear() } unseenNotifFilter.invalidateList("keyguard no longer showing") - trackUnseenNotifications() + // Keyguard is gone, notifications can be immediately marked as seen when they + // become visible. + trackSeenNotificationsWhileUnlocked() } } } - private suspend fun awaitTimeSpentNotDozing(duration: Duration) { - keyguardRepository.isDozing - // Use transformLatest so that the timeout delay is cancelled if the device enters doze, - // and is restarted when doze ends. - .transformLatest { isDozing -> - if (!isDozing) { - delay(duration) - // Signal timeout has completed - emit(Unit) + /** + * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually + * been "seen" while the device is on the keyguard. + */ + private suspend fun trackSeenNotificationsWhileLocked( + notificationsSeenWhileLocked: MutableSet<NotificationEntry>, + ) = coroutineScope { + // Remove removed notifications from the set + launch { + unseenEntryRemoved.collect { entry -> + if (notificationsSeenWhileLocked.remove(entry)) { + logger.logRemoveSeenOnLockscreen(entry) } } - // Suspend until the first emission - .first() + } + // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and + // is restarted when doze ends. + keyguardRepository.isDozing.collectLatest { isDozing -> + if (!isDozing) { + trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) + } + } } - // Track "unseen" notifications, marking them as seen when either shade is expanded or the + /** + * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually + * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen + * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. + */ + private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( + notificationsSeenWhileLocked: MutableSet<NotificationEntry> + ) = coroutineScope { + // All child tracking jobs will be cancelled automatically when this is cancelled. + val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() + + /** + * Wait for the user to spend enough time on the lock screen before removing notification + * from unseen set upon unlock. + */ + suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { + if (notificationsSeenWhileLocked.remove(entry)) { + logger.logResetSeenOnLockscreen(entry) + } + delay(SEEN_TIMEOUT) + notificationsSeenWhileLocked.add(entry) + trackingJobsByEntry.remove(entry) + logger.logSeenOnLockscreen(entry) + } + + /** Stop any unseen tracking when a notification is removed. */ + suspend fun stopTrackingRemovedNotifs(): Nothing = + unseenEntryRemoved.collect { entry -> + trackingJobsByEntry.remove(entry)?.let { + it.cancel() + logger.logStopTrackingLockscreenSeenDuration(entry) + } + } + + /** Start tracking new notifications when they are posted. */ + suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { + unseenEntryAdded.collect { entry -> + logger.logTrackingLockscreenSeenDuration(entry) + // If this is an update, reset the tracking. + trackingJobsByEntry[entry]?.let { + it.cancel() + logger.logResetSeenOnLockscreen(entry) + } + trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } + } + } + + // Start tracking for all notifications that are currently unseen. + logger.logTrackingLockscreenSeenDuration(unseenNotifications) + unseenNotifications.forEach { entry -> + trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } + } + + launch { trackNewUnseenNotifs() } + launch { stopTrackingRemovedNotifs() } + } + + // Track "seen" notifications, marking them as such when either shade is expanded or the // notification becomes heads up. - private suspend fun trackUnseenNotifications() { + private suspend fun trackSeenNotificationsWhileUnlocked() { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } @@ -250,6 +299,7 @@ constructor( ) { logger.logUnseenAdded(entry.key) unseenNotifications.add(entry) + unseenEntryAdded.tryEmit(entry) } } @@ -259,12 +309,14 @@ constructor( ) { logger.logUnseenUpdated(entry.key) unseenNotifications.add(entry) + unseenEntryAdded.tryEmit(entry) } } override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (unseenNotifications.remove(entry)) { logger.logUnseenRemoved(entry.key) + unseenEntryRemoved.tryEmit(entry) } } } @@ -347,6 +399,3 @@ constructor( private val SEEN_TIMEOUT = 5.seconds } } - -private val TransitionStep.isScreenTurningOff: Boolean - get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt index 1f8ec3411bcd..c61281661717 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.UnseenNotificationLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject private const val TAG = "KeyguardCoordinator" @@ -28,11 +29,14 @@ class KeyguardCoordinatorLogger constructor( @UnseenNotificationLog private val buffer: LogBuffer, ) { - fun logSeenOnLockscreen() = + fun logSeenOnLockscreen(entry: NotificationEntry) = buffer.log( TAG, LogLevel.DEBUG, - "Notifications on lockscreen will be marked as seen when unlocked." + messageInitializer = { str1 = entry.key }, + messagePrinter = { + "Notification [$str1] on lockscreen will be marked as seen when unlocked." + }, ) fun logTrackingUnseen(trackingUnseen: Boolean) = @@ -43,11 +47,21 @@ constructor( messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, ) - fun logAllMarkedSeenOnUnlock() = + fun logAllMarkedSeenOnUnlock( + seenCount: Int, + remainingUnseenCount: Int, + ) = buffer.log( TAG, LogLevel.DEBUG, - "Notifications have been marked as seen now that device is unlocked." + messageInitializer = { + int1 = seenCount + int2 = remainingUnseenCount + }, + messagePrinter = { + "$int1 Notifications have been marked as seen now that device is unlocked. " + + "$int2 notifications remain unseen." + }, ) fun logShadeExpanded() = @@ -96,4 +110,60 @@ constructor( messageInitializer = { str1 = key }, messagePrinter = { "Unseen notif has become heads up: $str1" }, ) + + fun logTrackingLockscreenSeenDuration(unseenNotifications: Set<NotificationEntry>) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { + str1 = unseenNotifications.joinToString { it.key } + int1 = unseenNotifications.size + }, + messagePrinter = { + "Tracking $int1 unseen notifications for lockscreen seen duration threshold: $str1" + }, + ) + } + + fun logTrackingLockscreenSeenDuration(entry: NotificationEntry) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = entry.key }, + messagePrinter = { + "Tracking new notification for lockscreen seen duration threshold: $str1" + }, + ) + } + + fun logStopTrackingLockscreenSeenDuration(entry: NotificationEntry) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = entry.key }, + messagePrinter = { + "Stop tracking removed notification for lockscreen seen duration threshold: $str1" + }, + ) + } + + fun logResetSeenOnLockscreen(entry: NotificationEntry) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = entry.key }, + messagePrinter = { + "Reset tracking updated notification for lockscreen seen duration threshold: $str1" + }, + ) + } + + fun logRemoveSeenOnLockscreen(entry: NotificationEntry) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = entry.key }, + messagePrinter = { "Notification marked as seen on lockscreen removed: $str1" }, + ) + } } 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 bfb6fb0fcdc7..9f397fe9ac0c 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 @@ -556,6 +556,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void onNotificationUpdated() { + if (mIsSummaryWithChildren) { + Trace.beginSection("ExpNotRow#onNotifUpdated (summary)"); + } else { + Trace.beginSection("ExpNotRow#onNotifUpdated (leaf)"); + } for (NotificationContentView l : mLayouts) { l.onNotificationUpdated(mEntry); } @@ -591,6 +596,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mUpdateSelfBackgroundOnUpdate = false; updateBackgroundColorsOfSelf(); } + Trace.endSection(); } private void updateBackgroundColorsOfSelf() { @@ -2588,6 +2594,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mIsSummaryWithChildren = mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0; if (mIsSummaryWithChildren) { + Trace.beginSection("ExpNotRow#onChildCountChanged (summary)"); NotificationViewWrapper wrapper = mChildrenContainer.getNotificationViewWrapper(); if (wrapper == null || wrapper.getNotificationHeader() == null) { mChildrenContainer.recreateNotificationHeader(mExpandClickListener, @@ -2599,6 +2606,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateChildrenAppearance(); updateChildrenVisibility(); applyChildrenRoundness(); + if (mIsSummaryWithChildren) { + Trace.endSection(); + } } protected void expandNotification() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 7a2bee91e972..b95018777fea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -148,7 +148,7 @@ public class ExpandableNotificationRowDragController { private void dismissShade() { // Speed up dismissing the shade since the drag needs to be handled by // the shell layer underneath - mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */, + mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */, false /* delayed */, 1.1f /* speedUpFactor */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index 77fd05186090..3e10f2ad52e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.Resources; +import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.TypedValue; import android.view.LayoutInflater; @@ -57,6 +58,7 @@ public class HybridGroupManager { } private HybridNotificationView inflateHybridView(View contentView, ViewGroup parent) { + Trace.beginSection("HybridGroupManager#inflateHybridView"); LayoutInflater inflater = LayoutInflater.from(mContext); int layout = contentView instanceof ConversationLayout ? R.layout.hybrid_conversation_notification @@ -64,6 +66,7 @@ public class HybridGroupManager { HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate(layout, parent, false); parent.addView(hybrid); + Trace.endSection(); return hybrid; } @@ -90,12 +93,18 @@ public class HybridGroupManager { public HybridNotificationView bindFromNotification(HybridNotificationView reusableView, View contentView, StatusBarNotification notification, ViewGroup parent) { + boolean isNewView = false; if (reusableView == null) { + Trace.beginSection("HybridGroupManager#bindFromNotification"); reusableView = inflateHybridView(contentView, parent); + isNewView = true; } CharSequence titleText = resolveTitle(notification.getNotification()); CharSequence contentText = resolveText(notification.getNotification()); reusableView.bind(titleText, contentText, contentView); + if (isNewView) { + Trace.endSection(); + } return reusableView; } 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 451d837b63a0..124df8c3b815 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 @@ -26,6 +26,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.RemoteException; +import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.util.AttributeSet; @@ -1193,6 +1194,7 @@ public class NotificationContentView extends FrameLayout implements Notification private void updateSingleLineView() { if (mIsChildInGroup) { + Trace.beginSection("NotifContentView#updateSingleLineView"); boolean isNewView = mSingleLineView == null; mSingleLineView = mHybridGroupManager.bindFromNotification( mSingleLineView, mContractedChild, mNotificationEntry.getSbn(), this); @@ -1200,6 +1202,7 @@ public class NotificationContentView extends FrameLayout implements Notification updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); } + Trace.endSection(); } else if (mSingleLineView != null) { removeView(mSingleLineView); mSingleLineView = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index d18757d3453f..f8e374de11e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -379,6 +379,7 @@ public class NotificationChildrenContainer extends ViewGroup } public void recreateNotificationHeader(OnClickListener listener, boolean isConversation) { + Trace.beginSection("NotifChildCont#recreateHeader"); mHeaderClickListener = listener; mIsConversation = isConversation; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); @@ -406,6 +407,7 @@ public class NotificationChildrenContainer extends ViewGroup recreateLowPriorityHeader(builder, isConversation); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); + Trace.endSection(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index cdc7cee9de5d..36025e8b8d2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -104,13 +104,13 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; +import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.FooterView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -315,7 +315,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } }; private NotificationStackScrollLogger mLogger; - private CentralSurfaces mCentralSurfaces; + private NotificationsController mNotificationsController; private ActivityStarter mActivityStarter; private final int[] mTempInt2 = new int[2]; private boolean mGenerateChildOrderChangedEvent; @@ -3989,7 +3989,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mAmbientState.setExpansionChanging(false); if (!mIsExpanded) { resetScrollPosition(); - mCentralSurfaces.resetUserExpandedStates(); + mNotificationsController.resetUserExpandedStates(); clearTemporaryViews(); clearUserLockedViews(); cancelActiveSwipe(); @@ -4060,6 +4060,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (!mIsExpansionChanging) { cancelActiveSwipe(); } + finalizeClearAllAnimation(); } updateNotificationAnimationStates(); updateChronometers(); @@ -4155,20 +4156,30 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable runAnimationFinishedRunnables(); clearTransient(); clearHeadsUpDisappearRunning(); + finalizeClearAllAnimation(); + } + private void finalizeClearAllAnimation() { if (mAmbientState.isClearAllInProgress()) { setClearAllInProgress(false); if (mShadeNeedsToClose) { mShadeNeedsToClose = false; - postDelayed( - () -> { - mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); - }, - DELAY_BEFORE_SHADE_CLOSE /* delayMillis */); + if (mIsExpanded) { + collapseShadeDelayed(); + } } } } + private void collapseShadeDelayed() { + postDelayed( + () -> { + mShadeController.animateCollapseShade( + CommandQueue.FLAG_EXCLUDE_NONE); + }, + DELAY_BEFORE_SHADE_CLOSE /* delayMillis */); + } + private void clearHeadsUpDisappearRunning() { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); @@ -4376,6 +4387,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable boolean nowHiddenAtAll = mAmbientState.isHiddenAtAll(); if (nowFullyHidden != wasFullyHidden) { updateVisibility(); + mSwipeHelper.resetTouchState(); } if (!wasHiddenAtAll && nowHiddenAtAll) { resetExposedMenuView(true /* animate */, true /* animate */); @@ -4516,6 +4528,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); } + @VisibleForTesting public void setClearAllInProgress(boolean clearAllInProgress) { mClearAllInProgress = clearAllInProgress; mAmbientState.setClearAllInProgress(clearAllInProgress); @@ -4570,8 +4583,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return max + getStackTranslation(); } - public void setCentralSurfaces(CentralSurfaces centralSurfaces) { - this.mCentralSurfaces = centralSurfaces; + public void setNotificationsController(NotificationsController notificationsController) { + this.mNotificationsController = notificationsController; } public void setActivityStarter(ActivityStarter activityStarter) { @@ -4669,6 +4682,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) { mController.updateShowEmptyShadeView(); updateFooter(); + mController.updateImportantForAccessibility(); } updateSpeedBumpIndex(); @@ -4680,6 +4694,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) { mController.updateShowEmptyShadeView(); updateFooter(); + mController.updateImportantForAccessibility(); } updateSpeedBumpIndex(); @@ -4692,6 +4707,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) { mController.updateShowEmptyShadeView(); updateFooter(); + mController.updateImportantForAccessibility(); } updateSpeedBumpIndex(); @@ -5132,7 +5148,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mHeadsUpAppearanceController = headsUpAppearanceController; } - private boolean isVisible(View child) { + @VisibleForTesting + public boolean isVisible(View child) { boolean hasClipBounds = child.getClipBounds(mTmpRect); return child.getVisibility() == View.VISIBLE && (!hasClipBounds || mTmpRect.height() > 0); @@ -5831,7 +5848,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private void cancelActiveSwipe() { - mSwipeHelper.resetSwipeState(); + mSwipeHelper.resetTouchState(); updateContinuousShadowDrawing(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index a52c84300b4d..a70862aead0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -100,6 +100,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStats; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.SilentHeader; +import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -146,6 +147,7 @@ public class NotificationStackScrollLayoutController { private final boolean mAllowLongPress; private final NotificationGutsManager mNotificationGutsManager; + private final NotificationsController mNotificationsController; private final NotificationVisibilityProvider mVisibilityProvider; private final HeadsUpManagerPhone mHeadsUpManager; private final NotificationRoundnessManager mNotificationRoundnessManager; @@ -330,6 +332,7 @@ public class NotificationStackScrollLayoutController { mView.updateSensitiveness(mStatusBarStateController.goingToFullShade(), mLockscreenUserManager.isAnyProfilePublicMode()); mView.onStatePostChange(mStatusBarStateController.fromShadeLocked()); + updateImportantForAccessibility(); } }; @@ -431,7 +434,7 @@ public class NotificationStackScrollLayoutController { @Override public void onSnooze(StatusBarNotification sbn, NotificationSwipeActionHelper.SnoozeOption snoozeOption) { - mCentralSurfaces.setNotificationSnoozed(sbn, snoozeOption); + mNotificationsController.setNotificationSnoozed(sbn, snoozeOption); } @Override @@ -616,6 +619,7 @@ public class NotificationStackScrollLayoutController { NotificationStackScrollLayout view, @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress, NotificationGutsManager notificationGutsManager, + NotificationsController notificationsController, NotificationVisibilityProvider visibilityProvider, HeadsUpManagerPhone headsUpManager, NotificationRoundnessManager notificationRoundnessManager, @@ -664,6 +668,7 @@ public class NotificationStackScrollLayoutController { mLogger = logger; mAllowLongPress = allowLongPress; mNotificationGutsManager = notificationGutsManager; + mNotificationsController = notificationsController; mVisibilityProvider = visibilityProvider; mHeadsUpManager = headsUpManager; mNotificationRoundnessManager = notificationRoundnessManager; @@ -714,7 +719,7 @@ public class NotificationStackScrollLayoutController { mView.setController(this); mView.setLogger(mLogger); mView.setTouchHandler(new TouchHandler()); - mView.setCentralSurfaces(mCentralSurfaces); + mView.setNotificationsController(mNotificationsController); mView.setActivityStarter(mActivityStarter); mView.setClearAllAnimationListener(this::onAnimationEnd); mView.setClearAllListener((selection) -> mUiEventLogger.log( @@ -1211,6 +1216,7 @@ public class NotificationStackScrollLayoutController { if (mView.getVisibility() == View.VISIBLE) { // Synchronize EmptyShadeView visibility with the parent container. updateShowEmptyShadeView(); + updateImportantForAccessibility(); } } @@ -1238,6 +1244,22 @@ public class NotificationStackScrollLayoutController { } /** + * Update the importantForAccessibility of NotificationStackScrollLayout. + * <p> + * We want the NSSL to be unimportant for accessibility when there's no + * notifications in it while the device is on lock screen, to avoid unlablel NSSL view. + * Otherwise, we want it to be important for accessibility to enable accessibility + * auto-scrolling in NSSL. + */ + public void updateImportantForAccessibility() { + if (getVisibleNotificationCount() == 0 && mView.onKeyguard()) { + mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } else { + mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + /** * @return true if {@link StatusBarStateController} is in transition to the KEYGUARD * and false otherwise. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 5ad5d841fad5..217d32c93c6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -389,6 +389,35 @@ constructor( } /** + * Whether we should animate an activity launch. + * + * Note: This method must be called *before* dismissing the keyguard. + */ + private fun shouldAnimateLaunch( + isActivityIntent: Boolean, + showOverLockscreen: Boolean, + ): Boolean { + // TODO(b/184121838): Support launch animations when occluded. + if (keyguardStateController.isOccluded) { + return false + } + + // Always animate if we are not showing the keyguard or if we animate over the lockscreen + // (without unlocking it). + if (showOverLockscreen || !keyguardStateController.isShowing) { + return true + } + + // We don't animate non-activity launches as they can break the animation. + // TODO(b/184121838): Support non activity launches on the lockscreen. + return isActivityIntent + } + + override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean { + return shouldAnimateLaunch(isActivityIntent, false) + } + + /** * Encapsulates the activity logic for activity starter. * * Logic is duplicated in {@link CentralSurfacesImpl} @@ -419,7 +448,7 @@ constructor( val animate = animationController != null && !willLaunchResolverActivity && - centralSurfaces?.shouldAnimateLaunch(true /* isActivityIntent */) == true + shouldAnimateLaunch(isActivityIntent = true) val animController = wrapAnimationController( animationController = animationController, @@ -538,7 +567,7 @@ constructor( val animate = !willLaunchResolverActivity && animationController != null && - centralSurfaces?.shouldAnimateLaunch(intent.isActivity) == true + shouldAnimateLaunch(intent.isActivity) // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we // run the animation on the keyguard). The animation will take care of (instantly) @@ -595,7 +624,7 @@ constructor( Log.w(TAG, "Sending intent failed: $e") if (!collapse) { // executeRunnableDismissingKeyguard did not collapse for us already. - centralSurfaces?.collapsePanelOnMainThread() + shadeControllerLazy.get().collapseOnMainThread() } // TODO: Dismiss Keyguard. } @@ -637,7 +666,7 @@ constructor( val animate = animationController != null && - centralSurfaces?.shouldAnimateLaunch( + shouldAnimateLaunch( /* isActivityIntent= */ true, showOverLockscreenWhenLocked ) == true @@ -804,7 +833,7 @@ constructor( shadeControllerLazy.get().isExpandedVisible && !statusBarKeyguardViewManagerLazy.get().isBouncerShowing ) { - shadeControllerLazy.get().animateCollapseShadeDelayed() + shadeControllerLazy.get().animateCollapseShadeForcedDelayed() } else { // Do it after DismissAction has been processed to conserve the // needed ordering. @@ -867,7 +896,8 @@ constructor( if (dismissShade) { return StatusBarLaunchAnimatorController( animationController, - it, + it.shadeViewController, + shadeControllerLazy.get(), notifShadeWindowControllerLazy.get(), isLaunchForActivity ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 118e961acd24..0929a4c90613 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -26,7 +26,6 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.PowerManager; import android.os.UserHandle; -import android.service.notification.StatusBarNotification; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; @@ -44,7 +43,6 @@ import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; -import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.qs.QSPanelController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; @@ -187,14 +185,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { return contextForUser.getPackageManager(); } - void animateExpandNotificationsPanel(); - - void animateExpandSettingsPanel(@Nullable String subpanel); - - void collapsePanelOnMainThread(); - - void togglePanel(); - void start(); boolean updateIsKeyguard(); @@ -230,25 +220,10 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { boolean isOccluded(); - //TODO: These can / should probably be moved to NotificationPresenter or ShadeController - void onLaunchAnimationCancelled(boolean isLaunchForActivity); - - void onLaunchAnimationEnd(boolean launchIsFullScreen); - - boolean shouldAnimateLaunch(boolean isActivityIntent, boolean showOverLockscreen); - - boolean shouldAnimateLaunch(boolean isActivityIntent); - boolean isDeviceInVrMode(); NotificationPresenter getPresenter(); - void postAnimateCollapsePanels(); - - void postAnimateForceCollapsePanels(); - - void postAnimateOpenPanels(); - boolean isPanelExpanded(); /** @@ -266,8 +241,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { */ default void onStatusBarTrackpadEvent(MotionEvent event) {} - void animateCollapseQuickSettings(); - /** */ boolean getCommandQueuePanelsEnabled(); @@ -293,8 +266,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void readyForKeyguardDone(); - void resetUserExpandedStates(); - void setLockscreenUser(int newUserId); void showKeyguard(); @@ -383,9 +354,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { boolean isDeviceInteractive(); - void setNotificationSnoozed(StatusBarNotification sbn, - NotificationSwipeActionHelper.SnoozeOption snoozeOption); - void awakenDreams(); void clearNotificationEffects(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 0ccc81981e58..337acb6ef88c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -208,7 +208,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba @Override public void animateCollapsePanels(int flags, boolean force) { - mShadeController.animateCollapsePanels(flags, force, false /* delayed */, + mShadeController.animateCollapseShade(flags, force, false /* delayed */, 1.0f /* speedUpFactor */); } @@ -218,11 +218,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba Log.d(CentralSurfaces.TAG, "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible()); } - if (!mCommandQueue.panelsEnabled()) { - return; - } - - mShadeViewController.expandToNotifications(); + mShadeController.animateExpandShade(); } @Override @@ -231,14 +227,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba Log.d(CentralSurfaces.TAG, "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible()); } - if (!mCommandQueue.panelsEnabled()) { - return; - } - - // Settings are not available in setup - if (!mDeviceProvisionedController.isCurrentUserSetup()) return; - - mShadeViewController.expandToQs(); + mShadeController.animateExpandQs(); } @Override @@ -559,7 +548,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if (mCentralSurfaces.isPanelExpanded()) { mShadeController.animateCollapseShade(); } else { - animateExpandNotificationsPanel(); + mShadeController.animateExpandShade(); } } 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 c220fd2aafe2..a020da584e00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -69,7 +69,6 @@ import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; @@ -173,7 +172,6 @@ import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanelController; @@ -412,24 +410,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mQSPanelController; } - /** */ - @Override - public void animateExpandNotificationsPanel() { - mCommandQueueCallbacks.animateExpandNotificationsPanel(); - } - - /** */ - @Override - public void animateExpandSettingsPanel(@Nullable String subpanel) { - mCommandQueueCallbacks.animateExpandSettingsPanel(subpanel); - } - - /** */ - @Override - public void togglePanel() { - mCommandQueueCallbacks.togglePanel(); - } - /** * The {@link StatusBarState} of the status bar. */ @@ -1823,58 +1803,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mKeyguardStateController.isOccluded(); } - /** A launch animation was cancelled. */ - //TODO: These can / should probably be moved to NotificationPresenter or ShadeController - @Override - public void onLaunchAnimationCancelled(boolean isLaunchForActivity) { - if (mPresenter.isPresenterFullyCollapsed() && !mPresenter.isCollapsing() - && isLaunchForActivity) { - mShadeController.onClosingFinished(); - } else { - mShadeController.collapseShade(true /* animate */); - } - } - - /** A launch animation ended. */ - @Override - public void onLaunchAnimationEnd(boolean launchIsFullScreen) { - if (!mPresenter.isCollapsing()) { - mShadeController.onClosingFinished(); - } - if (launchIsFullScreen) { - mShadeController.instantCollapseShade(); - } - } - - /** - * Whether we should animate an activity launch. - * - * Note: This method must be called *before* dismissing the keyguard. - */ - @Override - public boolean shouldAnimateLaunch(boolean isActivityIntent, boolean showOverLockscreen) { - // TODO(b/184121838): Support launch animations when occluded. - if (isOccluded()) { - return false; - } - - // Always animate if we are not showing the keyguard or if we animate over the lockscreen - // (without unlocking it). - if (showOverLockscreen || !mKeyguardStateController.isShowing()) { - return true; - } - - // We don't animate non-activity launches as they can break the animation. - // TODO(b/184121838): Support non activity launches on the lockscreen. - return isActivityIntent; - } - - /** Whether we should animate an activity launch. */ - @Override - public boolean shouldAnimateLaunch(boolean isActivityIntent) { - return shouldAnimateLaunch(isActivityIntent, false /* showOverLockscreen */); - } - @Override public boolean isDeviceInVrMode() { return mPresenter.isDeviceInVrMode(); @@ -1931,21 +1859,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public void postAnimateCollapsePanels() { - mMainExecutor.execute(mShadeController::animateCollapseShade); - } - - @Override - public void postAnimateForceCollapsePanels() { - mMainExecutor.execute(mShadeController::animateCollapseShadeForced); - } - - @Override - public void postAnimateOpenPanels() { - mMessageRouter.sendMessage(MSG_OPEN_SETTINGS_PANEL); - } - - @Override public boolean isPanelExpanded() { return mPanelExpanded; } @@ -1971,14 +1884,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mCentralSurfacesComponent.getNotificationPanelViewController().handleExternalTouch(event); } - @Override - public void animateCollapseQuickSettings() { - if (mState == StatusBarState.SHADE) { - mShadeSurface.collapse( - true, false /* delayed */, 1.0f /* speedUpFactor */); - } - } - private void onExpandedInvisible() { setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); if (!mNotificationActivityStarter.isCollapsingToShowActivityOverLockscreen()) { @@ -2314,7 +2219,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowController.setNotTouchable(false); } finishBarAnimations(); - resetUserExpandedStates(); + mNotificationsController.resetUserExpandedStates(); } Trace.endSection(); } @@ -2333,11 +2238,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } }; - @Override - public void resetUserExpandedStates() { - mNotificationsController.resetUserExpandedStates(); - } - /** * Notify the shade controller that the current user changed * @@ -2963,19 +2863,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } /** - * Collapse the panel directly if we are on the main thread, post the collapsing on the main - * thread if we are not. - */ - @Override - public void collapsePanelOnMainThread() { - if (Looper.getMainLooper().isCurrentThread()) { - mShadeController.collapseShade(); - } else { - mContext.getMainExecutor().execute(mShadeController::collapseShade); - } - } - - /** * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example, * from the power button). * @param wakingUp Whether we're updating because we're waking up (true) or going to sleep @@ -3577,12 +3464,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override - public void setNotificationSnoozed(StatusBarNotification sbn, SnoozeOption snoozeOption) { - mNotificationsController.setNotificationSnoozed(sbn, snoozeOption); - } - - - @Override public void awakenDreams() { mUiBgExecutor.execute(() -> { try { @@ -3651,7 +3532,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void onShadeVisibilityChanged(boolean visible) { if (mVisible != visible) { mVisible = visible; - if (!visible) { + if (visible) { + DejankUtils.notifyRendererOfExpensiveFrame( + mNotificationShadeWindowView, "onShadeVisibilityChanged"); + } else { mGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */); } @@ -3780,8 +3664,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (userSetup != mUserSetup) { mUserSetup = userSetup; - if (!mUserSetup) { - animateCollapseQuickSettings(); + if (!mUserSetup && mState == StatusBarState.SHADE) { + mShadeSurface.collapse(true /* animate */, false /* delayed */, + 1.0f /* speedUpFactor */); } updateQsExpansionEnabled(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 00ac3f4cdc11..b67ec581f8a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -3,6 +3,8 @@ package com.android.systemui.statusbar.phone import android.view.View import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.NotificationShadeWindowController /** @@ -11,7 +13,8 @@ import com.android.systemui.statusbar.NotificationShadeWindowController */ class StatusBarLaunchAnimatorController( private val delegate: ActivityLaunchAnimator.Controller, - private val centralSurfaces: CentralSurfaces, + private val shadeViewController: ShadeViewController, + private val shadeController: ShadeController, private val notificationShadeWindowController: NotificationShadeWindowController, private val isLaunchForActivity: Boolean = true ) : ActivityLaunchAnimator.Controller by delegate { @@ -23,25 +26,25 @@ class StatusBarLaunchAnimatorController( override fun onIntentStarted(willAnimate: Boolean) { delegate.onIntentStarted(willAnimate) if (willAnimate) { - centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(true) + shadeViewController.setIsLaunchAnimationRunning(true) } else { - centralSurfaces.collapsePanelOnMainThread() + shadeController.collapseOnMainThread() } } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { delegate.onLaunchAnimationStart(isExpandingFullyAbove) - centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(true) + shadeViewController.setIsLaunchAnimationRunning(true) if (!isExpandingFullyAbove) { - centralSurfaces.shadeViewController.collapseWithDuration( + shadeViewController.collapseWithDuration( ActivityLaunchAnimator.TIMINGS.totalDuration.toInt()) } } override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { delegate.onLaunchAnimationEnd(isExpandingFullyAbove) - centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(false) - centralSurfaces.onLaunchAnimationEnd(isExpandingFullyAbove) + shadeViewController.setIsLaunchAnimationRunning(false) + shadeController.onLaunchAnimationEnd(isExpandingFullyAbove) } override fun onLaunchAnimationProgress( @@ -50,12 +53,12 @@ class StatusBarLaunchAnimatorController( linearProgress: Float ) { delegate.onLaunchAnimationProgress(state, progress, linearProgress) - centralSurfaces.shadeViewController.applyLaunchAnimationProgress(linearProgress) + shadeViewController.applyLaunchAnimationProgress(linearProgress) } override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) { delegate.onLaunchAnimationCancelled() - centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(false) - centralSurfaces.onLaunchAnimationCancelled(isLaunchForActivity) + shadeViewController.setIsLaunchAnimationRunning(false) + shadeController.onLaunchAnimationCancelled(isLaunchForActivity) } }
\ No newline at end of file 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 28ec1ac5e593..f79a08173bf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -122,7 +122,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte private final CentralSurfaces mCentralSurfaces; private final NotificationPresenter mPresenter; - private final ShadeViewController mNotificationPanel; + private final ShadeViewController mShadeViewController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final ActivityLaunchAnimator mActivityLaunchAnimator; private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; @@ -158,7 +158,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte OnUserInteractionCallback onUserInteractionCallback, CentralSurfaces centralSurfaces, NotificationPresenter presenter, - ShadeViewController panel, + ShadeViewController shadeViewController, NotificationShadeWindowController notificationShadeWindowController, ActivityLaunchAnimator activityLaunchAnimator, NotificationLaunchAnimatorControllerProvider notificationAnimationProvider, @@ -193,7 +193,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte // TODO: use KeyguardStateController#isOccluded to remove this dependency mCentralSurfaces = centralSurfaces; mPresenter = presenter; - mNotificationPanel = panel; + mShadeViewController = shadeViewController; mActivityLaunchAnimator = activityLaunchAnimator; mNotificationAnimationProvider = notificationAnimationProvider; mUserTracker = userTracker; @@ -237,7 +237,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent, mLockscreenUserManager.getCurrentUserId()); final boolean animate = !willLaunchResolverActivity - && mCentralSurfaces.shouldAnimateLaunch(isActivityIntent); + && mActivityStarter.shouldAnimateLaunch(isActivityIntent); boolean showOverLockscreen = mKeyguardStateController.isShowing() && intent != null && mActivityIntentHelper.wouldPendingShowOverLockscreen(intent, mLockscreenUserManager.getCurrentUserId()); @@ -288,7 +288,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte } // Always defer the keyguard dismiss when animating. - return animate || !mNotificationPanel.isFullyCollapsed(); + return animate || !mShadeViewController.isFullyCollapsed(); } private void handleNotificationClickAfterPanelCollapsed( @@ -323,7 +323,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte removeHunAfterClick(row); // Show work challenge, do not run PendingIntent and // remove notification - collapseOnMainThread(); + mShadeController.collapseOnMainThread(); return; } } @@ -440,7 +440,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte ActivityLaunchAnimator.Controller animationController = new StatusBarLaunchAnimatorController( mNotificationAnimationProvider.getAnimatorController(row, null), - mCentralSurfaces, + mShadeViewController, + mShadeController, mNotificationShadeWindowController, isActivityIntent); mActivityLaunchAnimator.startPendingIntentWithAnimation( @@ -472,7 +473,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte @Override public void startNotificationGutsIntent(final Intent intent, final int appUid, ExpandableNotificationRow row) { - boolean animate = mCentralSurfaces.shouldAnimateLaunch(true /* isActivityIntent */); + boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -480,7 +481,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte ActivityLaunchAnimator.Controller animationController = new StatusBarLaunchAnimatorController( mNotificationAnimationProvider.getAnimatorController(row), - mCentralSurfaces, + mShadeViewController, + mShadeController, mNotificationShadeWindowController, true /* isActivityIntent */); @@ -507,7 +509,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte @Override public void startHistoryIntent(View view, boolean showHistory) { - boolean animate = mCentralSurfaces.shouldAnimateLaunch(true /* isActivityIntent */); + boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -529,7 +531,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte viewController == null ? null : new StatusBarLaunchAnimatorController( viewController, - mCentralSurfaces, + mShadeViewController, + mShadeController, mNotificationShadeWindowController, true /* isActivityIntent */); @@ -630,11 +633,4 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte return true; } - private void collapseOnMainThread() { - if (Looper.getMainLooper().isCurrentThread()) { - mShadeController.collapseShade(); - } else { - mMainThreadHandler.post(mShadeController::collapseShade); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index d585163aa223..02b7e9176cf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -21,7 +21,6 @@ import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD; import android.app.ActivityManager; -import android.app.Notification; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.ColorStateList; @@ -47,7 +46,6 @@ import android.view.OnReceiveContentListener; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; -import android.view.ViewGroupOverlay; import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; @@ -58,6 +56,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -436,25 +435,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (animate && parent != null && mIsFocusAnimationFlagActive) { ViewGroup grandParent = (ViewGroup) parent.getParent(); - ViewGroupOverlay overlay = parent.getOverlay(); View actionsContainer = getActionsContainerLayout(); int actionsContainerHeight = actionsContainer != null ? actionsContainer.getHeight() : 0; - // After adding this RemoteInputView to the overlay of the parent (and thus removing - // it from the parent itself), the parent will shrink in height. This causes the - // overlay to be moved. To correct the position of the overlay we need to offset it. - int overlayOffsetY = actionsContainerHeight - getHeight(); - overlay.add(this); + // When defocusing, the notification needs to shrink. Therefore, we need to free + // up the space that was needed for the RemoteInputView. This is done by setting + // a negative top margin of the height difference of the RemoteInputView and its + // sibling (the actions_container_layout containing the Reply button etc.) + final int heightToShrink = actionsContainerHeight - getHeight(); + setTopMargin(heightToShrink); if (grandParent != null) grandParent.setClipChildren(false); - Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); - View self = this; + final Animator animator = getDefocusAnimator(actionsContainer); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - overlay.remove(self); - parent.addView(self); + setTopMargin(0); if (grandParent != null) grandParent.setClipChildren(true); setVisibility(GONE); if (mWrapper != null) { @@ -499,6 +496,13 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } + private void setTopMargin(int topMargin) { + if (!(getLayoutParams() instanceof FrameLayout.LayoutParams)) return; + final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); + layoutParams.topMargin = topMargin; + setLayoutParams(layoutParams); + } + @VisibleForTesting protected void setViewRootImpl(ViewRootImpl viewRoot) { mTestableViewRootImpl = viewRoot; @@ -674,12 +678,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mProgressBar.setVisibility(INVISIBLE); mResetting = true; mSending = false; + mController.removeSpinning(mEntry.getKey(), mToken); onDefocus(true /* animate */, false /* logClose */, () -> { mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); mEditText.getText().clear(); mEditText.setEnabled(isAggregatedVisible()); mSendButton.setVisibility(VISIBLE); - mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); setAttachment(null); mResetting = false; @@ -874,7 +878,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f); scaleAnimator.addUpdateListener(valueAnimator -> { - setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0); + setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); }); scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); @@ -901,9 +905,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene * Creates an animator for the defocus animation. * * @param fadeInView View that will be faded in during the defocus animation. - * @param offsetY The RemoteInputView will be offset by offsetY during the animation */ - private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { + private Animator getDefocusAnimator(@Nullable View fadeInView) { final AnimatorSet animatorSet = new AnimatorSet(); final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); @@ -913,14 +916,14 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); scaleAnimator.addUpdateListener(valueAnimator -> { - setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY); + setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); }); scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); scaleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { - setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */); + setFocusAnimationScaleY(1f /* scaleY */); } }); @@ -942,9 +945,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene * Sets affected view properties for a vertical scale animation * * @param scaleY desired vertical view scale - * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation */ - private void setFocusAnimationScaleY(float scaleY, int verticalOffset) { + private void setFocusAnimationScaleY(float scaleY) { int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight()); Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(), mContentView.getHeight() - verticalBoundOffset); @@ -955,7 +957,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else { mContentBackgroundBounds = contentBackgroundBounds; } - setTranslationY(verticalBoundOffset + verticalOffset); + setTranslationY(verticalBoundOffset); } /** Handler for button click on send action in IME. */ diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt index 8214822f0335..1e73cb3b9b24 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt @@ -1,6 +1,7 @@ package com.android.systemui.unfold import android.os.SystemProperties +import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator import com.android.systemui.dagger.qualifiers.Main @@ -22,6 +23,8 @@ constructor( ) : TransitionProgressListener { private var isFirstAnimationAfterUnfold = false + private val touchVibrationAttributes = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK) init { if (vibrator != null) { @@ -71,7 +74,7 @@ constructor( } private fun playHaptics() { - vibrator?.vibrate(effect) + vibrator?.vibrate(effect, touchVibrationAttributes) } private val hapticsScale: Float diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index 7236e0fd134a..59f2cdb745ca 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -224,7 +224,7 @@ object UserSwitcherViewBinder { } sectionView.removeAllViewsInLayout() - for (viewModel in section) { + section.onEachIndexed { index, viewModel -> val view = layoutInflater.inflate( R.layout.user_switcher_fullscreen_popup_item, @@ -237,6 +237,13 @@ object UserSwitcherViewBinder { view.resources.getString(viewModel.textResourceId) view.setOnClickListener { viewModel.onClicked() } sectionView.addView(view) + // Ensure that the first item in the first section gets accessibility focus. + // Request for focus with a delay when view is inflated an added to the listview. + if (index == 0 && position == 0) { + view.postDelayed({ + view.requestAccessibilityFocus() + }, 200) + } } return sectionView } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index b24a69292186..b848d2e84faf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -2006,14 +2006,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (row.anim == null) { row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); row.anim.setInterpolator(new DecelerateInterpolator()); + row.anim.addListener( + getJankListener(row.view, TYPE_UPDATE, UPDATE_ANIMATION_DURATION)); } else { row.anim.cancel(); row.anim.setIntValues(progress, newProgress); } row.animTargetProgress = newProgress; row.anim.setDuration(UPDATE_ANIMATION_DURATION); - row.anim.addListener( - getJankListener(row.view, TYPE_UPDATE, UPDATE_ANIMATION_DURATION)); row.anim.start(); } else { // update slider directly to clamped value diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt index 14ad3acf7fb0..263d3750c657 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt @@ -26,18 +26,17 @@ import android.text.TextPaint import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import kotlin.math.ceil import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Mockito.`when` import org.mockito.Mockito.eq import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify - -import kotlin.math.ceil +import org.mockito.Mockito.`when` @RunWith(AndroidTestingRunner::class) @SmallTest @@ -56,15 +55,13 @@ class TextAnimatorTest : SysuiTestCase() { val paint = mock(TextPaint::class.java) `when`(textInterpolator.targetPaint).thenReturn(paint) - val textAnimator = TextAnimator(layout, {}).apply { - this.textInterpolator = textInterpolator - this.animator = valueAnimator - } + val textAnimator = + TextAnimator(layout, null, {}).apply { + this.textInterpolator = textInterpolator + this.animator = valueAnimator + } - textAnimator.setTextStyle( - weight = 400, - animate = true - ) + textAnimator.setTextStyle(weight = 400, animate = true) // If animation is requested, the base state should be rebased and the target state should // be updated. @@ -88,15 +85,13 @@ class TextAnimatorTest : SysuiTestCase() { val paint = mock(TextPaint::class.java) `when`(textInterpolator.targetPaint).thenReturn(paint) - val textAnimator = TextAnimator(layout, {}).apply { - this.textInterpolator = textInterpolator - this.animator = valueAnimator - } + val textAnimator = + TextAnimator(layout, null, {}).apply { + this.textInterpolator = textInterpolator + this.animator = valueAnimator + } - textAnimator.setTextStyle( - weight = 400, - animate = false - ) + textAnimator.setTextStyle(weight = 400, animate = false) // If animation is not requested, the progress should be 1 which is end of animation and the // base state is rebased to target state by calling rebase. @@ -118,15 +113,16 @@ class TextAnimatorTest : SysuiTestCase() { `when`(textInterpolator.targetPaint).thenReturn(paint) val animationEndCallback = mock(Runnable::class.java) - val textAnimator = TextAnimator(layout, {}).apply { - this.textInterpolator = textInterpolator - this.animator = valueAnimator - } + val textAnimator = + TextAnimator(layout, null, {}).apply { + this.textInterpolator = textInterpolator + this.animator = valueAnimator + } textAnimator.setTextStyle( - weight = 400, - animate = true, - onAnimationEnd = animationEndCallback + weight = 400, + animate = true, + onAnimationEnd = animationEndCallback ) // Verify animationEnd callback has been added. @@ -144,34 +140,27 @@ class TextAnimatorTest : SysuiTestCase() { val layout = makeLayout("Hello, World", PAINT) val valueAnimator = mock(ValueAnimator::class.java) val textInterpolator = mock(TextInterpolator::class.java) - val paint = TextPaint().apply { - typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf") - } + val paint = + TextPaint().apply { + typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf") + } `when`(textInterpolator.targetPaint).thenReturn(paint) - val textAnimator = TextAnimator(layout, {}).apply { - this.textInterpolator = textInterpolator - this.animator = valueAnimator - } + val textAnimator = + TextAnimator(layout, null, {}).apply { + this.textInterpolator = textInterpolator + this.animator = valueAnimator + } - textAnimator.setTextStyle( - weight = 400, - animate = true - ) + textAnimator.setTextStyle(weight = 400, animate = true) val prevTypeface = paint.typeface - textAnimator.setTextStyle( - weight = 700, - animate = true - ) + textAnimator.setTextStyle(weight = 700, animate = true) assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface) - textAnimator.setTextStyle( - weight = 400, - animate = true - ) + textAnimator.setTextStyle(weight = 400, animate = true) assertThat(paint.typeface).isSameInstanceAs(prevTypeface) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index b9f92a064bc8..b4a4a11a81a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -954,6 +954,25 @@ public class AuthControllerTest extends SysuiTestCase { eq(null) /* credentialAttestation */); } + @Test + public void testShowDialog_whenOwnerNotInForeground() { + PromptInfo promptInfo = createTestPromptInfo(); + promptInfo.setAllowBackgroundAuthentication(false); + switchTask("other_package"); + mAuthController.showAuthenticationDialog(promptInfo, + mReceiver /* receiver */, + new int[]{1} /* sensorIds */, + false /* credentialAllowed */, + true /* requireConfirmation */, + 0 /* userId */, + 0 /* operationId */, + "testPackage", + REQUEST_ID); + + assertNull(mAuthController.mCurrentDialog); + verify(mDialog1, never()).show(any(), any()); + } + private void showDialog(int[] sensorIds, boolean credentialAllowed) { mAuthController.showAuthenticationDialog(createTestPromptInfo(), mReceiver /* receiver */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java index ca6282c66a17..461ec653d819 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java @@ -28,6 +28,7 @@ import androidx.lifecycle.Observer; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dreams.DreamLogger; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; @@ -56,6 +57,8 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase { private FakeFeatureFlags mFeatureFlags; @Mock private Observer mObserver; + @Mock + private DreamLogger mLogger; @Before public void setUp() { @@ -66,7 +69,8 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase { mStateController = new DreamOverlayStateController( mExecutor, /* overlayEnabled= */ true, - mFeatureFlags); + mFeatureFlags, + mLogger); mLiveData = new ComplicationCollectionLiveData(mStateController); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index cfd51e3901bd..c97eedbec45d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -16,6 +16,8 @@ package com.android.systemui.dreams; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -512,4 +514,23 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewController, never()).wakeUp(callback, mMainExecutor); } + + @Test + public void testSystemFlagShowForAllUsersSetOnWindow() throws RemoteException { + final IDreamOverlayClient client = getClient(); + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); + mMainExecutor.runAllReady(); + + final ArgumentCaptor<WindowManager.LayoutParams> paramsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + + // Verify that a new window is added. + verify(mWindowManager).addView(any(), paramsCaptor.capture()); + + assertThat((paramsCaptor.getValue().privateFlags & SYSTEM_FLAG_SHOW_FOR_ALL_USERS) + == SYSTEM_FLAG_SHOW_FOR_ALL_USERS).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java index 7b4160551c82..2c1ebe4121af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -58,6 +58,9 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase { @Mock private FeatureFlags mFeatureFlags; + @Mock + private DreamLogger mLogger; + final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Before @@ -405,6 +408,6 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase { } private DreamOverlayStateController getDreamOverlayStateController(boolean overlayEnabled) { - return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags); + return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags, mLogger); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java index d16b7570e2c3..5dc0e55632fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java @@ -113,6 +113,8 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { DreamOverlayStateController mDreamOverlayStateController; @Mock UserTracker mUserTracker; + @Mock + DreamLogger mLogger; @Captor private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor; @@ -146,7 +148,8 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { mStatusBarWindowStateController, mDreamOverlayStatusBarItemsProvider, mDreamOverlayStateController, - mUserTracker); + mUserTracker, + mLogger); } @Test @@ -289,7 +292,8 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { mStatusBarWindowStateController, mDreamOverlayStatusBarItemsProvider, mDreamOverlayStateController, - mUserTracker); + mUserTracker, + mLogger); controller.onViewAttached(); verify(mView, never()).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index e8cbdf3db327..2830476874ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -22,7 +22,7 @@ import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogBuffer -import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat @@ -45,8 +45,6 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var logBufferEulogizer: LogBufferEulogizer - @Mock - private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager @Mock private lateinit var pw: PrintWriter @@ -70,6 +68,11 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var buffer2: LogBuffer + @Mock + private lateinit var table1: TableLogBuffer + @Mock + private lateinit var table2: TableLogBuffer + private val dumpManager = DumpManager() @Before @@ -83,21 +86,22 @@ class DumpHandlerTest : SysuiTestCase() { mutableMapOf( EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() } ), - exceptionHandlerManager ) } @Test fun testDumpablesCanBeDumpedSelectively() { // GIVEN a variety of registered dumpables and buffers - dumpManager.registerDumpable("dumpable1", dumpable1) - dumpManager.registerDumpable("dumpable2", dumpable2) - dumpManager.registerDumpable("dumpable3", dumpable3) + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN some of them are dumped explicitly - val args = arrayOf("dumpable1", "dumpable3", "buffer2") + val args = arrayOf("dumpable1", "dumpable3", "buffer2", "table2") dumpHandler.dump(fd, pw, args) // THEN only the requested ones have their dump() method called @@ -108,12 +112,14 @@ class DumpHandlerTest : SysuiTestCase() { verify(dumpable3).dump(pw, args) verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) verify(buffer2).dump(pw, 0) + verify(table1, never()).dump(any(), any()) + verify(table2).dump(pw, args) } @Test fun testDumpableMatchingIsBasedOnEndOfTag() { // GIVEN a dumpable registered to the manager - dumpManager.registerDumpable("com.android.foo.bar.dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1) // WHEN that module is dumped val args = arrayOf("dumpable1") @@ -131,6 +137,8 @@ class DumpHandlerTest : SysuiTestCase() { dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN a critical dump is requested val args = arrayOf("--dump-priority", "CRITICAL") @@ -144,6 +152,8 @@ class DumpHandlerTest : SysuiTestCase() { any(Array<String>::class.java)) verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) } @Test @@ -154,6 +164,8 @@ class DumpHandlerTest : SysuiTestCase() { dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN a normal dump is requested val args = arrayOf("--dump-priority", "NORMAL") @@ -169,6 +181,8 @@ class DumpHandlerTest : SysuiTestCase() { verify(dumpable3).dump(pw, args) verify(buffer1).dump(pw, 0) verify(buffer2).dump(pw, 0) + verify(table1).dump(pw, args) + verify(table2).dump(pw, args) } @Test @@ -184,6 +198,81 @@ class DumpHandlerTest : SysuiTestCase() { } @Test + fun testDumpBuffers() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a buffer dump is requested + val args = arrayOf("buffers", "--tail", "1") + dumpHandler.dump(fd, pw, args) + + // THEN all buffers are dumped (and no dumpables or tables) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1).dump(pw, tailLength = 1) + verify(buffer2).dump(pw, tailLength = 1) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) + } + + @Test + fun testDumpDumpables() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a dumpable dump is requested + val args = arrayOf("dumpables") + dumpHandler.dump(fd, pw, args) + + // THEN all dumpables are dumped (both critical and normal) (and no dumpables) + verify(dumpable1).dump(pw, args) + verify(dumpable2).dump(pw, args) + verify(dumpable3).dump(pw, args) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) + } + + @Test + fun testDumpTables() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a dumpable dump is requested + val args = arrayOf("tables") + dumpHandler.dump(fd, pw, args) + + // THEN all dumpables are dumped (both critical and normal) (and no dumpables) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + verify(table1).dump(pw, args) + verify(table2).dump(pw, args) + } + + @Test fun testDumpAllProtoDumpables() { dumpManager.registerDumpable("protoDumpable1", protoDumpable1) dumpManager.registerDumpable("protoDumpable2", protoDumpable2) @@ -207,6 +296,123 @@ class DumpHandlerTest : SysuiTestCase() { verify(protoDumpable2, never()).dumpProto(any(), any()) } + @Test + fun testDumpTarget_selectsShortestNamedDumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("first-dumpable", dumpable1) + dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2) + dumpManager.registerCriticalDumpable("third-dumpable", dumpable3) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("dumpable") + dumpHandler.dump(fd, pw, args) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + } + + @Test + fun testDumpTarget_selectsShortestNamedBuffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerBuffer("first-buffer", buffer1) + dumpManager.registerBuffer("scnd-buffer", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("buffer", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the matching buffer with the shorter name is dumped + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("big-buffer1", buffer1) + dumpManager.registerBuffer("big-buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("2") + dumpHandler.dump(fd, pw, args) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("2", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the matching buffer with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("a3x", dumpable3) + dumpManager.registerBuffer("ab1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("x") + dumpHandler.dump(fd, pw, args) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3).dump(pw, args) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("az1x", dumpable3) + dumpManager.registerBuffer("b1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("x", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1).dump(pw, tailLength = 14) + verify(buffer2, never()).dump(any(), anyInt()) + } + + private class EmptyCoreStartable : CoreStartable { override fun start() {} } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt index 02555cfa783a..6d5226f35e97 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt @@ -20,21 +20,17 @@ import androidx.test.filters.SmallTest import com.android.systemui.Dumpable import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogBuffer -import com.android.systemui.util.mockito.any -import java.io.PrintWriter +import com.android.systemui.log.table.TableLogBuffer +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest class DumpManagerTest : SysuiTestCase() { - @Mock private lateinit var pw: PrintWriter - @Mock private lateinit var dumpable1: Dumpable @Mock private lateinit var dumpable2: Dumpable @Mock private lateinit var dumpable3: Dumpable @@ -42,6 +38,9 @@ class DumpManagerTest : SysuiTestCase() { @Mock private lateinit var buffer1: LogBuffer @Mock private lateinit var buffer2: LogBuffer + @Mock private lateinit var table1: TableLogBuffer + @Mock private lateinit var table2: TableLogBuffer + private val dumpManager = DumpManager() @Before @@ -50,276 +49,144 @@ class DumpManagerTest : SysuiTestCase() { } @Test - fun testDumpTarget_dumpable() { - // GIVEN a variety of registered dumpables and buffers + fun testRegisterUnregister_dumpables() { + // GIVEN a variety of registered dumpables dumpManager.registerCriticalDumpable("dumpable1", dumpable1) dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) - // WHEN a dumpable is dumped explicitly - val args = arrayOf<String>() - dumpManager.dumpTarget("dumpable2", pw, args, tailLength = 0) + // WHEN the collection is requested + var dumpables = dumpManager.getDumpables().map { it.dumpable } - // THEN only the requested one has their dump() method called - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } + // THEN it contains the registered entries + assertThat(dumpables).containsExactly(dumpable1, dumpable2, dumpable3) - @Test - fun testDumpTarget_buffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) + // WHEN the dumpables are unregistered + dumpManager.unregisterDumpable("dumpable2") + dumpManager.unregisterDumpable("dumpable3") - // WHEN a buffer is dumped explicitly - val args = arrayOf<String>() - dumpManager.dumpTarget("buffer1", pw, args, tailLength = 14) + // WHEN the dumpable collection is requests + dumpables = dumpManager.getDumpables().map { it.dumpable } - // THEN only the requested one has their dump() method called - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 14) - verify(buffer2, never()).dump(any(), anyInt()) + // THEN it contains only the currently-registered entry + assertThat(dumpables).containsExactly(dumpable1) } @Test - fun testDumpableMatchingIsBasedOnEndOfTag() { - // GIVEN a dumpable registered to the manager - dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1) + fun testRegister_buffers() { + // GIVEN a set of registered buffers + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) - // WHEN that module is dumped - val args = arrayOf<String>() - dumpManager.dumpTarget("dumpable1", pw, arrayOf(), tailLength = 14) + // WHEN the collection is requested + val dumpables = dumpManager.getLogBuffers().map { it.buffer } - // THEN its dump() method is called - verify(dumpable1).dump(pw, args) + // THEN it contains the registered entries + assertThat(dumpables).containsExactly(buffer1, buffer2) } @Test - fun testDumpTarget_selectsShortestNamedDumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("first-dumpable", dumpable1) - dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2) - dumpManager.registerCriticalDumpable("third-dumpable", dumpable3) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("dumpable", pw, args, tailLength = 0) - - // THEN the matching dumpable with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - } + fun testRegister_tableLogBuffers() { + // GIVEN a set of registered buffers + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) - @Test - fun testDumpTarget_selectsShortestNamedBuffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerBuffer("first-buffer", buffer1) - dumpManager.registerBuffer("scnd-buffer", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("buffer", pw, args, tailLength = 14) - - // THEN the matching buffer with the shorter name is dumped - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2).dump(pw, tailLength = 14) - } + // WHEN the collection is requested + val tables = dumpManager.getTableLogBuffers().map { it.table } - @Test - fun testDumpTarget_selectsShortestNamedMatch_dumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("big-buffer1", buffer1) - dumpManager.registerBuffer("big-buffer2", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("2", pw, args, tailLength = 14) - - // THEN the matching dumpable with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) + // THEN it contains the registered entries + assertThat(tables).containsExactly(table1, table2) } @Test - fun testDumpTarget_selectsShortestNamedMatch_buffer() { - // GIVEN a variety of registered dumpables and buffers + fun registerDumpable_throwsWhenNameCannotBeAssigned() { + // GIVEN dumpable1 and buffer1 and table1 are registered dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("2", pw, args, tailLength = 14) - - // THEN the matching buffer with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2).dump(pw, tailLength = 14) + dumpManager.registerTableLogBuffer("table1", table1) + + // THEN an exception is thrown when trying to re-register a new dumpable under the same key + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerCriticalDumpable("dumpable1", dumpable2) + } + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerBuffer("buffer1", buffer2) + } + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerTableLogBuffer("table1", table2) + } } @Test - fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("d1x", dumpable1) - dumpManager.registerCriticalDumpable("d2x", dumpable2) - dumpManager.registerCriticalDumpable("a3x", dumpable3) - dumpManager.registerBuffer("ab1x", buffer1) - dumpManager.registerBuffer("b2x", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("x", pw, args, tailLength = 14) - - // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3).dump(pw, args) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("d1x", dumpable1) - dumpManager.registerCriticalDumpable("d2x", dumpable2) - dumpManager.registerCriticalDumpable("az1x", dumpable3) - dumpManager.registerBuffer("b1x", buffer1) - dumpManager.registerBuffer("b2x", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf<String>() - dumpManager.dumpTarget("x", pw, args, tailLength = 14) - - // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 14) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpDumpables() { - // GIVEN a variety of registered dumpables and buffers + fun registerDumpable_doesNotThrowWhenReRegistering() { + // GIVEN dumpable1 and buffer1 are registered dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - // WHEN a dumpable dump is requested - val args = arrayOf<String>() - dumpManager.dumpDumpables(pw, args) - - // THEN all dumpables are dumped (both critical and normal) (and no dumpables) - verify(dumpable1).dump(pw, args) - verify(dumpable2).dump(pw, args) - verify(dumpable3).dump(pw, args) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpBuffers() { - // GIVEN a variety of registered dumpables and buffers + // THEN no exception is thrown when trying to re-register a new dumpable under the same key dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a buffer dump is requested - dumpManager.dumpBuffers(pw, tailLength = 1) - // THEN all buffers are dumped (and no dumpables) - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 1) - verify(buffer2).dump(pw, tailLength = 1) + // No exception thrown } @Test - fun testCriticalDump() { - // GIVEN a variety of registered dumpables and buffers + fun getDumpables_returnsSafeCollection() { + // GIVEN a variety of registered dumpables dumpManager.registerCriticalDumpable("dumpable1", dumpable1) dumpManager.registerCriticalDumpable("dumpable2", dumpable2) dumpManager.registerNormalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - // WHEN a critical dump is requested - val args = arrayOf<String>() - dumpManager.dumpCritical(pw, args) + // WHEN the collection is retrieved + val dumpables = dumpManager.getDumpables() - // THEN only critical modules are dumped (and no buffers) - verify(dumpable1).dump(pw, args) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) + // WHEN the collection changes from underneath + dumpManager.unregisterDumpable("dumpable1") + dumpManager.unregisterDumpable("dumpable2") + dumpManager.unregisterDumpable("dumpable3") + + // THEN new collections are empty + assertThat(dumpManager.getDumpables()).isEmpty() + + // AND the collection is still safe to use + assertThat(dumpables).hasSize(3) } @Test - fun testNormalDump() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) + fun getBuffers_returnsSafeCollection() { + // GIVEN a set of registered buffers dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) - // WHEN a normal dump is requested - val args = arrayOf<String>() - dumpManager.dumpNormal(pw, args, tailLength = 2) + // WHEN the collection is requested + val buffers = dumpManager.getLogBuffers() + + // WHEN the collection changes + dumpManager.registerBuffer("buffer3", buffer1) - // THEN the normal module and all buffers are dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3).dump(pw, args) - verify(buffer1).dump(pw, tailLength = 2) - verify(buffer2).dump(pw, tailLength = 2) + // THEN the new entry is represented + assertThat(dumpManager.getLogBuffers()).hasSize(3) + + // AND the previous collection is unchanged + assertThat(buffers).hasSize(2) } @Test - fun testUnregister() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) + fun getTableBuffers_returnsSafeCollection() { + // GIVEN a set of registered buffers + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) - dumpManager.unregisterDumpable("dumpable2") - dumpManager.unregisterDumpable("dumpable3") + // WHEN the collection is requested + val tables = dumpManager.getTableLogBuffers() + + // WHEN the collection changes + dumpManager.registerTableLogBuffer("table3", table1) - // WHEN a dumpables dump is requested - val args = arrayOf<String>() - dumpManager.dumpDumpables(pw, args) + // THEN the new entry is represented + assertThat(dumpManager.getTableLogBuffers()).hasSize(3) - // THEN the unregistered dumpables (both normal and critical) are not dumped - verify(dumpable1).dump(pw, args) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) + // AND the previous collection is unchanged + assertThat(tables).hasSize(2) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt index cb38846a0514..3ff72028d5ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt @@ -18,20 +18,14 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpHandler.Companion.dump +import com.android.systemui.log.LogBuffer import com.android.systemui.util.io.FakeBasicFileAttributes import com.android.systemui.util.io.Files import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import java.io.BufferedWriter import java.io.ByteArrayOutputStream import java.io.IOException @@ -42,17 +36,29 @@ import java.nio.file.OpenOption import java.nio.file.Paths import java.nio.file.attribute.BasicFileAttributes import java.util.Arrays +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest class LogEulogizerTest : SysuiTestCase() { lateinit var eulogizer: LogBufferEulogizer - @Mock - lateinit var dumpManager: DumpManager + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var logBuffer1: LogBuffer + lateinit var logBufferEntry1: DumpsysEntry.LogBufferEntry + @Mock lateinit var logBuffer2: LogBuffer + lateinit var logBufferEntry2: DumpsysEntry.LogBufferEntry - @Mock - lateinit var files: Files + @Mock lateinit var files: Files private val clock = FakeSystemClock() @@ -67,37 +73,47 @@ class LogEulogizerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + logBufferEntry1 = DumpsysEntry.LogBufferEntry(logBuffer1, "logbuffer1") + logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2") - eulogizer = - LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE) + eulogizer = LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE) Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java))) - .thenReturn(fileWriter) + .thenReturn(fileWriter) Mockito.`when`( - files.readAttributes(eq(path), - eq(BasicFileAttributes::class.java), - any(LinkOption::class.java)) - ).thenReturn(fileAttrs) + files.readAttributes( + eq(path), + eq(BasicFileAttributes::class.java), + any(LinkOption::class.java) + ) + ) + .thenReturn(fileAttrs) Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES)) + + whenever(dumpManager.getLogBuffers()).thenReturn(listOf(logBufferEntry1, logBufferEntry2)) } @Test fun testFileIsCreated() { // GIVEN that the log file doesn't already exist Mockito.`when`( - files.readAttributes(eq(path), - eq(BasicFileAttributes::class.java), - any(LinkOption::class.java)) - ).thenThrow(IOException("File not found")) + files.readAttributes( + eq(path), + eq(BasicFileAttributes::class.java), + any(LinkOption::class.java) + ) + ) + .thenThrow(IOException("File not found")) // WHEN .record() is called val exception = RuntimeException("Something bad happened") assertEquals(exception, eulogizer.record(exception)) // THEN the buffers are dumped to the file - verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isNotEmpty()) } @@ -111,7 +127,8 @@ class LogEulogizerTest : SysuiTestCase() { assertEquals(exception, eulogizer.record(exception)) // THEN the buffers are dumped to the file - verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isNotEmpty()) } @@ -125,7 +142,8 @@ class LogEulogizerTest : SysuiTestCase() { assertEquals(exception, eulogizer.record(exception)) // THEN the file isn't written to - verify(dumpManager, never()).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2, never()).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isEmpty()) } @@ -161,9 +179,4 @@ class LogEulogizerTest : SysuiTestCase() { private const val MIN_WRITE_GAP = 10L private const val MAX_READ_AGE = 100L -private val FAKE_LINES = - arrayOf( - "First line", - "Second line", - "Third line" - ) +private val FAKE_LINES = arrayOf("First line", "Second line", "Third line") diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index c9ee1e8ef5b9..6aa5a00c36da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -67,6 +67,7 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.plugins.GlobalActions; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -128,6 +129,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private UserContextProvider mUserContextProvider; @Mock private VibratorHelper mVibratorHelper; @Mock private CentralSurfaces mCentralSurfaces; + @Mock private ShadeController mShadeController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private DialogLaunchAnimator mDialogLaunchAnimator; @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher; @@ -177,6 +179,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mHandler, mPackageManager, Optional.of(mCentralSurfaces), + mShadeController, mKeyguardUpdateMonitor, mDialogLaunchAnimator); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); @@ -317,7 +320,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0); gestureListener.onFling(start, end, 0, 1000); verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE); - verify(mCentralSurfaces).animateExpandSettingsPanel(null); + verify(mShadeController).animateExpandQs(); } @Test @@ -341,7 +344,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0); gestureListener.onFling(start, end, 0, 1000); verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE); - verify(mCentralSurfaces).animateExpandNotificationsPanel(); + verify(mShadeController).animateExpandShade(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt index 477e076669b7..22308414547a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt @@ -1,6 +1,7 @@ package com.android.systemui.keyguard import android.app.ActivityManager +import android.app.WallpaperManager import android.app.WindowConfiguration import android.graphics.Point import android.graphics.Rect @@ -21,6 +22,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat import com.android.systemui.util.mockito.whenever import junit.framework.Assert.assertEquals @@ -32,6 +34,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times @@ -64,6 +67,8 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var powerManager: PowerManager + @Mock + private lateinit var wallpaperManager: WallpaperManager @Mock private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController.Stub @@ -94,13 +99,14 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { keyguardUnlockAnimationController = KeyguardUnlockAnimationController( context, keyguardStateController, { keyguardViewMediator }, keyguardViewController, featureFlags, { biometricUnlockController }, statusBarStateController, - notificationShadeWindowController, powerManager + notificationShadeWindowController, powerManager, wallpaperManager ) keyguardUnlockAnimationController.setLauncherUnlockController( launcherUnlockAnimationController) whenever(keyguardViewController.viewRootImpl).thenReturn(mock(ViewRootImpl::class.java)) whenever(powerManager.isInteractive).thenReturn(true) + whenever(wallpaperManager.isLockscreenLiveWallpaperEnabled).thenReturn(false) // All of these fields are final, so we can't mock them, but are needed so that the surface // appear amount setter doesn't short circuit. @@ -173,6 +179,46 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { false /* cancelled */) } + @Test + fun onWakeAndUnlock_notifiesListenerWithTrue() { + whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true) + whenever(biometricUnlockController.mode).thenReturn( + BiometricUnlockController.MODE_WAKE_AND_UNLOCK) + + val listener = mock( + KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener::class.java) + keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(listener) + + keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation( + remoteAnimationTargets, + wallpaperTargets, + 0 /* startTime */, + false /* requestedShowSurfaceBehindKeyguard */ + ) + + verify(listener).onUnlockAnimationStarted(any(), eq(true), any(), any()) + } + + @Test + fun onWakeAndUnlockFromDream_notifiesListenerWithFalse() { + whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true) + whenever(biometricUnlockController.mode).thenReturn( + BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM) + + val listener = mock( + KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener::class.java) + keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(listener) + + keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation( + remoteAnimationTargets, + wallpaperTargets, + 0 /* startTime */, + false /* requestedShowSurfaceBehindKeyguard */ + ) + + verify(listener).onUnlockAnimationStarted(any(), eq(false), any(), any()) + } + /** * If we requested that the surface behind be made visible, and we're not flinging away the * keyguard, it means that we're swiping to unlock and want the surface visible so it can follow diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index 111b8e83a984..d36e77889810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -92,8 +92,8 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun affordance_walletNotEnabled_modelIsNone() = runBlockingTest { - setUpState(isWalletEnabled = false) + fun affordance_walletFeatureNotEnabled_modelIsNone() = runBlockingTest { + setUpState(isWalletFeatureAvailable = false) var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) @@ -165,7 +165,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_disabledWhenTheFeatureIsNotEnabled() = runTest { setUpState( - isWalletEnabled = false, + isWalletFeatureAvailable = false, ) assertThat(underTest.getPickerScreenState()) @@ -183,16 +183,15 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { } private fun setUpState( - isWalletEnabled: Boolean = true, + isWalletFeatureAvailable: Boolean = true, isWalletServiceAvailable: Boolean = true, isWalletQuerySuccessful: Boolean = true, hasSelectedCard: Boolean = true, ) { - whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled) - val walletClient: QuickAccessWalletClient = mock() whenever(walletClient.tileIcon).thenReturn(ICON) whenever(walletClient.isWalletServiceAvailable).thenReturn(isWalletServiceAvailable) + whenever(walletClient.isWalletFeatureAvailable).thenReturn(isWalletFeatureAvailable) whenever(walletController.walletClient).thenReturn(walletClient) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java index 705b485ce1b4..9dba9b5b3c3e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -16,6 +16,8 @@ package com.android.systemui.media.dialog; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.mock; @@ -33,6 +35,10 @@ import android.media.session.MediaSessionManager; import android.os.PowerExemptionManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; import androidx.test.filters.SmallTest; @@ -44,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.media.BluetoothMediaDevice; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; @@ -53,11 +60,14 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; +import com.google.common.base.Strings; + import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -68,6 +78,9 @@ import java.util.Optional; public class MediaOutputBroadcastDialogTest extends SysuiTestCase { private static final String TEST_PACKAGE = "test_package"; + private static final String BROADCAST_NAME_TEST = "Broadcast_name_test"; + private static final String BROADCAST_CODE_TEST = "112233"; + private static final String BROADCAST_CODE_UPDATE_TEST = "11223344"; // Mock private final MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class); @@ -106,6 +119,9 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null); when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null); + when(mLocalBluetoothLeBroadcast.getProgramInfo()).thenReturn(BROADCAST_NAME_TEST); + when(mLocalBluetoothLeBroadcast.getBroadcastCode()).thenReturn( + BROADCAST_CODE_TEST.getBytes(StandardCharsets.UTF_8)); mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, mMediaSessionManager, mLocalBluetoothManager, mStarter, @@ -194,4 +210,152 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { verify(mLocalBluetoothLeBroadcastAssistant, times(1)).addSource(any(), any(), anyBoolean()); } + + @Test + public void handleLeBroadcastMetadataChanged_checkBroadcastName() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + final TextView broadcastName = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_name_summary); + + mMediaOutputBroadcastDialog.handleLeBroadcastMetadataChanged(); + + assertThat(broadcastName.getText().toString()).isEqualTo(BROADCAST_NAME_TEST); + } + + @Test + public void handleLeBroadcastMetadataChanged_checkBroadcastCode() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + + final TextView broadcastCode = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_code_summary); + + mMediaOutputBroadcastDialog.handleLeBroadcastMetadataChanged(); + + assertThat(broadcastCode.getText().toString()).isEqualTo(BROADCAST_CODE_TEST); + } + + @Test + public void updateBroadcastInfo_stopBroadcastFailed_handleFailedUi() { + ImageView broadcastCodeEdit = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_code_edit); + TextView broadcastCode = mMediaOutputBroadcastDialog.mDialogView.requireViewById( + R.id.broadcast_code_summary); + broadcastCode.setText(BROADCAST_CODE_UPDATE_TEST); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null); + broadcastCodeEdit.callOnClick(); + + mMediaOutputBroadcastDialog.updateBroadcastInfo(true, BROADCAST_CODE_UPDATE_TEST); + assertThat(mMediaOutputBroadcastDialog.getRetryCount()).isEqualTo(1); + + mMediaOutputBroadcastDialog.updateBroadcastInfo(true, BROADCAST_CODE_UPDATE_TEST); + assertThat(mMediaOutputBroadcastDialog.getRetryCount()).isEqualTo(2); + + // It will be the MAX Retry Count = 3 + mMediaOutputBroadcastDialog.updateBroadcastInfo(true, BROADCAST_CODE_UPDATE_TEST); + assertThat(mMediaOutputBroadcastDialog.getRetryCount()).isEqualTo(0); + } + + @Test + public void afterTextChanged_nameLengthMoreThanMax_showErrorMessage() { + ImageView broadcastNameEdit = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_name_edit); + TextView broadcastName = mMediaOutputBroadcastDialog.mDialogView.requireViewById( + R.id.broadcast_name_summary); + broadcastName.setText(BROADCAST_NAME_TEST); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + broadcastNameEdit.callOnClick(); + EditText editText = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_edit_text); + TextView broadcastErrorMessage = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_error_message); + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + + // input the invalid text + String moreThanMax = Strings.repeat("a", + MediaOutputBroadcastDialog.BROADCAST_NAME_MAX_LENGTH + 3); + editText.setText(moreThanMax); + + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void afterTextChanged_enterValidNameAfterLengthMoreThanMax_noErrorMessage() { + ImageView broadcastNameEdit = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_name_edit); + TextView broadcastName = mMediaOutputBroadcastDialog.mDialogView.requireViewById( + R.id.broadcast_name_summary); + broadcastName.setText(BROADCAST_NAME_TEST); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + broadcastNameEdit.callOnClick(); + EditText editText = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_edit_text); + TextView broadcastErrorMessage = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_error_message); + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + + // input the invalid text + String testString = Strings.repeat("a", + MediaOutputBroadcastDialog.BROADCAST_NAME_MAX_LENGTH + 2); + editText.setText(testString); + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.VISIBLE); + + // input the valid text + testString = Strings.repeat("b", + MediaOutputBroadcastDialog.BROADCAST_NAME_MAX_LENGTH - 100); + editText.setText(testString); + + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + } + + @Test + public void afterTextChanged_codeLengthMoreThanMax_showErrorMessage() { + ImageView broadcastCodeEdit = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_code_edit); + TextView broadcastCode = mMediaOutputBroadcastDialog.mDialogView.requireViewById( + R.id.broadcast_code_summary); + broadcastCode.setText(BROADCAST_CODE_UPDATE_TEST); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + broadcastCodeEdit.callOnClick(); + EditText editText = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_edit_text); + TextView broadcastErrorMessage = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_error_message); + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + + // input the invalid text + String moreThanMax = Strings.repeat("a", + MediaOutputBroadcastDialog.BROADCAST_CODE_MAX_LENGTH + 1); + editText.setText(moreThanMax); + + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void afterTextChanged_codeLengthLessThanMin_showErrorMessage() { + ImageView broadcastCodeEdit = mMediaOutputBroadcastDialog.mDialogView + .requireViewById(R.id.broadcast_code_edit); + TextView broadcastCode = mMediaOutputBroadcastDialog.mDialogView.requireViewById( + R.id.broadcast_code_summary); + broadcastCode.setText(BROADCAST_CODE_UPDATE_TEST); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + broadcastCodeEdit.callOnClick(); + EditText editText = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_edit_text); + TextView broadcastErrorMessage = mMediaOutputBroadcastDialog.mAlertDialog.findViewById( + R.id.broadcast_error_message); + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.INVISIBLE); + + // input the invalid text + String moreThanMax = Strings.repeat("a", + MediaOutputBroadcastDialog.BROADCAST_CODE_MIN_LENGTH - 1); + editText.setText(moreThanMax); + + assertThat(broadcastErrorMessage.getVisibility()).isEqualTo(View.VISIBLE); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 810ab344e7d8..d98bcee1e01a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -67,8 +67,8 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.phone.AutoTileManager; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.FakeSharedPreferences; import com.android.systemui.util.concurrency.FakeExecutor; @@ -86,7 +86,6 @@ import org.mockito.stubbing.Answer; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; -import java.util.Optional; import java.util.concurrent.Executor; import javax.inject.Provider; @@ -110,7 +109,7 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private Provider<AutoTileManager> mAutoTiles; @Mock - private CentralSurfaces mCentralSurfaces; + private ShadeController mShadeController; @Mock private QSLogger mQSLogger; @Mock @@ -161,7 +160,7 @@ public class QSTileHostTest extends SysuiTestCase { mSecureSettings = new FakeSettings(); saveSetting(""); mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor, - mPluginManager, mTunerService, mAutoTiles, mCentralSurfaces, + mPluginManager, mTunerService, mAutoTiles, mShadeController, mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags); @@ -682,13 +681,13 @@ public class QSTileHostTest extends SysuiTestCase { QSFactory defaultFactory, Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, - CentralSurfaces centralSurfaces, QSLogger qsLogger, + ShadeController shadeController, QSLogger qsLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileLifecycleManager.Factory tileLifecycleManagerFactory, UserFileManager userFileManager, FeatureFlags featureFlags) { super(context, defaultFactory, mainExecutor, pluginManager, - tunerService, autoTiles, Optional.of(centralSurfaces), qsLogger, + tunerService, autoTiles, shadeController, qsLogger, userTracker, secureSettings, customTileStatePersister, tileLifecycleManagerFactory, userFileManager, featureFlags); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt index 45783abe9ee4..6556cfd22901 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt @@ -18,8 +18,7 @@ package com.android.systemui.qs.pipeline.domain.interactor import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.phone.CentralSurfaces -import java.util.Optional +import com.android.systemui.shade.ShadeController import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,7 +30,7 @@ import org.mockito.MockitoAnnotations @SmallTest class PanelInteractorImplTest : SysuiTestCase() { - @Mock private lateinit var centralSurfaces: CentralSurfaces + @Mock private lateinit var shadeController: ShadeController @Before fun setup() { @@ -40,37 +39,28 @@ class PanelInteractorImplTest : SysuiTestCase() { @Test fun openPanels_callsCentralSurfaces() { - val underTest = PanelInteractorImpl(Optional.of(centralSurfaces)) + val underTest = PanelInteractorImpl(shadeController) underTest.openPanels() - verify(centralSurfaces).postAnimateOpenPanels() + verify(shadeController).postAnimateExpandQs() } @Test fun collapsePanels_callsCentralSurfaces() { - val underTest = PanelInteractorImpl(Optional.of(centralSurfaces)) + val underTest = PanelInteractorImpl(shadeController) underTest.collapsePanels() - verify(centralSurfaces).postAnimateCollapsePanels() + verify(shadeController).postAnimateCollapseShade() } @Test fun forceCollapsePanels_callsCentralSurfaces() { - val underTest = PanelInteractorImpl(Optional.of(centralSurfaces)) + val underTest = PanelInteractorImpl(shadeController) underTest.forceCollapsePanels() - verify(centralSurfaces).postAnimateForceCollapsePanels() - } - - @Test - fun whenOptionalEmpty_doesnThrow() { - val underTest = PanelInteractorImpl(Optional.empty()) - - underTest.openPanels() - underTest.collapsePanels() - underTest.forceCollapsePanels() + verify(shadeController).postAnimateForceCollapseShade() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index f2812b5857b7..59b595393749 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -226,6 +226,17 @@ public class RecordingServiceTest extends SysuiTestCase { } @Test + public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification() + throws IOException { + doReturn(true).when(mController).isRecording(); + doThrow(new RuntimeException()).when(mScreenMediaRecorder).end(); + + mRecordingService.onStopped(); + + verify(mRecordingService).createErrorNotification(); + } + + @Test public void testOnSystemRequestedStop_recorderEndThrowsOOMError_releasesRecording() throws IOException { doReturn(true).when(mController).isRecording(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt new file mode 100644 index 000000000000..fbb77cdc3049 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt @@ -0,0 +1,283 @@ +/* + * 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.app.Notification +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.UserHandle +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.screenshot.ScreenshotController.SaveImageInBackgroundData +import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +@SmallTest +class SaveImageInBackgroundTaskTest : SysuiTestCase() { + private val imageExporter = mock<ImageExporter>() + private val smartActions = mock<ScreenshotSmartActions>() + private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>() + private val saveImageData = SaveImageInBackgroundData() + private val sharedTransitionSupplier = + mock<Supplier<ScreenshotController.SavedImageData.ActionTransition>>() + private val testScreenshotId: String = "testScreenshotId" + private val testBitmap = mock<Bitmap>() + private val testUser = UserHandle.getUserHandleForUid(0) + private val testIcon = mock<Icon>() + private val testImageTime = 1234.toLong() + private val flags = FakeFeatureFlags() + + private val smartActionsUriFuture = mock<CompletableFuture<List<Notification.Action>>>() + private val smartActionsFuture = mock<CompletableFuture<List<Notification.Action>>>() + + private val testUri: Uri = Uri.parse("testUri") + private val intent = + Intent(Intent.ACTION_SEND) + .setComponent( + ComponentName.unflattenFromString( + "com.google.android.test/com.google.android.test.TestActivity" + ) + ) + private val immutablePendingIntent = + PendingIntent.getBroadcast( + mContext, + 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + private val mutablePendingIntent = + PendingIntent.getBroadcast( + mContext, + 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + ) + + private val saveImageTask = + SaveImageInBackgroundTask( + mContext, + flags, + imageExporter, + smartActions, + saveImageData, + sharedTransitionSupplier, + smartActionsProvider, + ) + + @Before + fun setup() { + whenever( + smartActions.getSmartActionsFuture( + eq(testScreenshotId), + any(Uri::class.java), + eq(testBitmap), + eq(smartActionsProvider), + any(ScreenshotSmartActionType::class.java), + any(Boolean::class.java), + eq(testUser) + ) + ) + .thenReturn(smartActionsUriFuture) + whenever( + smartActions.getSmartActionsFuture( + eq(testScreenshotId), + eq(null), + eq(testBitmap), + eq(smartActionsProvider), + any(ScreenshotSmartActionType::class.java), + any(Boolean::class.java), + eq(testUser) + ) + ) + .thenReturn(smartActionsFuture) + } + + @Test + fun testQueryQuickShare_noAction() { + whenever( + smartActions.getSmartActions( + eq(testScreenshotId), + eq(smartActionsFuture), + any(Int::class.java), + eq(smartActionsProvider), + eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) + ) + ) + .thenReturn(ArrayList<Notification.Action>()) + + val quickShareAction = + saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri) + + assertNull(quickShareAction) + } + + @Test + fun testQueryQuickShare_withActions() { + val actions = ArrayList<Notification.Action>() + actions.add(constructAction("Action One", mutablePendingIntent)) + actions.add(constructAction("Action Two", mutablePendingIntent)) + whenever( + smartActions.getSmartActions( + eq(testScreenshotId), + eq(smartActionsUriFuture), + any(Int::class.java), + eq(smartActionsProvider), + eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) + ) + ) + .thenReturn(actions) + + val quickShareAction = + saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)!! + + assertEquals("Action One", quickShareAction.title) + assertEquals(mutablePendingIntent, quickShareAction.actionIntent) + } + + @Test + fun testCreateQuickShareAction_originalWasNull_returnsNull() { + val quickShareAction = + saveImageTask.createQuickShareAction( + null, + testScreenshotId, + testUri, + testImageTime, + testBitmap, + testUser + ) + + assertNull(quickShareAction) + } + + @Test + fun testCreateQuickShareAction_immutableIntentDifferentAction_returnsNull() { + val actions = ArrayList<Notification.Action>() + actions.add(constructAction("New Test Action", immutablePendingIntent)) + whenever( + smartActions.getSmartActions( + eq(testScreenshotId), + eq(smartActionsUriFuture), + any(Int::class.java), + eq(smartActionsProvider), + eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) + ) + ) + .thenReturn(actions) + val origAction = constructAction("Old Test Action", immutablePendingIntent) + + val quickShareAction = + saveImageTask.createQuickShareAction( + origAction, + testScreenshotId, + testUri, + testImageTime, + testBitmap, + testUser, + ) + + assertNull(quickShareAction) + } + + @Test + fun testCreateQuickShareAction_mutableIntent_returnsSafeIntent() { + val actions = ArrayList<Notification.Action>() + val action = constructAction("Action One", mutablePendingIntent) + actions.add(action) + whenever( + smartActions.getSmartActions( + eq(testScreenshotId), + eq(smartActionsUriFuture), + any(Int::class.java), + eq(smartActionsProvider), + eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) + ) + ) + .thenReturn(actions) + + val quickShareAction = + saveImageTask.createQuickShareAction( + constructAction("Test Action", mutablePendingIntent), + testScreenshotId, + testUri, + testImageTime, + testBitmap, + testUser + ) + val quickSharePendingIntent = + quickShareAction.actionIntent.intent.extras!!.getParcelable( + ScreenshotController.EXTRA_ACTION_INTENT, + PendingIntent::class.java + ) + + assertEquals("Test Action", quickShareAction.title) + assertEquals(mutablePendingIntent, quickSharePendingIntent) + } + + @Test + fun testCreateQuickShareAction_immutableIntent_returnsSafeIntent() { + val actions = ArrayList<Notification.Action>() + val action = constructAction("Test Action", immutablePendingIntent) + actions.add(action) + whenever( + smartActions.getSmartActions( + eq(testScreenshotId), + eq(smartActionsUriFuture), + any(Int::class.java), + eq(smartActionsProvider), + eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) + ) + ) + .thenReturn(actions) + + val quickShareAction = + saveImageTask.createQuickShareAction( + constructAction("Test Action", immutablePendingIntent), + testScreenshotId, + testUri, + testImageTime, + testBitmap, + testUser, + )!! + + assertEquals("Test Action", quickShareAction.title) + assertEquals( + immutablePendingIntent, + quickShareAction.actionIntent.intent.extras!!.getParcelable( + ScreenshotController.EXTRA_ACTION_INTENT, + PendingIntent::class.java + ) + ) + } + + private fun constructAction(title: String, intent: PendingIntent): Notification.Action { + return Notification.Action.Builder(testIcon, title, intent).build() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index b1f8475f7d74..470c824eb60f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -1100,6 +1100,13 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + public void onShadeFlingEnd_mExpandImmediateShouldBeReset() { + mNotificationPanelViewController.onFlingEnd(false); + + verify(mQsController).setExpandImmediate(false); + } + + @Test public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() { mStatusBarStateController.setState(SHADE); enableSplitShade(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt new file mode 100644 index 000000000000..ef66756790d8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt @@ -0,0 +1,107 @@ +/* + * 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.shade + +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.WindowManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.assist.AssistManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.row.NotificationGutsManager +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import dagger.Lazy +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ShadeControllerImplTest : SysuiTestCase() { + @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var statusBarWindowController: StatusBarWindowController + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var assistManager: AssistManager + @Mock private lateinit var gutsManager: NotificationGutsManager + @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController + @Mock private lateinit var display: Display + + private lateinit var shadeController: ShadeControllerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(windowManager.defaultDisplay).thenReturn(display) + whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + shadeController = + ShadeControllerImpl( + commandQueue, + FakeExecutor(FakeSystemClock()), + keyguardStateController, + statusBarStateController, + statusBarKeyguardViewManager, + statusBarWindowController, + deviceProvisionedController, + notificationShadeWindowController, + windowManager, + Lazy { assistManager }, + Lazy { gutsManager }, + ) + shadeController.setNotificationPanelViewController(notificationPanelViewController) + } + + @Test + fun testDisableNotificationShade() { + whenever(commandQueue.panelsEnabled()).thenReturn(false) + + // Trying to open it does nothing. + shadeController.animateExpandShade() + verify(notificationPanelViewController, never()).expandToNotifications() + shadeController.animateExpandQs() + verify(notificationPanelViewController, never()).expand(ArgumentMatchers.anyBoolean()) + } + + @Test + fun testEnableNotificationShade() { + whenever(commandQueue.panelsEnabled()).thenReturn(true) + + // Can now be opened. + shadeController.animateExpandShade() + verify(notificationPanelViewController).expandToNotifications() + shadeController.animateExpandQs() + verify(notificationPanelViewController).expandToQs() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java index 109f185c625e..22c9e45d48af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java @@ -76,5 +76,6 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); + verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java index ea822aa00429..a3ecde0fe976 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java @@ -75,5 +75,6 @@ public class KeyboardShortcutsTest extends SysuiTestCase { mKeyboardShortcuts.toggle(mContext, DEVICE_ID); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); + verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 39ed5535ff3b..914301f2e830 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -60,9 +60,13 @@ import org.mockito.MockitoAnnotations class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { @Mock private lateinit var systemEventCoordinator: SystemEventCoordinator + @Mock private lateinit var statusBarWindowController: StatusBarWindowController + @Mock private lateinit var statusBarContentInsetProvider: StatusBarContentInsetsProvider + @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var listener: SystemStatusAnimationCallback private lateinit var systemClock: FakeSystemClock @@ -380,6 +384,32 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { } @Test + fun testPrivacyDot_isRemovedDuringChipDisappearAnimation() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + createAndScheduleFakePrivacyEvent() + + // fast forward to ANIMATING_OUT state + fastForwardAnimationToState(ANIMATING_OUT) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + + // remove persistent dot + systemStatusAnimationScheduler.removePersistentDot() + testScheduler.runCurrent() + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + testScheduler.runCurrent() + + // verify that animationState changes to IDLE and onHidePersistentDot callback is invoked + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onHidePersistentDot() + } + + @Test fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringQueuedState() = runTest { // Instantiate class under test with TestScope from runTest initializeSystemStatusAnimationScheduler(testScope = this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt index 2fbe87158eba..ea70e9e44c66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -32,7 +32,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -46,11 +45,14 @@ import com.android.systemui.statusbar.notification.interruption.KeyguardNotifica import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScheduler @@ -62,9 +64,8 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.same import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never import org.mockito.Mockito.verify -import java.util.function.Consumer -import kotlin.time.Duration.Companion.seconds import org.mockito.Mockito.`when` as whenever @SmallTest @@ -75,7 +76,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val keyguardTransitionRepository = FakeKeyguardTransitionRepository() - private val notifPipelineFlags: NotifPipelineFlags = mock() private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() private val statusBarStateController: StatusBarStateController = mock() @@ -136,13 +136,8 @@ class KeyguardCoordinatorTest : SysuiTestCase() { ) testScheduler.runCurrent() - // WHEN: The shade is expanded - whenever(statusBarStateController.isExpanded).thenReturn(true) - statusBarStateListener.onExpandedChanged(true) - testScheduler.runCurrent() - - // THEN: The notification is still treated as "unseen" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + // THEN: We are no longer listening for shade expansions + verify(statusBarStateController, never()).addCallback(any()) } } @@ -152,6 +147,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) + // WHEN: A notification is posted val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) @@ -162,6 +161,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: The keyguard is now showing keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD) + ) testScheduler.runCurrent() // THEN: The notification is recognized as "seen" and is filtered out. @@ -169,6 +171,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: The keyguard goes away keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE) + ) testScheduler.runCurrent() // THEN: The notification is shown regardless @@ -182,9 +187,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(true) runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder() + val fakeEntry = + NotificationEntryBuilder() .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) - .build() + .build() collectionListener.onEntryAdded(fakeEntry) // WHEN: The keyguard is now showing @@ -202,11 +208,13 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(true) runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build().apply { - row = mock<ExpandableNotificationRow>().apply { - whenever(isMediaRow).thenReturn(true) + val fakeEntry = + NotificationEntryBuilder().build().apply { + row = + mock<ExpandableNotificationRow>().apply { + whenever(isMediaRow).thenReturn(true) + } } - } collectionListener.onEntryAdded(fakeEntry) // WHEN: The keyguard is now showing @@ -299,14 +307,12 @@ class KeyguardCoordinatorTest : SysuiTestCase() { runKeyguardCoordinatorTest { // WHEN: A new notification is posted val fakeSummary = NotificationEntryBuilder().build() - val fakeChild = NotificationEntryBuilder() + val fakeChild = + NotificationEntryBuilder() .setGroup(context, "group") .setGroupSummary(context, false) .build() - GroupEntryBuilder() - .setSummary(fakeSummary) - .addChild(fakeChild) - .build() + GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() collectionListener.onEntryAdded(fakeSummary) collectionListener.onEntryAdded(fakeChild) @@ -331,6 +337,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() { runKeyguardCoordinatorTest { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() // WHEN: five seconds have passed testScheduler.advanceTimeBy(5.seconds) @@ -338,10 +348,16 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) testScheduler.runCurrent() // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD) + ) testScheduler.runCurrent() // THEN: The notification is now recognized as "seen" and is filtered out. @@ -354,11 +370,17 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // GIVEN: Keyguard is showing, unseen notification is present keyguardRepository.setKeyguardShowing(true) runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) @@ -369,14 +391,212 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } + @Test + fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { + // GIVEN: Keyguard is showing, not dozing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + val firstEntry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(firstEntry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: another unseen notification is posted + val secondEntry = NotificationEntryBuilder().setId(2).build() + collectionListener.onEntryAdded(secondEntry) + testScheduler.runCurrent() + + // WHEN: four more seconds have passed + testScheduler.advanceTimeBy(4.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // THEN: The first notification is considered seen and is filtered out. + assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue() + + // THEN: The second notification is still considered unseen and is not filtered out + assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: five more seconds have passed + testScheduler.advanceTimeBy(5.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is removed + collectionListener.onEntryRemoved(entry, 0) + testScheduler.runCurrent() + + // WHEN: the notification is re-posted + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one more second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is removed + collectionListener.onEntryRemoved(entry, 0) + testScheduler.runCurrent() + + // WHEN: the notification is re-posted + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one more second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is updated + collectionListener.onEntryUpdated(entry) + testScheduler.runCurrent() + + // WHEN: four more seconds have passed + testScheduler.advanceTimeBy(4.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + private fun runKeyguardCoordinatorTest( testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit ) { val testDispatcher = UnconfinedTestDispatcher() val testScope = TestScope(testDispatcher) - val fakeSettings = FakeSettings().apply { - putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) - } + val fakeSettings = + FakeSettings().apply { + putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + } val seenNotificationsProvider = SeenNotificationsProviderImpl() val keyguardCoordinator = KeyguardCoordinator( @@ -387,7 +607,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository, keyguardTransitionRepository, mock<KeyguardCoordinatorLogger>(), - notifPipelineFlags, testScope.backgroundScope, sectionHeaderVisibilityProvider, fakeSettings, @@ -397,11 +616,12 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardCoordinator.attach(notifPipeline) testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { KeyguardCoordinatorTestScope( - keyguardCoordinator, - testScope, - seenNotificationsProvider, - fakeSettings, - ).testBlock() + keyguardCoordinator, + testScope, + seenNotificationsProvider, + fakeSettings, + ) + .testBlock() } } @@ -414,10 +634,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val testScheduler: TestCoroutineScheduler get() = scope.testScheduler - val onStateChangeListener: Consumer<String> = - withArgCaptor { - verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) - } + val onStateChangeListener: Consumer<String> = withArgCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } val unseenFilter: NotifFilter get() = keyguardCoordinator.unseenNotifFilter @@ -426,11 +645,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addCollectionListener(capture()) } - val onHeadsUpChangedListener: OnHeadsUpChangedListener get() = - withArgCaptor { verify(headsUpManager).addListener(capture()) } + val onHeadsUpChangedListener: OnHeadsUpChangedListener + get() = withArgCaptor { verify(headsUpManager).addListener(capture()) } - val statusBarStateListener: StatusBarStateController.StateListener get() = - withArgCaptor { verify(statusBarStateController).addCallback(capture()) } + val statusBarStateListener: StatusBarStateController.StateListener + get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) } var showOnlyUnseenNotifsOnKeyguardSetting: Boolean get() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index 915924f13197..bdc8135707bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -108,7 +108,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { mRow.doDragCallback(0, 0); verify(controller).startDragAndDrop(mRow); - verify(mShadeController).animateCollapsePanels(eq(0), eq(true), + verify(mShadeController).animateCollapseShade(eq(0), eq(true), eq(false), anyFloat()); verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 9938fa8700fd..0e966dc655f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.content.res.Resources; import android.metrics.LogMaker; import android.testing.AndroidTestingRunner; +import android.view.View; import androidx.test.filters.SmallTest; @@ -71,6 +72,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.collection.render.NotifStats; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; +import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; @@ -106,6 +108,7 @@ import java.util.Optional; public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationGutsManager mNotificationGutsManager; + @Mock private NotificationsController mNotificationsController; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private HeadsUpManagerPhone mHeadsUpManager; @Mock private NotificationRoundnessManager mNotificationRoundnessManager; @@ -432,6 +435,84 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mNotificationStackScrollLayout).setStatusBarState(KEYGUARD); } + @Test + public void updateImportantForAccessibility_noChild_onKeyGuard_notImportantForA11y() { + // GIVEN: Controller is attached, active notifications is empty, + // and mNotificationStackScrollLayout.onKeyguard() is true + initController(/* viewIsAttached= */ true); + when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); + mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); + + // WHEN: call updateImportantForAccessibility + mController.updateImportantForAccessibility(); + + // THEN: mNotificationStackScrollLayout should not be important for A11y + verify(mNotificationStackScrollLayout) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } + + @Test + public void updateImportantForAccessibility_hasChild_onKeyGuard_importantForA11y() { + // GIVEN: Controller is attached, active notifications is not empty, + // and mNotificationStackScrollLayout.onKeyguard() is true + initController(/* viewIsAttached= */ true); + when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); + mController.getNotifStackController().setNotifStats( + new NotifStats( + /* numActiveNotifs = */ 1, + /* hasNonClearableAlertingNotifs = */ false, + /* hasClearableAlertingNotifs = */ false, + /* hasNonClearableSilentNotifs = */ false, + /* hasClearableSilentNotifs = */ false) + ); + + // WHEN: call updateImportantForAccessibility + mController.updateImportantForAccessibility(); + + // THEN: mNotificationStackScrollLayout should be important for A11y + verify(mNotificationStackScrollLayout) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + @Test + public void updateImportantForAccessibility_hasChild_notOnKeyGuard_importantForA11y() { + // GIVEN: Controller is attached, active notifications is not empty, + // and mNotificationStackScrollLayout.onKeyguard() is false + initController(/* viewIsAttached= */ true); + when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); + mController.getNotifStackController().setNotifStats( + new NotifStats( + /* numActiveNotifs = */ 1, + /* hasNonClearableAlertingNotifs = */ false, + /* hasClearableAlertingNotifs = */ false, + /* hasNonClearableSilentNotifs = */ false, + /* hasClearableSilentNotifs = */ false) + ); + + // WHEN: call updateImportantForAccessibility + mController.updateImportantForAccessibility(); + + // THEN: mNotificationStackScrollLayout should be important for A11y + verify(mNotificationStackScrollLayout) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + @Test + public void updateImportantForAccessibility_noChild_notOnKeyGuard_importantForA11y() { + // GIVEN: Controller is attached, active notifications is empty, + // and mNotificationStackScrollLayout.onKeyguard() is false + initController(/* viewIsAttached= */ true); + when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); + mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); + + // WHEN: call updateImportantForAccessibility + mController.updateImportantForAccessibility(); + + // THEN: mNotificationStackScrollLayout should be important for A11y + verify(mNotificationStackScrollLayout) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } @@ -455,6 +536,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mNotificationStackScrollLayout, true, mNotificationGutsManager, + mNotificationsController, mVisibilityProvider, mHeadsUpManager, mNotificationRoundnessManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a4ee349f5b71..ee02a7b6e090 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -79,9 +80,9 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; +import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.FooterView; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; @@ -114,7 +115,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { private TestableResources mTestableResources; @Rule public MockitoRule mockito = MockitoJUnit.rule(); - @Mock private CentralSurfaces mCentralSurfaces; + @Mock private NotificationsController mNotificationsController; @Mock private SysuiStatusBarStateController mBarState; @Mock private GroupMembershipManager mGroupMembershipManger; @Mock private GroupExpansionManager mGroupExpansionManager; @@ -181,7 +182,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mNotificationStackSizeCalculator); mStackScroller = spy(mStackScrollerInternal); mStackScroller.setShelfController(notificationShelfController); - mStackScroller.setCentralSurfaces(mCentralSurfaces); + mStackScroller.setNotificationsController(mNotificationsController); mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true); when(mStackScrollLayoutController.getNotificationRoundnessManager()) @@ -575,10 +576,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.inflateFooterView(); // add notification - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - NotificationEntry entry = mock(NotificationEntry.class); - when(row.getEntry()).thenReturn(entry); - when(entry.isClearable()).thenReturn(true); + ExpandableNotificationRow row = createClearableRow(); mStackScroller.addContainerView(row); mStackScroller.onUpdateRowStates(); @@ -648,6 +646,50 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + public void testClearNotifications_clearAllInProgress() { + ExpandableNotificationRow row = createClearableRow(); + when(row.getEntry().hasFinishedInitialization()).thenReturn(true); + doReturn(true).when(mStackScroller).isVisible(row); + mStackScroller.addContainerView(row); + + mStackScroller.clearNotifications(ROWS_ALL, false); + + assertClearAllInProgress(true); + verify(mNotificationRoundnessManager).setClearAllInProgress(true); + } + + @Test + public void testOnChildAnimationFinished_resetsClearAllInProgress() { + mStackScroller.setClearAllInProgress(true); + + mStackScroller.onChildAnimationFinished(); + + assertClearAllInProgress(false); + verify(mNotificationRoundnessManager).setClearAllInProgress(false); + } + + @Test + public void testShadeCollapsed_resetsClearAllInProgress() { + mStackScroller.setClearAllInProgress(true); + + mStackScroller.setIsExpanded(false); + + assertClearAllInProgress(false); + verify(mNotificationRoundnessManager).setClearAllInProgress(false); + } + + @Test + public void testShadeExpanded_doesntChangeClearAllInProgress() { + mStackScroller.setClearAllInProgress(true); + clearInvocations(mNotificationRoundnessManager); + + mStackScroller.setIsExpanded(true); + + assertClearAllInProgress(true); + verify(mNotificationRoundnessManager, never()).setClearAllInProgress(anyBoolean()); + } + + @Test public void testAddNotificationUpdatesSpeedBumpIndex() { // initial state calculated == 0 assertEquals(0, mStackScroller.getSpeedBumpIndex()); @@ -794,7 +836,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - public void onShadeClosesWithAnimationWillResetSwipeState() { + public void onShadeClosesWithAnimationWillResetTouchState() { // GIVEN shade is expanded mStackScroller.setIsExpanded(true); clearInvocations(mNotificationSwipeHelper); @@ -804,12 +846,12 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setIsExpanded(false); mStackScroller.onExpansionStopped(); - // VERIFY swipe is reset - verify(mNotificationSwipeHelper).resetSwipeState(); + // VERIFY touch is reset + verify(mNotificationSwipeHelper).resetTouchState(); } @Test - public void onShadeClosesWithoutAnimationWillResetSwipeState() { + public void onShadeClosesWithoutAnimationWillResetTouchState() { // GIVEN shade is expanded mStackScroller.setIsExpanded(true); clearInvocations(mNotificationSwipeHelper); @@ -817,8 +859,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { // WHEN closing the shade without the animation mStackScroller.setIsExpanded(false); - // VERIFY swipe is reset - verify(mNotificationSwipeHelper).resetSwipeState(); + // VERIFY touch is reset + verify(mNotificationSwipeHelper).resetTouchState(); } @Test @@ -896,6 +938,21 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setStatusBarState(state); } + private ExpandableNotificationRow createClearableRow() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + NotificationEntry entry = mock(NotificationEntry.class); + when(row.canViewBeCleared()).thenReturn(true); + when(row.getEntry()).thenReturn(entry); + when(entry.isClearable()).thenReturn(true); + + return row; + } + + private void assertClearAllInProgress(boolean expected) { + assertEquals(expected, mStackScroller.getClearAllInProgress()); + assertEquals(expected, mAmbientState.isClearAllInProgress()); + } + private static void mockBoundsOnScreen(View view, Rect bounds) { doAnswer(invocation -> { Rect out = invocation.getArgument(0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index 3870d996d2ae..cb71fb8f703a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -153,12 +152,6 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { verify(mCentralSurfaces).updateQsExpansionEnabled(); verify(mShadeController).animateCollapseShade(); - - // Trying to open it does nothing. - mSbcqCallbacks.animateExpandNotificationsPanel(); - verify(mShadeViewController, never()).expandToNotifications(); - mSbcqCallbacks.animateExpandSettingsPanel(null); - verify(mShadeViewController, never()).expand(anyBoolean()); } @Test @@ -171,12 +164,6 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { StatusBarManager.DISABLE2_NONE, false); verify(mCentralSurfaces).updateQsExpansionEnabled(); verify(mShadeController, never()).animateCollapseShade(); - - // Can now be opened. - mSbcqCallbacks.animateExpandNotificationsPanel(); - verify(mShadeViewController).expandToNotifications(); - mSbcqCallbacks.animateExpandSettingsPanel(null); - verify(mShadeViewController).expandToQs(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 4ed113fe1358..4fb5b0735fcc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -431,10 +431,12 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mShadeController = spy(new ShadeControllerImpl( mCommandQueue, + mMainExecutor, mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager, mStatusBarWindowController, + mDeviceProvisionedController, mNotificationShadeWindowController, mContext.getSystemService(WindowManager.class), () -> mAssistManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt index 3dec45b4ff9f..b9c7e6133669 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.unfold +import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator import android.testing.AndroidTestingRunner @@ -53,7 +54,7 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { progressProvider.onTransitionProgress(0.5f) progressProvider.onTransitionFinishing() - verify(vibrator).vibrate(any<VibrationEffect>()) + verify(vibrator).vibrate(any<VibrationEffect>(), any<VibrationAttributes>()) } @Test @@ -64,7 +65,7 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { progressProvider.onTransitionProgress(0.99f) progressProvider.onTransitionFinishing() - verify(vibrator, never()).vibrate(any<VibrationEffect>()) + verify(vibrator, never()).vibrate(any<VibrationEffect>(), any<VibrationAttributes>()) } @Test @@ -84,7 +85,7 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { progressProvider.onTransitionFinished() testFoldProvider.onFoldUpdate(isFolded = true) - verify(vibrator, never()).vibrate(any<VibrationEffect>()) + verify(vibrator, never()).vibrate(any<VibrationEffect>(), any<VibrationAttributes>()) } @Test @@ -112,6 +113,6 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { progressProvider.onTransitionFinishing() progressProvider.onTransitionFinished() - verify(vibrator).vibrate(any<VibrationEffect>()) + verify(vibrator).vibrate(any<VibrationEffect>(), any<VibrationAttributes>()) } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 6a0c69015df9..21986b73c808 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -958,7 +958,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final ComponentName menuToMigrate = AccessibilityUtils.getAccessibilityMenuComponentToMigrate(mPackageManager, userId); if (menuToMigrate != null) { - mPackageManager.setComponentEnabledSetting( + // PackageManager#setComponentEnabledSetting disables the component for only the user + // linked to PackageManager's context, but mPackageManager is linked to the system user, + // so grab a new PackageManager for the current user to support secondary users. + final PackageManager userPackageManager = + mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0) + .getPackageManager(); + userPackageManager.setComponentEnabledSetting( menuToMigrate, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); @@ -1845,6 +1851,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // find out a way to detect the device finished the OTA and switch the user. migrateAccessibilityButtonSettingsIfNecessaryLocked(userState, null, /* restoreFromSdkInt = */0); + // Package components are disabled per user, so secondary users also need their migrated + // Accessibility Menu component disabled. + disableAccessibilityMenuToMigrateIfNeeded(); if (announceNewUser) { // Schedule announcement of the current user if needed. diff --git a/services/autofill/java/com/android/server/autofill/LogFieldClassificationScoreOnResultListener.java b/services/autofill/java/com/android/server/autofill/LogFieldClassificationScoreOnResultListener.java new file mode 100644 index 000000000000..b4aca1530204 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/LogFieldClassificationScoreOnResultListener.java @@ -0,0 +1,79 @@ +/* + * 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.server.autofill; + +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.service.autofill.FieldClassification; +import android.service.autofill.FillEventHistory.Event.NoSaveReason; +import android.util.Slog; +import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager.AutofillCommitReason; + +import java.util.ArrayList; + +class LogFieldClassificationScoreOnResultListener implements + RemoteCallback.OnResultListener { + + private static final String TAG = "LogFieldClassificationScoreOnResultListener"; + + private Session mSession; + private final @NoSaveReason int mSaveDialogNotShowReason; + private final @AutofillCommitReason int mCommitReason; + private final int mViewsSize; + private final AutofillId[] mAutofillIds; + private final String[] mUserValues; + private final String[] mCategoryIds; + private final ArrayList<AutofillId> mDetectedFieldIds; + private final ArrayList<FieldClassification> mDetectedFieldClassifications; + LogFieldClassificationScoreOnResultListener(Session session, + int saveDialogNotShowReason, + int commitReason, int viewsSize, AutofillId[] autofillIds, String[] userValues, + String[] categoryIds, ArrayList<AutofillId> detectedFieldIds, + ArrayList<FieldClassification> detectedFieldClassifications) { + this.mSession = session; + this.mSaveDialogNotShowReason = saveDialogNotShowReason; + this.mCommitReason = commitReason; + this.mViewsSize = viewsSize; + this.mAutofillIds = autofillIds; + this.mUserValues = userValues; + this.mCategoryIds = categoryIds; + this.mDetectedFieldIds = detectedFieldIds; + this.mDetectedFieldClassifications = detectedFieldClassifications; + } + + public void onResult(@Nullable Bundle result) { + // Create a local copy to safe guard race condition + Session session = mSession; + if (session == null) { + Slog.wtf(TAG, "session is null when calling onResult()"); + return; + } + session.handleLogFieldClassificationScore( + result, + mSaveDialogNotShowReason, + mCommitReason, + mViewsSize, + mAutofillIds, + mUserValues, + mCategoryIds, + mDetectedFieldIds, + mDetectedFieldClassifications); + mSession = null; + } +} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 1bfdb435bbb1..a5c4ac750458 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -184,6 +184,7 @@ import com.android.server.wm.ActivityTaskManagerInternal; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -3129,76 +3130,91 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // Then use the results, asynchronously - final RemoteCallback callback = new RemoteCallback((result) -> { - if (result == null) { - if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results"); - logContextCommitted(null, null, saveDialogNotShowReason, commitReason); - return; - } - final Scores scores = result.getParcelable(EXTRA_SCORES, android.service.autofill.AutofillFieldClassificationService.Scores.class); - if (scores == null) { - Slog.w(TAG, "No field classification score on " + result); - return; - } - int i = 0, j = 0; - try { - // Iteract over all autofill fields first - for (i = 0; i < viewsSize; i++) { - final AutofillId autofillId = autofillIds[i]; - - // Search the best scores for each category (as some categories could have - // multiple user values - ArrayMap<String, Float> scoresByField = null; - for (j = 0; j < userValues.length; j++) { - final String categoryId = categoryIds[j]; - final float score = scores.scores[i][j]; - if (score > 0) { - if (scoresByField == null) { - scoresByField = new ArrayMap<>(userValues.length); - } - final Float currentScore = scoresByField.get(categoryId); - if (currentScore != null && currentScore > score) { - if (sVerbose) { - Slog.v(TAG, "skipping score " + score - + " because it's less than " + currentScore); - } - continue; - } + final RemoteCallback callback = new RemoteCallback( + new LogFieldClassificationScoreOnResultListener( + this, + saveDialogNotShowReason, + commitReason, + viewsSize, + autofillIds, + userValues, + categoryIds, + detectedFieldIds, + detectedFieldClassifications)); + + fcStrategy.calculateScores(callback, currentValues, userValues, categoryIds, + defaultAlgorithm, defaultArgs, algorithms, args); + } + + void handleLogFieldClassificationScore(@Nullable Bundle result, int saveDialogNotShowReason, + int commitReason, int viewsSize, AutofillId[] autofillIds, String[] userValues, + String[] categoryIds, ArrayList<AutofillId> detectedFieldIds, + ArrayList<FieldClassification> detectedFieldClassifications) { + if (result == null) { + if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results"); + logContextCommitted(null, null, saveDialogNotShowReason, commitReason); + return; + } + final Scores scores = result.getParcelable(EXTRA_SCORES, + android.service.autofill.AutofillFieldClassificationService.Scores.class); + if (scores == null) { + Slog.w(TAG, "No field classification score on " + result); + return; + } + int i = 0, j = 0; + try { + // Iteract over all autofill fields first + for (i = 0; i < viewsSize; i++) { + final AutofillId autofillId = autofillIds[i]; + + // Search the best scores for each category (as some categories could have + // multiple user values + ArrayMap<String, Float> scoresByField = null; + for (j = 0; j < userValues.length; j++) { + final String categoryId = categoryIds[j]; + final float score = scores.scores[i][j]; + if (score > 0) { + if (scoresByField == null) { + scoresByField = new ArrayMap<>(userValues.length); + } + final Float currentScore = scoresByField.get(categoryId); + if (currentScore != null && currentScore > score) { if (sVerbose) { - Slog.v(TAG, "adding score " + score + " at index " + j + " and id " - + autofillId); + Slog.v(TAG, "skipping score " + score + + " because it's less than " + currentScore); } - scoresByField.put(categoryId, score); - } else if (sVerbose) { - Slog.v(TAG, "skipping score 0 at index " + j + " and id " + autofillId); + continue; } + if (sVerbose) { + Slog.v(TAG, "adding score " + score + " at index " + j + " and id " + + autofillId); + } + scoresByField.put(categoryId, score); + } else if (sVerbose) { + Slog.v(TAG, "skipping score 0 at index " + j + " and id " + autofillId); } - if (scoresByField == null) { - if (sVerbose) Slog.v(TAG, "no score for autofillId=" + autofillId); - continue; - } - - // Then create the matches for that autofill id - final ArrayList<Match> matches = new ArrayList<>(scoresByField.size()); - for (j = 0; j < scoresByField.size(); j++) { - final String fieldId = scoresByField.keyAt(j); - final float score = scoresByField.valueAt(j); - matches.add(new Match(fieldId, score)); - } - detectedFieldIds.add(autofillId); - detectedFieldClassifications.add(new FieldClassification(matches)); - } // for i - } catch (ArrayIndexOutOfBoundsException e) { - wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e); - return; - } - - logContextCommitted(detectedFieldIds, detectedFieldClassifications, - saveDialogNotShowReason, commitReason); - }); + } + if (scoresByField == null) { + if (sVerbose) Slog.v(TAG, "no score for autofillId=" + autofillId); + continue; + } - fcStrategy.calculateScores(callback, currentValues, userValues, categoryIds, - defaultAlgorithm, defaultArgs, algorithms, args); + // Then create the matches for that autofill id + final ArrayList<Match> matches = new ArrayList<>(scoresByField.size()); + for (j = 0; j < scoresByField.size(); j++) { + final String fieldId = scoresByField.keyAt(j); + final float score = scoresByField.valueAt(j); + matches.add(new Match(fieldId, score)); + } + detectedFieldIds.add(autofillId); + detectedFieldClassifications.add(new FieldClassification(matches)); + } // for i + } catch (ArrayIndexOutOfBoundsException e) { + wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e); + return; + } + logContextCommitted(detectedFieldIds, detectedFieldClassifications, + saveDialogNotShowReason, commitReason); } /** @@ -4925,16 +4941,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return null; } - final boolean isWhitelisted = mService + final boolean isAllowlisted = mService .isWhitelistedForAugmentedAutofillLocked(mComponentName); - if (!isWhitelisted) { + if (!isAllowlisted) { if (sVerbose) { Slog.v(TAG, "triggerAugmentedAutofillLocked(): " + ComponentName.flattenToShortString(mComponentName) + " not whitelisted "); } logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), - mCurrentViewId, isWhitelisted, /* isInline= */ null); + mCurrentViewId, isAllowlisted, /* isInline= */ null); return null; } @@ -4967,32 +4983,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final AutofillId focusedId = mCurrentViewId; - final Function<InlineFillUi, Boolean> inlineSuggestionsResponseCallback = - response -> { - synchronized (mLock) { - return mInlineSessionController.setInlineFillUiLocked(response); - } - }; final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill = - (inlineSuggestionsRequest) -> { - synchronized (mLock) { - logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), - focusedId, isWhitelisted, inlineSuggestionsRequest != null); - remoteService.onRequestAutofillLocked(id, mClient, - taskId, mComponentName, mActivityToken, - AutofillId.withoutSession(focusedId), currentValue, - inlineSuggestionsRequest, inlineSuggestionsResponseCallback, - /*onErrorCallback=*/ () -> { - synchronized (mLock) { - cancelAugmentedAutofillLocked(); - - // Also cancel augmented in IME - mInlineSessionController.setInlineFillUiLocked( - InlineFillUi.emptyUi(mCurrentViewId)); - } - }, mService.getRemoteInlineSuggestionRenderServiceLocked(), userId); - } - }; + new AugmentedAutofillInlineSuggestionRequestConsumer( + this, focusedId, isAllowlisted, mode, currentValue); // When the inline suggestion render service is available and the view is focused, there // are 3 cases when augmented autofill should ask IME for inline suggestion request, @@ -5010,14 +5003,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState || mSessionFlags.mExpiredResponse) && (isViewFocusedLocked(flags) || isRequestSupportFillDialog(flags))) { if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill"); - remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( - (extras) -> { - synchronized (mLock) { - mInlineSessionController.onCreateInlineSuggestionsRequestLocked( - focusedId, /*requestConsumer=*/ requestAugmentedAutofill, - extras); - } - }, mHandler)); + remoteRenderService.getInlineSuggestionsRendererInfo( + new RemoteCallback( + new AugmentedAutofillInlineSuggestionRendererOnResultListener( + this, focusedId, requestAugmentedAutofill), + mHandler)); } else { requestAugmentedAutofill.accept( mInlineSessionController.getInlineSuggestionsRequestLocked().orElse(null)); @@ -5028,6 +5018,169 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return mAugmentedAutofillDestroyer; } + private static class AugmentedAutofillInlineSuggestionRendererOnResultListener + implements RemoteCallback.OnResultListener { + + WeakReference<Session> mSessionWeakRef; + final AutofillId mFocusedId; + Consumer<InlineSuggestionsRequest> mRequestAugmentedAutofill; + + AugmentedAutofillInlineSuggestionRendererOnResultListener( + Session session, + AutofillId focussedId, + Consumer<InlineSuggestionsRequest> requestAugmentedAutofill) { + mSessionWeakRef = new WeakReference<>(session); + mFocusedId = focussedId; + mRequestAugmentedAutofill = requestAugmentedAutofill; + } + + @Override + public void onResult(@Nullable Bundle result) { + Session session = mSessionWeakRef.get(); + + if (logIfSessionNull( + session, "AugmentedAutofillInlineSuggestionRendererOnResultListener:")) { + return; + } + synchronized (session.mLock) { + session.mInlineSessionController.onCreateInlineSuggestionsRequestLocked( + mFocusedId, /*requestConsumer=*/ mRequestAugmentedAutofill, + result); + } + } + } + + private static class AugmentedAutofillInlineSuggestionRequestConsumer + implements Consumer<InlineSuggestionsRequest> { + + WeakReference<Session> mSessionWeakRef; + final AutofillId mFocusedId; + final boolean mIsAllowlisted; + final int mMode; + final AutofillValue mCurrentValue; + + AugmentedAutofillInlineSuggestionRequestConsumer( + Session session, + AutofillId focussedId, + boolean isAllowlisted, + int mode, + AutofillValue currentValue) { + mSessionWeakRef = new WeakReference<>(session); + mFocusedId = focussedId; + mIsAllowlisted = isAllowlisted; + mMode = mode; + mCurrentValue = currentValue; + + } + @Override + public void accept(InlineSuggestionsRequest inlineSuggestionsRequest) { + Session session = mSessionWeakRef.get(); + + if (logIfSessionNull( + session, "AugmentedAutofillInlineSuggestionRequestConsumer:")) { + return; + } + session.onAugmentedAutofillInlineSuggestionAccept( + inlineSuggestionsRequest, mFocusedId, mIsAllowlisted, mMode, mCurrentValue); + + } + } + + private static class AugmentedAutofillInlineSuggestionsResponseCallback + implements Function<InlineFillUi, Boolean> { + + WeakReference<Session> mSessionWeakRef; + + AugmentedAutofillInlineSuggestionsResponseCallback(Session session) { + this.mSessionWeakRef = new WeakReference<>(session); + } + + @Override + public Boolean apply(InlineFillUi inlineFillUi) { + Session session = mSessionWeakRef.get(); + + if (logIfSessionNull( + session, "AugmentedAutofillInlineSuggestionsResponseCallback:")) { + return false; + } + + synchronized (session.mLock) { + return session.mInlineSessionController.setInlineFillUiLocked(inlineFillUi); + } + } + } + + private static class AugmentedAutofillErrorCallback implements Runnable { + + WeakReference<Session> mSessionWeakRef; + + AugmentedAutofillErrorCallback(Session session) { + this.mSessionWeakRef = new WeakReference<>(session); + } + + @Override + public void run() { + Session session = mSessionWeakRef.get(); + + if (logIfSessionNull(session, "AugmentedAutofillErrorCallback:")) { + return; + } + session.onAugmentedAutofillErrorCallback(); + } + } + + /** + * If the session is null or has been destroyed, log the error msg, and return true. + * This is a helper function intended to be called when de-referencing from a weak reference. + * @param session + * @param logPrefix + * @return true if the session is null, false otherwise. + */ + private static boolean logIfSessionNull(Session session, String logPrefix) { + if (session == null) { + Slog.wtf(TAG, logPrefix + " Session null"); + return true; + } + if (session.mDestroyed) { + // TODO: Update this to return in this block. We aren't doing this to preserve the + // behavior, but can be modified once we have more time to soak the changes. + Slog.w(TAG, logPrefix + " Session destroyed, but following through"); + // Follow-through + } + return false; + } + + private void onAugmentedAutofillInlineSuggestionAccept( + InlineSuggestionsRequest inlineSuggestionsRequest, + AutofillId focussedId, + boolean isAllowlisted, + int mode, + AutofillValue currentValue) { + synchronized (mLock) { + final RemoteAugmentedAutofillService remoteService = + mService.getRemoteAugmentedAutofillServiceLocked(); + logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), + focussedId, isAllowlisted, inlineSuggestionsRequest != null); + remoteService.onRequestAutofillLocked(id, mClient, + taskId, mComponentName, mActivityToken, + AutofillId.withoutSession(focussedId), currentValue, + inlineSuggestionsRequest, + new AugmentedAutofillInlineSuggestionsResponseCallback(this), + new AugmentedAutofillErrorCallback(this), + mService.getRemoteInlineSuggestionRenderServiceLocked(), userId); + } + } + + private void onAugmentedAutofillErrorCallback() { + synchronized (mLock) { + cancelAugmentedAutofillLocked(); + + // Also cancel augmented in IME + mInlineSessionController.setInlineFillUiLocked( + InlineFillUi.emptyUi(mCurrentViewId)); + } + } + @GuardedBy("mLock") private void cancelAugmentedAutofillLocked() { final RemoteAugmentedAutofillService remoteService = mService diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 51359add8fd1..cfd9f16541f2 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -50,6 +50,7 @@ import android.content.Context; import android.content.pm.ActivityPresentationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.os.Binder; @@ -69,6 +70,7 @@ import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.provider.Settings; import android.service.contentcapture.ActivityEvent.ActivityEventType; +import android.service.contentcapture.ContentCaptureServiceInfo; import android.service.contentcapture.IDataShareCallback; import android.service.contentcapture.IDataShareReadAdapter; import android.service.voice.VoiceInteractionManagerInternal; @@ -79,6 +81,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.contentcapture.ContentCaptureCondition; +import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.ContentCaptureHelper; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.DataRemovalRequest; @@ -88,11 +91,15 @@ import android.view.contentcapture.IContentCaptureOptionsCallback; import android.view.contentcapture.IDataShareWriteAdapter; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AbstractRemoteService; import com.android.internal.infra.GlobalWhitelistState; import com.android.internal.os.IResultReceiver; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; +import com.android.server.contentprotection.ContentProtectionBlocklistManager; +import com.android.server.contentprotection.ContentProtectionPackageManager; +import com.android.server.contentprotection.RemoteContentProtectionService; import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.FrameworkResourcesServiceNameResolver; @@ -117,7 +124,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * with other sources to provide contextual data in other areas of the system * such as Autofill. */ -public final class ContentCaptureManagerService extends +public class ContentCaptureManagerService extends AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> { private static final String TAG = ContentCaptureManagerService.class.getSimpleName(); @@ -205,6 +212,10 @@ public final class ContentCaptureManagerService extends final GlobalContentCaptureOptions mGlobalContentCaptureOptions = new GlobalContentCaptureOptions(); + @Nullable private final ComponentName mContentProtectionServiceComponentName; + + @Nullable private final ContentProtectionBlocklistManager mContentProtectionBlocklistManager; + public ContentCaptureManagerService(@NonNull Context context) { super(context, new FrameworkResourcesServiceNameResolver(context, com.android.internal.R.string.config_defaultContentCaptureService), @@ -242,6 +253,20 @@ public final class ContentCaptureManagerService extends mServiceNameResolver.getServiceName(userId), mServiceNameResolver.isTemporary(userId)); } + + if (getEnableContentProtectionReceiverLocked()) { + mContentProtectionServiceComponentName = getContentProtectionServiceComponentName(); + if (mContentProtectionServiceComponentName != null) { + mContentProtectionBlocklistManager = createContentProtectionBlocklistManager(); + mContentProtectionBlocklistManager.updateBlocklist( + mDevCfgContentProtectionAppsBlocklistSize); + } else { + mContentProtectionBlocklistManager = null; + } + } else { + mContentProtectionServiceComponentName = null; + mContentProtectionBlocklistManager = null; + } } @Override // from AbstractMasterSystemService @@ -397,7 +422,9 @@ public final class ContentCaptureManagerService extends } } - private void setFineTuneParamsFromDeviceConfig() { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + protected void setFineTuneParamsFromDeviceConfig() { synchronized (mLock) { mDevCfgMaxBufferSize = DeviceConfig.getInt( @@ -443,6 +470,8 @@ public final class ContentCaptureManagerService extends ContentCaptureManager .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE, ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE); + // mContentProtectionBlocklistManager.updateBlocklist not called on purpose here to keep + // it immutable at this point mDevCfgContentProtectionBufferSize = DeviceConfig.getInt( DeviceConfig.NAMESPACE_CONTENT_CAPTURE, @@ -754,6 +783,98 @@ public final class ContentCaptureManagerService extends mGlobalContentCaptureOptions.dump(prefix2, pw); } + /** + * Used by the constructor in order to be able to override the value in the tests. + * + * @hide + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @GuardedBy("mLock") + protected boolean getEnableContentProtectionReceiverLocked() { + return mDevCfgEnableContentProtectionReceiver; + } + + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @NonNull + protected ContentProtectionBlocklistManager createContentProtectionBlocklistManager() { + return new ContentProtectionBlocklistManager( + new ContentProtectionPackageManager(getContext())); + } + + @Nullable + private ComponentName getContentProtectionServiceComponentName() { + String flatComponentName = getContentProtectionServiceFlatComponentName(); + ComponentName componentName = ComponentName.unflattenFromString(flatComponentName); + if (componentName == null) { + return null; + } + + // Check permissions by trying to construct {@link ContentCaptureServiceInfo} + try { + createContentProtectionServiceInfo(componentName); + } catch (Exception ex) { + // Swallow, exception was already logged + return null; + } + + return componentName; + } + + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @Nullable + protected String getContentProtectionServiceFlatComponentName() { + return getContext() + .getString(com.android.internal.R.string.config_defaultContentProtectionService); + } + + /** + * Can also throw runtime exceptions such as {@link SecurityException}. + * + * @hide + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @NonNull + protected ContentCaptureServiceInfo createContentProtectionServiceInfo( + @NonNull ComponentName componentName) throws PackageManager.NameNotFoundException { + return new ContentCaptureServiceInfo( + getContext(), componentName, /* isTemp= */ false, UserHandle.getCallingUserId()); + } + + @Nullable + private RemoteContentProtectionService createRemoteContentProtectionService() { + if (mContentProtectionServiceComponentName == null) { + // This case should not be possible but make sure + return null; + } + synchronized (mLock) { + if (!mDevCfgEnableContentProtectionReceiver) { + return null; + } + } + return createRemoteContentProtectionService(mContentProtectionServiceComponentName); + } + + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @NonNull + protected RemoteContentProtectionService createRemoteContentProtectionService( + @NonNull ComponentName componentName) { + return new RemoteContentProtectionService( + getContext(), + componentName, + UserHandle.getCallingUserId(), + isBindInstantServiceAllowed()); + } + + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @NonNull + protected ContentCaptureManagerServiceStub getContentCaptureManagerServiceStub() { + return mContentCaptureManagerServiceStub; + } + final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub { @Override @@ -987,6 +1108,19 @@ public final class ContentCaptureManagerService extends public void setDefaultServiceEnabled(@UserIdInt int userId, boolean enabled) { ContentCaptureManagerService.this.setDefaultServiceEnabled(userId, enabled); } + + @Override + public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { + RemoteContentProtectionService service = createRemoteContentProtectionService(); + if (service == null) { + return; + } + try { + service.onLoginDetected(events); + } catch (Exception ex) { + Slog.e(TAG, "Failed to call remote service", ex); + } + } } private final class LocalService extends ContentCaptureManagerInternal { @@ -1075,14 +1209,21 @@ public final class ContentCaptureManagerService extends @GuardedBy("mGlobalWhitelistStateLock") public ContentCaptureOptions getOptions(@UserIdInt int userId, @NonNull String packageName) { - boolean packageWhitelisted; + boolean isContentCaptureReceiverEnabled; + boolean isContentProtectionReceiverEnabled; ArraySet<ComponentName> whitelistedComponents = null; + synchronized (mGlobalWhitelistStateLock) { - packageWhitelisted = isWhitelisted(userId, packageName); - if (!packageWhitelisted) { - // Full package is not allowlisted: check individual components first + isContentCaptureReceiverEnabled = + isContentCaptureReceiverEnabled(userId, packageName); + isContentProtectionReceiverEnabled = + isContentProtectionReceiverEnabled(packageName); + + if (!isContentCaptureReceiverEnabled) { + // Full package is not allowlisted: check individual components next whitelistedComponents = getWhitelistedComponents(userId, packageName); - if (whitelistedComponents == null + if (!isContentProtectionReceiverEnabled + && whitelistedComponents == null && packageName.equals(mServicePackages.get(userId))) { // No components allowlisted either, but let it go because it's the // service's own package @@ -1101,7 +1242,9 @@ public final class ContentCaptureManagerService extends } } - if (!packageWhitelisted && whitelistedComponents == null) { + if (!isContentCaptureReceiverEnabled + && !isContentProtectionReceiverEnabled + && whitelistedComponents == null) { // No can do! if (verbose) { Slog.v(TAG, "getOptionsForPackage(" + packageName + "): not whitelisted"); @@ -1118,9 +1261,9 @@ public final class ContentCaptureManagerService extends mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize, mDevCfgDisableFlushForViewTreeAppearing, - /* enableReceiver= */ true, + isContentCaptureReceiverEnabled || whitelistedComponents != null, new ContentCaptureOptions.ContentProtectionOptions( - mDevCfgEnableContentProtectionReceiver, + isContentProtectionReceiverEnabled, mDevCfgContentProtectionBufferSize), whitelistedComponents); if (verbose) Slog.v(TAG, "getOptionsForPackage(" + packageName + "): " + options); @@ -1141,6 +1284,36 @@ public final class ContentCaptureManagerService extends } } } + + @Override // from GlobalWhitelistState + public boolean isWhitelisted(@UserIdInt int userId, @NonNull String packageName) { + return isContentCaptureReceiverEnabled(userId, packageName) + || isContentProtectionReceiverEnabled(packageName); + } + + @Override // from GlobalWhitelistState + public boolean isWhitelisted(@UserIdInt int userId, @NonNull ComponentName componentName) { + return super.isWhitelisted(userId, componentName) + || isContentProtectionReceiverEnabled(componentName.getPackageName()); + } + + private boolean isContentCaptureReceiverEnabled( + @UserIdInt int userId, @NonNull String packageName) { + return super.isWhitelisted(userId, packageName); + } + + private boolean isContentProtectionReceiverEnabled(@NonNull String packageName) { + if (mContentProtectionServiceComponentName == null + || mContentProtectionBlocklistManager == null) { + return false; + } + synchronized (mLock) { + if (!mDevCfgEnableContentProtectionReceiver) { + return false; + } + } + return mContentProtectionBlocklistManager.isAllowed(packageName); + } } private static class DataShareCallbackDelegate extends IDataShareCallback.Stub { diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java index 715cf9a8807e..a0fd28b3f279 100644 --- a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java +++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java @@ -35,7 +35,7 @@ import java.util.stream.Collectors; * * @hide */ -class ContentProtectionBlocklistManager { +public class ContentProtectionBlocklistManager { private static final String TAG = "ContentProtectionBlocklistManager"; @@ -46,7 +46,7 @@ class ContentProtectionBlocklistManager { @Nullable private Set<String> mPackageNameBlocklist; - protected ContentProtectionBlocklistManager( + public ContentProtectionBlocklistManager( @NonNull ContentProtectionPackageManager contentProtectionPackageManager) { mContentProtectionPackageManager = contentProtectionPackageManager; } diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java index 1847e5d708a3..4ebac07ec3ea 100644 --- a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java +++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java @@ -43,7 +43,7 @@ public class ContentProtectionPackageManager { @NonNull private final PackageManager mPackageManager; - ContentProtectionPackageManager(@NonNull Context context) { + public ContentProtectionPackageManager(@NonNull Context context) { mPackageManager = context.getPackageManager(); } diff --git a/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java b/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java index 7b34e737b58c..f5e5a431e3dd 100644 --- a/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java +++ b/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java @@ -37,7 +37,8 @@ import java.time.Duration; * * @hide */ -class RemoteContentProtectionService extends ServiceConnector.Impl<IContentCaptureDirectManager> { +public class RemoteContentProtectionService + extends ServiceConnector.Impl<IContentCaptureDirectManager> { private static final String TAG = RemoteContentProtectionService.class.getSimpleName(); @@ -45,7 +46,7 @@ class RemoteContentProtectionService extends ServiceConnector.Impl<IContentCaptu @NonNull private final ComponentName mComponentName; - protected RemoteContentProtectionService( + public RemoteContentProtectionService( @NonNull Context context, @NonNull ComponentName componentName, int userId, diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 7d016c82adc5..26421b75c297 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -313,6 +313,11 @@ public class BootReceiver extends BroadcastReceiver { private static final DropboxRateLimiter sDropboxRateLimiter = new DropboxRateLimiter(); + /** Initialize the rate limiter. */ + public static void initDropboxRateLimiter() { + sDropboxRateLimiter.init(); + } + /** * Reset the dropbox rate limiter. */ diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 96b660ea03f1..d4845aa99810 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -70,6 +70,7 @@ import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_FGS_PERMI import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN; +import static android.os.PowerExemptionManager.REASON_OTHER; import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI; @@ -85,7 +86,6 @@ import static android.os.PowerExemptionManager.REASON_SYSTEM_MODULE; import static android.os.PowerExemptionManager.REASON_SYSTEM_UID; import static android.os.PowerExemptionManager.REASON_TEMP_ALLOWED_WHILE_IN_USE; import static android.os.PowerExemptionManager.REASON_UID_VISIBLE; -import static android.os.PowerExemptionManager.REASON_UNKNOWN; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.PowerExemptionManager.getReasonCodeFromProcState; import static android.os.PowerExemptionManager.reasonCodeToString; @@ -7429,33 +7429,34 @@ public final class ActiveServices { boolean isStartService) { // Check DeviceConfig flag. if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) { + if (!r.mAllowWhileInUsePermissionInFgs) { + // BGFGS start restrictions are disabled. We're allowing while-in-use permissions. + // Note REASON_OTHER since there's no other suitable reason. + r.mAllowWhileInUsePermissionInFgsReason = REASON_OTHER; + } r.mAllowWhileInUsePermissionInFgs = true; } - final @ReasonCode int allowWhileInUse; - // Either (or both) mAllowWhileInUsePermissionInFgs or mAllowStartForeground is // newly allowed? boolean newlyAllowed = false; if (!r.mAllowWhileInUsePermissionInFgs || (r.mAllowStartForeground == REASON_DENIED)) { - allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( + @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges, isBindService); // We store them to compare the old and new while-in-use logics to each other. // (They're not used for any other purposes.) if (!r.mAllowWhileInUsePermissionInFgs) { r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED); + r.mAllowWhileInUsePermissionInFgsReason = allowWhileInUse; } if (r.mAllowStartForeground == REASON_DENIED) { r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked( allowWhileInUse, callingPackage, callingPid, callingUid, intent, r, backgroundStartPrivileges, isBindService); } - } else { - allowWhileInUse = REASON_UNKNOWN; } - r.mAllowWhileInUsePermissionInFgsReason = allowWhileInUse; } /** diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 44e198b53761..ee7791461cc5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -1345,6 +1345,8 @@ final class ActivityManagerConstants extends ContentObserver { // The following read from Settings. updateActivityStartsLoggingEnabled(); updateForegroundServiceStartsLoggingEnabled(); + // Read DropboxRateLimiter params from flags. + mService.initDropboxRateLimiter(); } private void loadDeviceConfigConstants() { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a6debf6f3d51..31a9e9205ecf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -9206,6 +9206,11 @@ public class ActivityManagerService extends IActivityManager.Stub private final DropboxRateLimiter mDropboxRateLimiter = new DropboxRateLimiter(); + /** Initializes the Dropbox Rate Limiter parameters from flags. */ + public void initDropboxRateLimiter() { + mDropboxRateLimiter.init(); + } + /** * Write a description of an error (crash, WTF, ANR) to the drop box. * @param eventType to include in the drop box tag ("crash", "wtf", etc.) @@ -19527,7 +19532,7 @@ public class ActivityManagerService extends IActivityManager.Stub for (Display display : allDisplays) { int displayId = display.getDisplayId(); // TODO(b/247592632): check other properties like isSecure or proper display type - if (display.isValid() + if (display.isValid() && ((display.getFlags() & Display.FLAG_PRIVATE) == 0) && (allowOnDefaultDisplay || displayId != Display.DEFAULT_DISPLAY)) { displayIds[numberValidDisplays++] = displayId; } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 0b5b1cb2902e..4b6d32427d68 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -25,6 +25,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UptimeMillisLong; +import android.app.BroadcastOptions; import android.content.Intent; import android.content.pm.ResolveInfo; import android.os.SystemClock; @@ -257,7 +258,10 @@ class BroadcastProcessQueue { deferredStatesApplyConsumer.accept(record, recordIndex); } - if (record.isReplacePending()) { + // Ignore FLAG_RECEIVER_REPLACE_PENDING if the sender specified the policy using the + // BroadcastOptions delivery group APIs. + if (record.isReplacePending() + && record.getDeliveryGroupPolicy() == BroadcastOptions.DELIVERY_GROUP_POLICY_ALL) { final BroadcastRecord replacedBroadcastRecord = replaceBroadcast(record, recordIndex); if (replacedBroadcastRecord != null) { return replacedBroadcastRecord; diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index f6004d7d2b7f..c6165cd3220e 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -339,7 +339,7 @@ public class BroadcastQueueImpl extends BroadcastQueue { private BroadcastRecord replaceBroadcastLocked(ArrayList<BroadcastRecord> queue, BroadcastRecord r, String typeForLogging) { final Intent intent = r.intent; - for (int i = queue.size() - 1; i > 0; i--) { + for (int i = queue.size() - 1; i >= 0; i--) { final BroadcastRecord old = queue.get(i); if (old.userId == r.userId && intent.filterEquals(old.intent)) { if (DEBUG_BROADCAST) { diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index d8c7eefdfcc6..5356fdfe9a4c 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -719,11 +719,12 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private void skipAndCancelReplacedBroadcasts(ArraySet<BroadcastRecord> replacedBroadcasts) { for (int i = 0; i < replacedBroadcasts.size(); ++i) { final BroadcastRecord r = replacedBroadcasts.valueAt(i); - r.resultCode = Activity.RESULT_CANCELED; - r.resultData = null; - r.resultExtras = null; - scheduleResultTo(r); - notifyFinishBroadcast(r); + // Skip all the receivers in the replaced broadcast + for (int rcvrIdx = 0; rcvrIdx < r.receivers.size(); ++rcvrIdx) { + if (!isDeliveryStateTerminal(r.getDeliveryState(rcvrIdx))) { + mBroadcastConsumerSkipAndCanceled.accept(r, rcvrIdx); + } + } } } diff --git a/services/core/java/com/android/server/am/DropboxRateLimiter.java b/services/core/java/com/android/server/am/DropboxRateLimiter.java index b5c7215df899..003e6141aeb0 100644 --- a/services/core/java/com/android/server/am/DropboxRateLimiter.java +++ b/services/core/java/com/android/server/am/DropboxRateLimiter.java @@ -17,6 +17,7 @@ package com.android.server.am; import android.os.SystemClock; +import android.provider.DeviceConfig; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.Slog; @@ -30,17 +31,26 @@ public class DropboxRateLimiter { // After RATE_LIMIT_ALLOWED_ENTRIES have been collected (for a single breakdown of // process/eventType) further entries will be rejected until RATE_LIMIT_BUFFER_DURATION has // elapsed, after which the current count for this breakdown will be reset. - private static final long RATE_LIMIT_BUFFER_DURATION = 10 * DateUtils.MINUTE_IN_MILLIS; + private static final long RATE_LIMIT_BUFFER_DURATION_DEFAULT = 10 * DateUtils.MINUTE_IN_MILLIS; // Indicated how many buffer durations to wait before the rate limit buffer will be cleared. // E.g. if set to 3 will wait 3xRATE_LIMIT_BUFFER_DURATION before clearing the buffer. - private static final long RATE_LIMIT_BUFFER_EXPIRY_FACTOR = 3; + private static final long RATE_LIMIT_BUFFER_EXPIRY_FACTOR_DEFAULT = 3; // The number of entries to keep per breakdown of process/eventType. - private static final int RATE_LIMIT_ALLOWED_ENTRIES = 6; + private static final int RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT = 6; // If a process is rate limited twice in a row we consider it crash-looping and rate limit it // more aggressively. - private static final int STRICT_RATE_LIMIT_ALLOWED_ENTRIES = 1; - private static final long STRICT_RATE_LIMIT_BUFFER_DURATION = 20 * DateUtils.MINUTE_IN_MILLIS; + private static final int STRICT_RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT = 1; + private static final long STRICT_RATE_LIMIT_BUFFER_DURATION_DEFAULT = + 20 * DateUtils.MINUTE_IN_MILLIS; + + private static final String FLAG_NAMESPACE = "dropbox"; + + private long mRateLimitBufferDuration; + private long mRateLimitBufferExpiryFactor; + private int mRateLimitAllowedEntries; + private int mStrictRatelimitAllowedEntries; + private long mStrictRateLimitBufferDuration; @GuardedBy("mErrorClusterRecords") private final ArrayMap<String, ErrorRecord> mErrorClusterRecords = new ArrayMap<>(); @@ -54,6 +64,36 @@ public class DropboxRateLimiter { public DropboxRateLimiter(Clock clock) { mClock = clock; + + mRateLimitBufferDuration = RATE_LIMIT_BUFFER_DURATION_DEFAULT; + mRateLimitBufferExpiryFactor = RATE_LIMIT_BUFFER_EXPIRY_FACTOR_DEFAULT; + mRateLimitAllowedEntries = RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT; + mStrictRatelimitAllowedEntries = STRICT_RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT; + mStrictRateLimitBufferDuration = STRICT_RATE_LIMIT_BUFFER_DURATION_DEFAULT; + } + + /** Initializes the rate limiter parameters from flags. */ + public void init() { + mRateLimitBufferDuration = DeviceConfig.getLong( + FLAG_NAMESPACE, + "DropboxRateLimiter__rate_limit_buffer_duration", + RATE_LIMIT_BUFFER_DURATION_DEFAULT); + mRateLimitBufferExpiryFactor = DeviceConfig.getLong( + FLAG_NAMESPACE, + "DropboxRateLimiter__rate_limit_buffer_expiry_factor", + RATE_LIMIT_BUFFER_EXPIRY_FACTOR_DEFAULT); + mRateLimitAllowedEntries = DeviceConfig.getInt( + FLAG_NAMESPACE, + "DropboxRateLimiter__rate_limit_allowed_entries", + RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT); + mStrictRatelimitAllowedEntries = DeviceConfig.getInt( + FLAG_NAMESPACE, + "DropboxRateLimiter__strict_rate_limit_allowed_entries", + STRICT_RATE_LIMIT_ALLOWED_ENTRIES_DEFAULT); + mStrictRateLimitBufferDuration = DeviceConfig.getLong( + FLAG_NAMESPACE, + "DropboxRateLimiter__strict_rate_limit_buffer_duration", + STRICT_RATE_LIMIT_BUFFER_DURATION_DEFAULT); } /** The interface clock to use for tracking the time elapsed. */ @@ -116,7 +156,7 @@ public class DropboxRateLimiter { private void maybeRemoveExpiredRecords(long currentTime) { if (currentTime - mLastMapCleanUp - <= RATE_LIMIT_BUFFER_EXPIRY_FACTOR * RATE_LIMIT_BUFFER_DURATION) { + <= mRateLimitBufferExpiryFactor * mRateLimitBufferDuration) { return; } @@ -219,15 +259,15 @@ public class DropboxRateLimiter { } public int getAllowedEntries() { - return isRepeated() ? STRICT_RATE_LIMIT_ALLOWED_ENTRIES : RATE_LIMIT_ALLOWED_ENTRIES; + return isRepeated() ? mStrictRatelimitAllowedEntries : mRateLimitAllowedEntries; } public long getBufferDuration() { - return isRepeated() ? STRICT_RATE_LIMIT_BUFFER_DURATION : RATE_LIMIT_BUFFER_DURATION; + return isRepeated() ? mStrictRateLimitBufferDuration : mRateLimitBufferDuration; } public boolean hasExpired(long currentTime) { - long bufferExpiry = RATE_LIMIT_BUFFER_EXPIRY_FACTOR * getBufferDuration(); + long bufferExpiry = mRateLimitBufferExpiryFactor * getBufferDuration(); return currentTime - mStartTime > bufferExpiry; } } diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 80406e6f66e4..38e7371e7075 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -113,6 +113,11 @@ public class ForegroundServiceTypeLoggerModule { // We use this to get the duration an API was active after // the stop call. final SparseArray<Long> mLastFgsTimeStamp = new SparseArray<>(); + + // A map of API types to first FGS start call timestamps + // We use this to get the duration an API was active after + // the stop call. + final SparseArray<Long> mFirstFgsTimeStamp = new SparseArray<>(); } // SparseArray that tracks all UIDs that have made various @@ -146,6 +151,7 @@ public class ForegroundServiceTypeLoggerModule { if (fgsIndex < 0) { uidState.mRunningFgs.put(apiType, new ArrayMap<>()); fgsIndex = uidState.mRunningFgs.indexOfKey(apiType); + uidState.mFirstFgsTimeStamp.put(apiType, System.currentTimeMillis()); } final ArrayMap<ComponentName, ServiceRecord> fgsList = uidState.mRunningFgs.valueAt(fgsIndex); @@ -237,7 +243,7 @@ public class ForegroundServiceTypeLoggerModule { // there's no more FGS running for this type, just get rid of it uidState.mRunningFgs.remove(apiType); // but we need to keep track of the timestamp in case an API stops - uidState.mLastFgsTimeStamp.put(apiType, record.mFgsExitTime); + uidState.mLastFgsTimeStamp.put(apiType, System.currentTimeMillis()); } } if (!apisFound.isEmpty()) { @@ -454,8 +460,17 @@ public class ForegroundServiceTypeLoggerModule { public void logFgsApiEvent(ServiceRecord r, int fgsState, @FgsApiState int apiState, @ForegroundServiceApiType int apiType, long timestamp) { - final long apiDurationBeforeFgsStart = r.mFgsEnterTime - timestamp; - final long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime; + long apiDurationBeforeFgsStart = r.createRealTime - timestamp; + long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime; + UidState uidState = mUids.get(r.appInfo.uid); + if (uidState != null) { + if (uidState.mFirstFgsTimeStamp.contains(apiType)) { + apiDurationBeforeFgsStart = uidState.mFirstFgsTimeStamp.get(apiType) - timestamp; + } + if (uidState.mLastFgsTimeStamp.contains(apiType)) { + apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); + } + } final int[] apiTypes = new int[1]; apiTypes[0] = apiType; final long[] timeStamps = new long[1]; diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index 7841b699ec98..ffe5a6e6b958 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -18,6 +18,7 @@ package com.android.server.am; import android.annotation.UptimeMillisLong; import android.app.ActivityManagerInternal.OomAdjReason; +import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -320,5 +321,8 @@ final class ProcessCachedOptimizerRecord { pw.print(prefix); pw.print("isFreezeExempt="); pw.print(mFreezeExempt); pw.print(" isPendingFreeze="); pw.print(mPendingFreeze); pw.print(" " + IS_FROZEN + "="); pw.println(mFrozen); + pw.print(prefix); pw.print("earliestFreezableTimeMs="); + TimeUtils.formatDuration(mEarliestFreezableTimeMillis, nowUptime, pw); + pw.println(); } } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 8c227f5488d3..9db9e77c82c9 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -72,6 +72,10 @@ public class SettingsToPropertiesMapper { Settings.Global.NATIVE_FLAGS_HEALTH_CHECK_ENABLED, }; + // TODO(b/282593625): Move this constant to DeviceConfig module + private static final String NAMESPACE_TETHERING_U_OR_LATER_NATIVE = + "tethering_u_or_later_native"; + // All the flags under the listed DeviceConfig scopes will be synced to native level. // // NOTE: please grant write permission system property prefix @@ -106,7 +110,8 @@ public class SettingsToPropertiesMapper { DeviceConfig.NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT, DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE_BOOT, DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE, - DeviceConfig.NAMESPACE_HDMI_CONTROL + DeviceConfig.NAMESPACE_HDMI_CONTROL, + NAMESPACE_TETHERING_U_OR_LATER_NATIVE }; private final String[] mGlobalSettings; diff --git a/services/core/java/com/android/server/am/UidObserverController.java b/services/core/java/com/android/server/am/UidObserverController.java index a2582083c409..a6677a5185ca 100644 --- a/services/core/java/com/android/server/am/UidObserverController.java +++ b/services/core/java/com/android/server/am/UidObserverController.java @@ -429,21 +429,23 @@ public class UidObserverController { } } - pw.println(); - pw.print(" mUidChangeDispatchCount="); - pw.print(mUidChangeDispatchCount); - pw.println(); - pw.println(" Slow UID dispatches:"); - for (int i = 0; i < count; i++) { - final UidObserverRegistration reg = (UidObserverRegistration) - mUidObservers.getRegisteredCallbackCookie(i); - pw.print(" "); - pw.print(mUidObservers.getRegisteredCallbackItem(i).getClass().getTypeName()); - pw.print(": "); - pw.print(reg.mSlowDispatchCount); - pw.print(" / Max "); - pw.print(reg.mMaxDispatchTime); - pw.println("ms"); + if (dumpPackage == null) { + pw.println(); + pw.print(" mUidChangeDispatchCount="); + pw.print(mUidChangeDispatchCount); + pw.println(); + pw.println(" Slow UID dispatches:"); + for (int i = 0; i < count; i++) { + final UidObserverRegistration reg = (UidObserverRegistration) + mUidObservers.getRegisteredCallbackCookie(i); + pw.print(" "); + pw.print(mUidObservers.getRegisteredCallbackItem(i).getClass().getTypeName()); + pw.print(": "); + pw.print(reg.mSlowDispatchCount); + pw.print(" / Max "); + pw.print(reg.mMaxDispatchTime); + pw.println("ms"); + } } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index ad65490bfc8f..2ba522373b10 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -2210,8 +2210,8 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.setPreferredDevicesForStrategyInt( mAccessibilityStrategyId, Arrays.asList(defaultDevice)); } else { - mDeviceInventory.removePreferredDevicesForStrategInt(mCommunicationStrategyId); - mDeviceInventory.removePreferredDevicesForStrategInt(mAccessibilityStrategyId); + mDeviceInventory.removePreferredDevicesForStrategyInt(mCommunicationStrategyId); + mDeviceInventory.removePreferredDevicesForStrategyInt(mAccessibilityStrategyId); } mDeviceInventory.applyConnectedDevicesRoles(); } else { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 7b2e732702a3..d1cae490a31d 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -823,7 +823,7 @@ public class AudioDeviceInventory { return status; } // Only used for internal requests - /*package*/ int removePreferredDevicesForStrategInt(int strategy) { + /*package*/ int removePreferredDevicesForStrategyInt(int strategy) { return clearDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_PREFERRED, true /*internal */); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ed66432b9f58..db08bea8dfb1 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -3593,16 +3593,15 @@ public class AudioService extends IAudioService.Stub synchronized (mHdmiClientLock) { if (mHdmiManager != null) { // At most one of mHdmiPlaybackClient and mHdmiTvClient should be non-null - HdmiClient fullVolumeHdmiClient = mHdmiPlaybackClient; + HdmiClient hdmiClient = mHdmiPlaybackClient; if (mHdmiTvClient != null) { - fullVolumeHdmiClient = mHdmiTvClient; + hdmiClient = mHdmiTvClient; } - if (fullVolumeHdmiClient != null + if (((mHdmiPlaybackClient != null && isFullVolumeDevice(device)) + || (mHdmiTvClient != null && mHdmiSystemAudioSupported)) && mHdmiCecVolumeControlEnabled - && streamTypeAlias == AudioSystem.STREAM_MUSIC - // vol change on a full volume device - && isFullVolumeDevice(device)) { + && streamTypeAlias == AudioSystem.STREAM_MUSIC) { int keyCode = KeyEvent.KEYCODE_UNKNOWN; switch (direction) { case AudioManager.ADJUST_RAISE: @@ -3626,14 +3625,14 @@ public class AudioService extends IAudioService.Stub try { switch (keyEventMode) { case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL: - fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true); - fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false); + hdmiClient.sendVolumeKeyEvent(keyCode, true); + hdmiClient.sendVolumeKeyEvent(keyCode, false); break; case AudioDeviceVolumeManager.ADJUST_MODE_START: - fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true); + hdmiClient.sendVolumeKeyEvent(keyCode, true); break; case AudioDeviceVolumeManager.ADJUST_MODE_END: - fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false); + hdmiClient.sendVolumeKeyEvent(keyCode, false); break; default: Log.e(TAG, "Invalid keyEventMode " + keyEventMode); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 9de2578df2ad..1162231b23aa 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -4598,6 +4598,22 @@ public final class DisplayManagerService extends SystemService { } @Override + public AmbientLightSensorData getAmbientLightSensorData(int displayId) { + synchronized (mSyncRoot) { + final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (display == null) { + return null; + } + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + return null; + } + SensorData data = device.getDisplayDeviceConfig().getAmbientLightSensor(); + return new AmbientLightSensorData(data.name, data.type); + } + } + + @Override public IntArray getDisplayGroupIds() { Set<Integer> visitedIds = new ArraySet<>(); IntArray displayGroupIds = new IntArray(); diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 1722fc97da0b..dec9f62c8739 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -701,11 +701,15 @@ final class LocalDisplayAdapter extends DisplayAdapter { maxDisplayMode == null ? mInfo.width : maxDisplayMode.getPhysicalWidth(); final int maxHeight = maxDisplayMode == null ? mInfo.height : maxDisplayMode.getPhysicalHeight(); - mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, - mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); - mInfo.roundedCorners = RoundedCorners.fromResources( - res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + // We cannot determine cutouts and rounded corners of external displays. + if (mStaticDisplayInfo.isInternal) { + mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, + mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + mInfo.roundedCorners = RoundedCorners.fromResources( + res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + } + mInfo.installOrientation = mStaticDisplayInfo.installOrientation; mInfo.displayShape = DisplayShape.fromResources( diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 2c54e1cea3fa..3eab4b0b1835 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1579,6 +1579,10 @@ public class HdmiControlService extends SystemService { // If the device is not TV, we can't convert path to port-id, so stop here. return true; } + // Invalidate the physical address if parameters length is too short. + if (params.length < offset + 2) { + return false; + } int path = HdmiUtils.twoBytesToInt(params, offset); if (path != Constants.INVALID_PHYSICAL_ADDRESS && path == getPhysicalAddress()) { return true; diff --git a/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java b/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java new file mode 100644 index 000000000000..ce868497b0e4 --- /dev/null +++ b/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java @@ -0,0 +1,431 @@ +/* + * Copyright 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.server.input; + +import android.annotation.MainThread; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManagerInternal; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.Slog; +import android.util.TypedValue; +import android.view.Display; +import android.view.DisplayInfo; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; +import com.android.server.display.utils.SensorUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard + * backlight based on ambient light sensor. + */ +final class AmbientKeyboardBacklightController implements DisplayManager.DisplayListener, + SensorEventListener { + + private static final String TAG = "KbdBacklightController"; + + // To enable these logs, run: + // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // Number of light sensor responses required to overcome temporal hysteresis. + @VisibleForTesting + public static final int HYSTERESIS_THRESHOLD = 2; + + private static final int MSG_BRIGHTNESS_CALLBACK = 0; + private static final int MSG_SETUP_DISPLAY_AND_SENSOR = 1; + + private static final Object sAmbientControllerLock = new Object(); + + private final Context mContext; + private final Handler mHandler; + + @Nullable + @GuardedBy("sAmbientControllerLock") + private Sensor mLightSensor; + @GuardedBy("sAmbientControllerLock") + private String mCurrentDefaultDisplayUniqueId; + + // List of currently registered ambient backlight listeners + @GuardedBy("sAmbientControllerLock") + private final List<AmbientKeyboardBacklightListener> mAmbientKeyboardBacklightListeners = + new ArrayList<>(); + + private BrightnessStep[] mBrightnessSteps; + private int mCurrentBrightnessStepIndex; + private HysteresisState mHysteresisState; + private int mHysteresisCount = 0; + private float mSmoothingConstant; + private int mSmoothedLux; + private int mSmoothedLuxAtLastAdjustment; + + private enum HysteresisState { + // The most-recent mSmoothedLux matched mSmoothedLuxAtLastAdjustment. + STABLE, + // The most-recent mSmoothedLux was less than mSmoothedLuxAtLastAdjustment. + DECREASING, + // The most-recent mSmoothedLux was greater than mSmoothedLuxAtLastAdjustment. + INCREASING, + // The brightness should be adjusted immediately after the next sensor reading. + IMMEDIATE, + } + + AmbientKeyboardBacklightController(Context context, Looper looper) { + mContext = context; + mHandler = new Handler(looper, this::handleMessage); + initConfiguration(); + } + + public void systemRunning() { + mHandler.sendEmptyMessage(MSG_SETUP_DISPLAY_AND_SENSOR); + DisplayManager displayManager = Objects.requireNonNull( + mContext.getSystemService(DisplayManager.class)); + displayManager.registerDisplayListener(this, mHandler); + } + + public void registerAmbientBacklightListener(AmbientKeyboardBacklightListener listener) { + synchronized (sAmbientControllerLock) { + if (mAmbientKeyboardBacklightListeners.contains(listener)) { + throw new IllegalStateException( + "AmbientKeyboardBacklightListener was already registered, listener = " + + listener); + } + if (mAmbientKeyboardBacklightListeners.isEmpty()) { + // Add sensor listener when we add the first ambient backlight listener. + addSensorListener(mLightSensor); + } + mAmbientKeyboardBacklightListeners.add(listener); + } + } + + public void unregisterAmbientBacklightListener(AmbientKeyboardBacklightListener listener) { + synchronized (sAmbientControllerLock) { + if (!mAmbientKeyboardBacklightListeners.contains(listener)) { + throw new IllegalStateException( + "AmbientKeyboardBacklightListener was never registered, listener = " + + listener); + } + mAmbientKeyboardBacklightListeners.remove(listener); + if (mAmbientKeyboardBacklightListeners.isEmpty()) { + removeSensorListener(mLightSensor); + } + } + } + + private void sendBrightnessAdjustment(int brightnessValue) { + Message msg = Message.obtain(mHandler, MSG_BRIGHTNESS_CALLBACK, brightnessValue); + mHandler.sendMessage(msg); + } + + @MainThread + private void handleBrightnessCallback(int brightnessValue) { + synchronized (sAmbientControllerLock) { + for (AmbientKeyboardBacklightListener listener : mAmbientKeyboardBacklightListeners) { + listener.onKeyboardBacklightValueChanged(brightnessValue); + } + } + } + + @MainThread + private void handleAmbientLuxChange(float rawLux) { + if (rawLux < 0) { + Slog.w(TAG, "Light sensor doesn't have valid value"); + return; + } + updateSmoothedLux(rawLux); + + if (mHysteresisState != HysteresisState.IMMEDIATE + && mSmoothedLux == mSmoothedLuxAtLastAdjustment) { + mHysteresisState = HysteresisState.STABLE; + return; + } + + int newStepIndex = Math.max(0, mCurrentBrightnessStepIndex); + int numSteps = mBrightnessSteps.length; + + if (mSmoothedLux > mSmoothedLuxAtLastAdjustment) { + if (mHysteresisState != HysteresisState.IMMEDIATE + && mHysteresisState != HysteresisState.INCREASING) { + if (DEBUG) { + Slog.d(TAG, "ALS transitioned to brightness increasing state"); + } + mHysteresisState = HysteresisState.INCREASING; + mHysteresisCount = 0; + } + for (; newStepIndex < numSteps; newStepIndex++) { + if (mSmoothedLux < mBrightnessSteps[newStepIndex].mIncreaseLuxThreshold) { + break; + } + } + } else if (mSmoothedLux < mSmoothedLuxAtLastAdjustment) { + if (mHysteresisState != HysteresisState.IMMEDIATE + && mHysteresisState != HysteresisState.DECREASING) { + if (DEBUG) { + Slog.d(TAG, "ALS transitioned to brightness decreasing state"); + } + mHysteresisState = HysteresisState.DECREASING; + mHysteresisCount = 0; + } + for (; newStepIndex >= 0; newStepIndex--) { + if (mSmoothedLux > mBrightnessSteps[newStepIndex].mDecreaseLuxThreshold) { + break; + } + } + } + + if (mHysteresisState == HysteresisState.IMMEDIATE) { + mCurrentBrightnessStepIndex = newStepIndex; + mSmoothedLuxAtLastAdjustment = mSmoothedLux; + mHysteresisState = HysteresisState.STABLE; + mHysteresisCount = 0; + sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue); + return; + } + + if (newStepIndex == mCurrentBrightnessStepIndex) { + return; + } + + mHysteresisCount++; + if (DEBUG) { + Slog.d(TAG, "Incremented hysteresis count to " + mHysteresisCount + " (lux went from " + + mSmoothedLuxAtLastAdjustment + " to " + mSmoothedLux + ")"); + } + if (mHysteresisCount >= HYSTERESIS_THRESHOLD) { + mCurrentBrightnessStepIndex = newStepIndex; + mSmoothedLuxAtLastAdjustment = mSmoothedLux; + mHysteresisCount = 1; + sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue); + } + } + + @MainThread + private void handleDisplayChange() { + DisplayManagerInternal displayManagerInternal = LocalServices.getService( + DisplayManagerInternal.class); + DisplayInfo displayInfo = displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY); + synchronized (sAmbientControllerLock) { + if (Objects.equals(mCurrentDefaultDisplayUniqueId, displayInfo.uniqueId)) { + return; + } + if (DEBUG) { + Slog.d(TAG, "Default display changed: resetting the light sensor"); + } + // Keep track of current default display + mCurrentDefaultDisplayUniqueId = displayInfo.uniqueId; + // Clear all existing sensor listeners + if (!mAmbientKeyboardBacklightListeners.isEmpty()) { + removeSensorListener(mLightSensor); + } + mLightSensor = getAmbientLightSensor( + displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY)); + // Re-add sensor listeners if required; + if (!mAmbientKeyboardBacklightListeners.isEmpty()) { + addSensorListener(mLightSensor); + } + } + } + + private Sensor getAmbientLightSensor( + DisplayManagerInternal.AmbientLightSensorData ambientSensor) { + SensorManager sensorManager = Objects.requireNonNull( + mContext.getSystemService(SensorManager.class)); + if (DEBUG) { + Slog.d(TAG, "Ambient Light sensor data: " + ambientSensor); + } + return SensorUtils.findSensor(sensorManager, ambientSensor.sensorType, + ambientSensor.sensorName, Sensor.TYPE_LIGHT); + } + + private void updateSmoothedLux(float rawLux) { + // For the first sensor reading, use raw lux value directly without smoothing. + if (mHysteresisState == HysteresisState.IMMEDIATE) { + mSmoothedLux = (int) rawLux; + } else { + mSmoothedLux = + (int) (mSmoothingConstant * rawLux + (1 - mSmoothingConstant) * mSmoothedLux); + } + if (DEBUG) { + Slog.d(TAG, "Current smoothed lux from ALS = " + mSmoothedLux); + } + } + + @VisibleForTesting + public void addSensorListener(@Nullable Sensor sensor) { + SensorManager sensorManager = mContext.getSystemService(SensorManager.class); + if (sensorManager == null || sensor == null) { + return; + } + // Reset values before registering listener + reset(); + sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, mHandler); + if (DEBUG) { + Slog.d(TAG, "Registering ALS listener"); + } + } + + private void removeSensorListener(@Nullable Sensor sensor) { + SensorManager sensorManager = mContext.getSystemService(SensorManager.class); + if (sensorManager == null || sensor == null) { + return; + } + sensorManager.unregisterListener(this, sensor); + if (DEBUG) { + Slog.d(TAG, "Unregistering ALS listener"); + } + } + + private void initConfiguration() { + Resources res = mContext.getResources(); + int[] brightnessValueArray = res.getIntArray( + com.android.internal.R.array.config_autoKeyboardBacklightBrightnessValues); + int[] decreaseThresholdArray = res.getIntArray( + com.android.internal.R.array.config_autoKeyboardBacklightDecreaseLuxThreshold); + int[] increaseThresholdArray = res.getIntArray( + com.android.internal.R.array.config_autoKeyboardBacklightIncreaseLuxThreshold); + if (brightnessValueArray.length != decreaseThresholdArray.length + || decreaseThresholdArray.length != increaseThresholdArray.length) { + throw new IllegalArgumentException( + "The config files for auto keyboard backlight brightness must contain arrays " + + "of equal lengths"); + } + final int size = brightnessValueArray.length; + mBrightnessSteps = new BrightnessStep[size]; + for (int i = 0; i < size; i++) { + int increaseThreshold = + increaseThresholdArray[i] < 0 ? Integer.MAX_VALUE : increaseThresholdArray[i]; + int decreaseThreshold = + decreaseThresholdArray[i] < 0 ? Integer.MIN_VALUE : decreaseThresholdArray[i]; + mBrightnessSteps[i] = new BrightnessStep(brightnessValueArray[i], increaseThreshold, + decreaseThreshold); + } + + int numSteps = mBrightnessSteps.length; + if (numSteps == 0 || mBrightnessSteps[0].mDecreaseLuxThreshold != Integer.MIN_VALUE + || mBrightnessSteps[numSteps - 1].mIncreaseLuxThreshold != Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "The config files for auto keyboard backlight brightness must contain arrays " + + "of length > 0 and have -1 or Integer.MIN_VALUE as lower bound for " + + "decrease thresholds and -1 or Integer.MAX_VALUE as upper bound for " + + "increase thresholds"); + } + + final TypedValue smoothingConstantValue = new TypedValue(); + res.getValue( + com.android.internal.R.dimen.config_autoKeyboardBrightnessSmoothingConstant, + smoothingConstantValue, + true /*resolveRefs*/); + mSmoothingConstant = smoothingConstantValue.getFloat(); + if (mSmoothingConstant <= 0.0 || mSmoothingConstant > 1.0) { + throw new IllegalArgumentException( + "The config files for auto keyboard backlight brightness must contain " + + "smoothing constant in range (0.0, 1.0]."); + } + + if (DEBUG) { + Log.d(TAG, "Brightness steps: " + Arrays.toString(mBrightnessSteps) + + " Smoothing constant = " + mSmoothingConstant); + } + } + + private void reset() { + mHysteresisState = HysteresisState.IMMEDIATE; + mSmoothedLux = 0; + mSmoothedLuxAtLastAdjustment = 0; + mCurrentBrightnessStepIndex = -1; + } + + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_BRIGHTNESS_CALLBACK: + handleBrightnessCallback((int) msg.obj); + return true; + case MSG_SETUP_DISPLAY_AND_SENSOR: + handleDisplayChange(); + return true; + } + return false; + } + + @Override + public void onSensorChanged(SensorEvent event) { + handleAmbientLuxChange(event.values[0]); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onDisplayAdded(int displayId) { + handleDisplayChange(); + } + + @Override + public void onDisplayRemoved(int displayId) { + handleDisplayChange(); + } + + @Override + public void onDisplayChanged(int displayId) { + handleDisplayChange(); + } + + public interface AmbientKeyboardBacklightListener { + /** + * @param value between [0, 255] to which keyboard backlight needs to be set according + * to Ambient light sensor. + */ + void onKeyboardBacklightValueChanged(int value); + } + + private static class BrightnessStep { + private final int mBrightnessValue; + private final int mIncreaseLuxThreshold; + private final int mDecreaseLuxThreshold; + + private BrightnessStep(int brightnessValue, int increaseLuxThreshold, + int decreaseLuxThreshold) { + mBrightnessValue = brightnessValue; + mIncreaseLuxThreshold = increaseLuxThreshold; + mDecreaseLuxThreshold = decreaseLuxThreshold; + } + + @Override + public String toString() { + return "BrightnessStep{" + "mBrightnessValue=" + mBrightnessValue + + ", mIncreaseThreshold=" + mIncreaseLuxThreshold + ", mDecreaseThreshold=" + + mDecreaseLuxThreshold + '}'; + } + } +} diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java index 7c7f1513bd96..a646d1e9bcb0 100644 --- a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java +++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java @@ -39,14 +39,21 @@ public final class InputFeatureFlagProvider { InputProperties.enable_keyboard_backlight_animation().orElse(false); // To disable Custom keyboard backlight levels support via IDC files run: - // adb shell setprop persist.input.keyboard_backlight_custom_levels.enabled false (requires + // adb shell setprop persist.input.keyboard.backlight_custom_levels.enabled false (requires // restart) private static final boolean KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED = InputProperties.enable_keyboard_backlight_custom_levels().orElse(true); + // To disable als based ambient keyboard backlight control run: + // adb shell setprop persist.input.keyboard.ambient_backlight_control.enabled false (requires + // restart) + private static final boolean AMBIENT_KEYBOARD_BACKLIGHT_CONTROL_ENABLED = + InputProperties.enable_ambient_keyboard_backlight_control().orElse(true); + private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); private static Optional<Boolean> sKeyboardBacklightCustomLevelsOverride = Optional.empty(); + private static Optional<Boolean> sAmbientKeyboardBacklightControlOverride = Optional.empty(); public static boolean isKeyboardBacklightControlEnabled() { return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); @@ -61,6 +68,11 @@ public final class InputFeatureFlagProvider { KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED); } + public static boolean isAmbientKeyboardBacklightControlEnabled() { + return sAmbientKeyboardBacklightControlOverride.orElse( + AMBIENT_KEYBOARD_BACKLIGHT_CONTROL_ENABLED); + } + public static void setKeyboardBacklightControlEnabled(boolean enabled) { sKeyboardBacklightControlOverride = Optional.of(enabled); } @@ -73,6 +85,10 @@ public final class InputFeatureFlagProvider { sKeyboardBacklightCustomLevelsOverride = Optional.of(enabled); } + public static void setAmbientKeyboardBacklightControlEnabled(boolean enabled) { + sAmbientKeyboardBacklightControlOverride = Optional.of(enabled); + } + /** * Clears all input feature flag overrides. */ @@ -80,5 +96,6 @@ public final class InputFeatureFlagProvider { sKeyboardBacklightControlOverride = Optional.empty(); sKeyboardBacklightAnimationOverride = Optional.empty(); sKeyboardBacklightCustomLevelsOverride = Optional.empty(); + sAmbientKeyboardBacklightControlOverride = Optional.empty(); } } diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 36238a8cfd23..1253b5b5b303 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -18,6 +18,7 @@ package com.android.server.input; import android.animation.ValueAnimator; import android.annotation.BinderThread; +import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; import android.hardware.input.IKeyboardBacklightListener; @@ -105,6 +106,12 @@ final class KeyboardBacklightController implements private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords = new SparseArray<>(); + private final AmbientKeyboardBacklightController mAmbientController; + @Nullable + private AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener mAmbientListener; + + private int mAmbientBacklightValue = 0; + static { // Fixed brightness levels to avoid issues when converting back and forth from the // device brightness range to [0-255] @@ -128,6 +135,7 @@ final class KeyboardBacklightController implements mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); mAnimatorFactory = animatorFactory; + mAmbientController = new AmbientKeyboardBacklightController(context, looper); } @Override @@ -151,6 +159,11 @@ final class KeyboardBacklightController implements } }; observer.startObserving(UEVENT_KEYBOARD_BACKLIGHT_TAG); + + if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { + // Start ambient backlight controller + mAmbientController.systemRunning(); + } } @Override @@ -183,9 +196,21 @@ final class KeyboardBacklightController implements if (inputDevice == null || state == null) { return; } - Light keyboardBacklight = state.mLight; // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS - final int currBrightnessLevel = state.mBrightnessLevel; + final int currBrightnessLevel; + if (state.mUseAmbientController) { + int index = Arrays.binarySearch(state.mBrightnessValueForLevel, mAmbientBacklightValue); + // Set current level to the lower bound of the ambient value in the brightness array. + if (index < 0) { + int lowerBound = Math.max(0, -(index + 1) - 1); + currBrightnessLevel = + direction == Direction.DIRECTION_UP ? lowerBound : lowerBound + 1; + } else { + currBrightnessLevel = index; + } + } else { + currBrightnessLevel = state.mBrightnessLevel; + } final int newBrightnessLevel; if (direction == Direction.DIRECTION_UP) { newBrightnessLevel = Math.min(currBrightnessLevel + 1, @@ -193,33 +218,67 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } - updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); + state.setBrightnessLevel(newBrightnessLevel); + + // Might need to stop listening to ALS since user has manually selected backlight + // level through keyboard up/down button + updateAmbientLightListener(); + + maybeBackupBacklightBrightness(inputDevice, state.mLight, + state.mBrightnessValueForLevel[newBrightnessLevel]); + + if (DEBUG) { + Slog.d(TAG, + "Changing state from " + state.mBrightnessLevel + " to " + newBrightnessLevel); + } + + synchronized (mKeyboardBacklightListenerRecords) { + for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { + IKeyboardBacklightState callbackState = new IKeyboardBacklightState(); + callbackState.brightnessLevel = newBrightnessLevel; + callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps(); + mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged( + deviceId, callbackState, true); + } + } + } + + private void maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight, + int brightnessValue) { + // Don't back up or restore when ALS based keyboard backlight is enabled + if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { + return; + } synchronized (mDataStore) { try { mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(), keyboardBacklight.getId(), - state.mBrightnessValueForLevel[newBrightnessLevel]); + brightnessValue); } finally { mDataStore.saveIfNeeded(); } } } - private void restoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) { + private void maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) { + // Don't back up or restore when ALS based keyboard backlight is enabled + if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { + return; + } KeyboardBacklightState state = mKeyboardBacklights.get(inputDevice.getId()); OptionalInt brightness; synchronized (mDataStore) { brightness = mDataStore.getKeyboardBacklightBrightness( inputDevice.getDescriptor(), keyboardBacklight.getId()); } - if (brightness.isPresent()) { + if (state != null && brightness.isPresent()) { int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt())); - int index = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue); - if (index < 0) { - index = Math.min(state.getNumBrightnessChangeSteps(), -(index + 1)); + int newLevel = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue); + if (newLevel < 0) { + newLevel = Math.min(state.getNumBrightnessChangeSteps(), -(newLevel + 1)); } - updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); + state.setBrightnessLevel(newLevel); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } @@ -260,6 +319,16 @@ final class KeyboardBacklightController implements } else { handleUserInactivity(); } + updateAmbientLightListener(); + } + + @VisibleForTesting + public void handleAmbientLightValueChanged(int brightnessValue) { + mAmbientBacklightValue = brightnessValue; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onAmbientBacklightValueChanged(); + } } private boolean handleMessage(Message msg) { @@ -292,12 +361,14 @@ final class KeyboardBacklightController implements @Override public void onInputDeviceAdded(int deviceId) { onInputDeviceChanged(deviceId); + updateAmbientLightListener(); } @VisibleForTesting @Override public void onInputDeviceRemoved(int deviceId) { mKeyboardBacklights.remove(deviceId); + updateAmbientLightListener(); } @VisibleForTesting @@ -318,7 +389,7 @@ final class KeyboardBacklightController implements } // The keyboard backlight was added or changed. mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); - restoreBacklightBrightness(inputDevice, keyboardBacklight); + maybeRestoreBacklightBrightness(inputDevice, keyboardBacklight); } private InputDevice getInputDevice(int deviceId) { @@ -379,30 +450,6 @@ final class KeyboardBacklightController implements } } - private void updateBacklightState(int deviceId, int brightnessLevel, - boolean isTriggeredByKeyPress) { - KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); - if (state == null) { - return; - } - - state.setBrightnessLevel(brightnessLevel); - - synchronized (mKeyboardBacklightListenerRecords) { - for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { - IKeyboardBacklightState callbackState = new IKeyboardBacklightState(); - callbackState.brightnessLevel = brightnessLevel; - callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps(); - mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged( - deviceId, callbackState, isTriggeredByKeyPress); - } - } - - if (DEBUG) { - Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); - } - } - private void onKeyboardBacklightListenerDied(int pid) { synchronized (mKeyboardBacklightListenerRecords) { mKeyboardBacklightListenerRecords.remove(pid); @@ -420,6 +467,25 @@ final class KeyboardBacklightController implements } } + private void updateAmbientLightListener() { + if (!InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { + return; + } + boolean needToListenAmbientLightSensor = false; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + needToListenAmbientLightSensor |= mKeyboardBacklights.valueAt(i).mUseAmbientController; + } + needToListenAmbientLightSensor &= mIsInteractive; + if (needToListenAmbientLightSensor && mAmbientListener == null) { + mAmbientListener = this::handleAmbientLightValueChanged; + mAmbientController.registerAmbientBacklightListener(mAmbientListener); + } + if (!needToListenAmbientLightSensor && mAmbientListener != null) { + mAmbientController.unregisterAmbientBacklightListener(mAmbientListener); + mAmbientListener = null; + } + } + private static boolean isValidBacklightNodePath(String devPath) { if (TextUtils.isEmpty(devPath)) { return false; @@ -485,6 +551,8 @@ final class KeyboardBacklightController implements private int mBrightnessLevel; private ValueAnimator mAnimator; private final int[] mBrightnessValueForLevel; + private boolean mUseAmbientController = + InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled(); KeyboardBacklightState(int deviceId, Light light) { mDeviceId = deviceId; @@ -525,15 +593,25 @@ final class KeyboardBacklightController implements } private void onBacklightStateChanged() { - setBacklightValue(mIsBacklightOn ? mBrightnessValueForLevel[mBrightnessLevel] : 0); + int toValue = mUseAmbientController ? mAmbientBacklightValue + : mBrightnessValueForLevel[mBrightnessLevel]; + setBacklightValue(mIsBacklightOn ? toValue : 0); } private void setBrightnessLevel(int brightnessLevel) { + // Once we manually set level, disregard ambient light controller + mUseAmbientController = false; if (mIsBacklightOn) { setBacklightValue(mBrightnessValueForLevel[brightnessLevel]); } mBrightnessLevel = brightnessLevel; } + private void onAmbientBacklightValueChanged() { + if (mIsBacklightOn && mUseAmbientController) { + setBacklightValue(mAmbientBacklightValue); + } + } + private void cancelAnimation() { if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index 44ae454e7ef2..c212e8e3c82c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -19,6 +19,7 @@ package com.android.server.inputmethod; import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; +import android.annotation.Nullable; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -65,8 +66,8 @@ final class InputMethodMenuController { private boolean mShowImeWithHardKeyboard; @GuardedBy("ImfLock.class") - private final InputMethodDialogWindowContext mDialogWindowContext = - new InputMethodDialogWindowContext(); + @Nullable + private InputMethodDialogWindowContext mDialogWindowContext; InputMethodMenuController(InputMethodManagerService service) { mService = service; @@ -124,11 +125,13 @@ final class InputMethodMenuController { } } + if (mDialogWindowContext == null) { + mDialogWindowContext = new InputMethodDialogWindowContext(); + } final Context dialogWindowContext = mDialogWindowContext.get(displayId); mDialogBuilder = new AlertDialog.Builder(dialogWindowContext); mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu()); - // TODO(b/277061090): refactor UI components should not be created while holding a lock. final Context dialogContext = mDialogBuilder.getContext(); final TypedArray a = dialogContext.obtainStyledAttributes(null, com.android.internal.R.styleable.DialogPreference, @@ -196,11 +199,10 @@ final class InputMethodMenuController { attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; attrs.setTitle("Select input method"); w.setAttributes(attrs); - // TODO(b/277062834) decouple/remove dependency on IMMS mService.updateSystemUiLocked(); mService.sendOnNavButtonFlagsChangedLocked(); + mSwitchingDialog.show(); } - mSwitchingDialog.show(); } private boolean isScreenLocked() { @@ -274,7 +276,6 @@ final class InputMethodMenuController { private final int mTextViewResourceId; private final List<ImeSubtypeListItem> mItemsList; public int mCheckedItem; - private ImeSubtypeListAdapter(Context context, int textViewResourceId, List<ImeSubtypeListItem> itemsList, int checkedItem) { super(context, textViewResourceId, itemsList); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 8e65943cf297..e72fcdfa8283 100755..100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -119,6 +119,7 @@ import static android.service.notification.NotificationListenerService.TRIM_LIGH import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ALLOW_DISMISS_ONGOING; +import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.WAKE_LOCK_FOR_POSTING_NOTIFICATION; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES; @@ -223,6 +224,8 @@ import android.os.IInterface; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; @@ -234,6 +237,7 @@ import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.os.VibrationEffect; +import android.os.WorkSource; import android.permission.PermissionManager; import android.provider.DeviceConfig; import android.provider.Settings; @@ -559,6 +563,7 @@ public class NotificationManagerService extends SystemService { private PermissionHelper mPermissionHelper; private UsageStatsManagerInternal mUsageStatsManagerInternal; private TelecomManager mTelecomManager; + private PowerManager mPowerManager; private PostNotificationTrackerFactory mPostNotificationTrackerFactory; final IBinder mForegroundToken = new Binder(); @@ -923,7 +928,7 @@ public class NotificationManagerService extends SystemService { if (oldFlags != flags) { summary.getSbn().getNotification().flags = flags; mHandler.post(new EnqueueNotificationRunnable(userId, summary, isAppForeground, - mPostNotificationTrackerFactory.newTracker())); + mPostNotificationTrackerFactory.newTracker(null))); } } @@ -1457,7 +1462,7 @@ public class NotificationManagerService extends SystemService { // want to adjust the flag behaviour. mHandler.post(new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r, true /* isAppForeground*/, - mPostNotificationTrackerFactory.newTracker())); + mPostNotificationTrackerFactory.newTracker(null))); } } } @@ -1488,7 +1493,7 @@ public class NotificationManagerService extends SystemService { mHandler.post( new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r, /* foreground= */ true, - mPostNotificationTrackerFactory.newTracker())); + mPostNotificationTrackerFactory.newTracker(null))); } } } @@ -1843,7 +1848,7 @@ public class NotificationManagerService extends SystemService { } } else if (action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - if (userHandle >= 0) { + if (userHandle >= 0 && !mDpm.isKeepProfilesRunningEnabled()) { cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, true, userHandle, REASON_PROFILE_TURNED_OFF, null); mSnoozeHelper.clearData(userHandle); @@ -2233,7 +2238,7 @@ public class NotificationManagerService extends SystemService { UsageStatsManagerInternal usageStatsManagerInternal, TelecomManager telecomManager, NotificationChannelLogger channelLogger, SystemUiSystemPropertiesFlags.FlagResolver flagResolver, - PermissionManager permissionManager, + PermissionManager permissionManager, PowerManager powerManager, PostNotificationTrackerFactory postNotificationTrackerFactory) { mHandler = handler; Resources resources = getContext().getResources(); @@ -2265,6 +2270,7 @@ public class NotificationManagerService extends SystemService { mDpm = dpm; mUm = userManager; mTelecomManager = telecomManager; + mPowerManager = powerManager; mPostNotificationTrackerFactory = postNotificationTrackerFactory; mPlatformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); @@ -2568,6 +2574,7 @@ public class NotificationManagerService extends SystemService { getContext().getSystemService(TelecomManager.class), new NotificationChannelLoggerImpl(), SystemUiSystemPropertiesFlags.getResolver(), getContext().getSystemService(PermissionManager.class), + getContext().getSystemService(PowerManager.class), new PostNotificationTrackerFactory() {}); publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false, @@ -2684,7 +2691,7 @@ public class NotificationManagerService extends SystemService { final boolean isAppForeground = mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground, - mPostNotificationTrackerFactory.newTracker())); + mPostNotificationTrackerFactory.newTracker(null))); } } @@ -6574,7 +6581,7 @@ public class NotificationManagerService extends SystemService { void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int incomingUserId, boolean postSilently) { - PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(); + PostNotificationTracker tracker = acquireWakeLockForPost(pkg, callingUid); boolean enqueued = false; try { enqueued = enqueueNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, @@ -6586,6 +6593,22 @@ public class NotificationManagerService extends SystemService { } } + private PostNotificationTracker acquireWakeLockForPost(String pkg, int uid) { + if (mFlagResolver.isEnabled(WAKE_LOCK_FOR_POSTING_NOTIFICATION)) { + // The package probably doesn't have WAKE_LOCK permission and should not require it. + return Binder.withCleanCallingIdentity(() -> { + WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "NotificationManagerService:post:" + pkg); + wakeLock.setWorkSource(new WorkSource(uid, pkg)); + // TODO(b/275044361): Adjust to a more reasonable number when we have the data. + wakeLock.acquire(30_000); + return mPostNotificationTrackerFactory.newTracker(wakeLock); + }); + } else { + return mPostNotificationTrackerFactory.newTracker(null); + } + } + /** * @return True if we successfully processed the notification and handed off the task of * enqueueing it to a background thread; false otherwise. @@ -7105,7 +7128,7 @@ public class NotificationManagerService extends SystemService { mHandler.post( new NotificationManagerService.EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, isAppForeground, - mPostNotificationTrackerFactory.newTracker())); + mPostNotificationTrackerFactory.newTracker(null))); } } } @@ -12137,20 +12160,20 @@ public class NotificationManagerService extends SystemService { } interface PostNotificationTrackerFactory { - default PostNotificationTracker newTracker() { - return new PostNotificationTracker(); + default PostNotificationTracker newTracker(@Nullable WakeLock optionalWakelock) { + return new PostNotificationTracker(optionalWakelock); } } static class PostNotificationTracker { @ElapsedRealtimeLong private final long mStartTime; - @Nullable private NotificationRecordLogger.NotificationReported mReport; + @Nullable private final WakeLock mWakeLock; private boolean mOngoing; @VisibleForTesting - PostNotificationTracker() { - // TODO(b/275044361): (Conditionally) receive a wakelock. + PostNotificationTracker(@Nullable WakeLock wakeLock) { mStartTime = SystemClock.elapsedRealtime(); + mWakeLock = wakeLock; mOngoing = true; if (DBG) { Slog.d(TAG, "PostNotification: Started"); @@ -12168,9 +12191,8 @@ public class NotificationManagerService extends SystemService { } /** - * Cancels the tracker (TODO(b/275044361): releasing the acquired WakeLock). Either - * {@link #finish} or {@link #cancel} (exclusively) should be called on this object before - * it's discarded. + * Cancels the tracker (releasing the acquired WakeLock). Either {@link #finish} or + * {@link #cancel} (exclusively) should be called on this object before it's discarded. */ void cancel() { if (!isOngoing()) { @@ -12178,9 +12200,9 @@ public class NotificationManagerService extends SystemService { return; } mOngoing = false; - - // TODO(b/275044361): Release wakelock. - + if (mWakeLock != null) { + Binder.withCleanCallingIdentity(() -> mWakeLock.release()); + } if (DBG) { long elapsedTime = SystemClock.elapsedRealtime() - mStartTime; Slog.d(TAG, TextUtils.formatSimple("PostNotification: Abandoned after %d ms", @@ -12189,9 +12211,9 @@ public class NotificationManagerService extends SystemService { } /** - * Finishes the tracker (TODO(b/275044361): releasing the acquired WakeLock) and returns the - * time elapsed since the operation started, in milliseconds. Either {@link #finish} or - * {@link #cancel} (exclusively) should be called on this object before it's discarded. + * Finishes the tracker (releasing the acquired WakeLock) and returns the time elapsed since + * the operation started, in milliseconds. Either {@link #finish} or {@link #cancel} + * (exclusively) should be called on this object before it's discarded. */ @DurationMillisLong long finish() { @@ -12201,9 +12223,9 @@ public class NotificationManagerService extends SystemService { return elapsedTime; } mOngoing = false; - - // TODO(b/275044361): Release wakelock. - + if (mWakeLock != null) { + Binder.withCleanCallingIdentity(() -> mWakeLock.release()); + } if (DBG) { Slog.d(TAG, TextUtils.formatSimple("PostNotification: Finished in %d ms", elapsedTime)); diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index d3d1cc5d05cb..f8ee6b04e9a2 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -96,6 +96,8 @@ public final class NativeTombstoneManager { registerForUserRemoval(); registerForPackageRemoval(); + BootReceiver.initDropboxRateLimiter(); + // Scan existing tombstones. mHandler.post(() -> { final File[] tombstoneFiles = TOMBSTONE_DIR.listFiles(); diff --git a/services/core/java/com/android/server/pm/DumpHelper.java b/services/core/java/com/android/server/pm/DumpHelper.java index fcaaa90dbc8a..b5647d0092f5 100644 --- a/services/core/java/com/android/server/pm/DumpHelper.java +++ b/services/core/java/com/android/server/pm/DumpHelper.java @@ -617,7 +617,9 @@ final class DumpHelper { pw.println(" --checkin: dump for a checkin"); pw.println(" -f: print details of intent filters"); pw.println(" -h: print this help"); + pw.println(" ---proto: dump data to proto"); pw.println(" --all-components: include all component names in package dump"); + pw.println(" --include-apex: includes the apex packages in package dump"); pw.println(" cmd may be one of:"); pw.println(" apex: list active APEXes and APEX session state"); pw.println(" l[ibraries]: list known shared libraries"); @@ -631,7 +633,7 @@ final class DumpHelper { pw.println(" prov[iders]: dump content providers"); pw.println(" p[ackages]: dump installed packages"); pw.println(" q[ueries]: dump app queryability calculations"); - pw.println(" s[hared-users]: dump shared user IDs"); + pw.println(" s[hared-users] [noperm]: dump shared user IDs"); pw.println(" m[essages]: print collected runtime messages"); pw.println(" v[erifiers]: print package verifier info"); pw.println(" d[omain-preferred-apps]: print domains preferred apps"); @@ -644,9 +646,12 @@ final class DumpHelper { pw.println(" dexopt: dump dexopt state"); pw.println(" compiler-stats: dump compiler statistics"); pw.println(" service-permissions: dump permissions required by services"); - pw.println(" snapshot: dump snapshot statistics"); + pw.println(" snapshot [--full|--brief]: dump snapshot statistics"); pw.println(" protected-broadcasts: print list of protected broadcast actions"); pw.println(" known-packages: dump known packages"); + pw.println(" changes: dump the packages that have been changed"); + pw.println(" frozen: dump the frozen packages"); + pw.println(" volumes: dump the loaded volumes"); pw.println(" <package.name>: info about given package"); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4f00dc37caa6..fa7c063abbe7 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2145,7 +2145,7 @@ final class InstallPackageHelper { final String pkgName = pkg.getPackageName(); final int[] installedForUsers = installRequest.getOriginUsers(); final int installReason = installRequest.getInstallReason(); - final String installerPackageName = installRequest.getSourceInstallerPackageName(); + final String installerPackageName = installRequest.getInstallerPackageName(); if (DEBUG_INSTALL) Slog.d(TAG, "New package installed in " + pkg.getPath()); synchronized (mPm.mLock) { diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 95e790450724..34648740d54c 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -366,12 +366,6 @@ final class InstallRequest { public String getApexModuleName() { return mApexModuleName; } - - @Nullable - public String getSourceInstallerPackageName() { - return mInstallArgs.mInstallSource.mInstallerPackageName; - } - public boolean isRollback() { return mInstallArgs != null && mInstallArgs.mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index b90fe6165b8a..2c0e74d65e7f 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -511,7 +511,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } catch (FileNotFoundException e) { // Missing sessions are okay, probably first boot - } catch (IOException | XmlPullParserException e) { + } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) { Slog.wtf(TAG, "Failed reading install sessions", e); } finally { IoUtils.closeQuietly(fis); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 41234813ee67..c1709f8faead 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -3261,7 +3261,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Nullable - private String getDevicePolicyManagementRoleHolderPackageName(int userId) { + public String getDevicePolicyManagementRoleHolderPackageName(int userId) { return Binder.withCleanCallingIdentity(() -> { RoleManager roleManager = mContext.getSystemService(RoleManager.class); List<String> roleHolders = @@ -4110,7 +4110,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService final int livingUserCount = livingUsers.size(); for (int i = 0; i < livingUserCount; i++) { final int userId = livingUsers.get(i).id; - if (mSettings.isPermissionUpgradeNeeded(userId)) { + final boolean isPermissionUpgradeNeeded = !Objects.equals( + mPermissionManager.getDefaultPermissionGrantFingerprint(userId), + Build.FINGERPRINT); + if (isPermissionUpgradeNeeded) { grantPermissionsUserIds = ArrayUtils.appendInt( grantPermissionsUserIds, userId); } @@ -4118,6 +4121,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService // If we upgraded grant all default permissions before kicking off. for (int userId : grantPermissionsUserIds) { mLegacyPermissionManager.grantDefaultPermissions(userId); + mPermissionManager.setDefaultPermissionGrantFingerprint(Build.FINGERPRINT, userId); } if (grantPermissionsUserIds == EMPTY_INT_ARRAY) { // If we did not grant default permissions, we preload from this the @@ -4286,6 +4290,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (!convertedFromPreCreated || !readPermissionStateForUser(userId)) { mPermissionManager.onUserCreated(userId); mLegacyPermissionManager.grantDefaultPermissions(userId); + mPermissionManager.setDefaultPermissionGrantFingerprint(Build.FINGERPRINT, userId); mDomainVerificationManager.clearUser(userId); } } @@ -4295,7 +4300,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPermissionManager.writeLegacyPermissionStateTEMP(); mSettings.readPermissionStateForUserSyncLPr(userId); mPermissionManager.readLegacyPermissionStateTEMP(); - return mSettings.isPermissionUpgradeNeeded(userId); + final boolean isPermissionUpgradeNeeded = !Objects.equals( + mPermissionManager.getDefaultPermissionGrantFingerprint(userId), + Build.FINGERPRINT); + return isPermissionUpgradeNeeded; } } diff --git a/services/core/java/com/android/server/pm/ResilientAtomicFile.java b/services/core/java/com/android/server/pm/ResilientAtomicFile.java index 19aa4f8e8d0b..54ca426a6dc3 100644 --- a/services/core/java/com/android/server/pm/ResilientAtomicFile.java +++ b/services/core/java/com/android/server/pm/ResilientAtomicFile.java @@ -230,7 +230,9 @@ final class ResilientAtomicFile implements Closeable { + Log.getStackTraceString(e)); } - mCurrentFile.delete(); + if (!mCurrentFile.delete()) { + throw new IllegalStateException("Failed to remove " + mCurrentFile); + } mCurrentFile = null; } diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index ba825774daf6..08934c69e099 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -724,6 +724,10 @@ public final class SuspendPackageHelper { for (PackageInfo info : pkgInfos) { result.add(info.packageName); } + + // Role holder may be null, but ArraySet handles it correctly. + result.remove(mPm.getDevicePolicyManagementRoleHolderPackageName(userId)); + return result; } diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index d108e1487564..d55f85cde5af 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -986,11 +986,14 @@ public class PackageInfoUtils { } /** @see ApplicationInfo#privateFlagsExt */ - public static int appInfoPrivateFlagsExt(int pkgWithoutStateFlags, + private static int appInfoPrivateFlagsExt(int pkgWithoutStateFlags, @Nullable PackageStateInternal pkgSetting) { // @formatter:off - // TODO: Add state specific flags - return pkgWithoutStateFlags; + int flags = pkgWithoutStateFlags; + if (pkgSetting != null) { + flags |= flag(pkgSetting.getCpuAbiOverride() != null, ApplicationInfo.PRIVATE_FLAG_EXT_CPU_OVERRIDE); + } + return flags; // @formatter:on } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 956bcf211ee2..30afb0bf8853 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -687,6 +687,18 @@ public class PermissionManagerService extends IPermissionManager.Stub { mPermissionManagerServiceImpl.writeLegacyPermissionsTEMP(legacyPermissionSettings); } + @Nullable + @Override + public String getDefaultPermissionGrantFingerprint(@UserIdInt int userId) { + return mPermissionManagerServiceImpl.getDefaultPermissionGrantFingerprint(userId); + } + + @Override + public void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, + @UserIdInt int userId) { + mPermissionManagerServiceImpl.setDefaultPermissionGrantFingerprint(fingerprint, userId); + } + @Override public void onPackageAdded(@NonNull PackageState packageState, boolean isInstantApp, @Nullable AndroidPackage oldPkg) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index b4e4ce0623a4..a299b56a191b 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -2833,9 +2833,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } else if (!permissionPolicyInitialized || (!hardRestricted || restrictionExempt)) { if ((origPermState != null && origPermState.isGranted())) { - if (!uidState.grantPermission(bp)) { - wasChanged = true; - } + uidState.grantPermission(bp); } } if (mIsLeanback && NOTIFICATION_PERMISSIONS.contains(permName)) { @@ -4599,6 +4597,19 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } } + @Nullable + @Override + public String getDefaultPermissionGrantFingerprint(@UserIdInt int userId) { + return mPackageManagerInt.isPermissionUpgradeNeeded(userId) ? null : Build.FINGERPRINT; + } + + @Override + public void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, + @UserIdInt int userId) { + // Ignored - default permission grant here shares the same version with runtime permission + // upgrade, and the new version is set by that later. + } + private void onPackageAddedInternal(@NonNull PackageState packageState, @NonNull AndroidPackage pkg, boolean isInstantApp, @Nullable AndroidPackage oldPkg) { if (!pkg.getAdoptPermissions().isEmpty()) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java index 8d8df966dfab..128f847715ab 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java @@ -523,6 +523,17 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte void writeLegacyPermissionsTEMP(@NonNull LegacyPermissionSettings legacyPermissionSettings); /** + * Get the fingerprint for default permission grants. + */ + @Nullable + String getDefaultPermissionGrantFingerprint(@UserIdInt int userId); + + /** + * Set the fingerprint for default permission grants. + */ + void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, @UserIdInt int userId); + + /** * Callback when the system is ready. */ void onSystemReady(); @@ -603,6 +614,6 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte * @param userId the user ID the package is uninstalled for */ void onPackageUninstalled(@NonNull String packageName, int appId, - @NonNull PackageState packageState, @NonNull AndroidPackage pkg, + @NonNull PackageState packageState, @Nullable AndroidPackage pkg, @NonNull List<AndroidPackage> sharedUserPkgs, @UserIdInt int userId); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java index 240a73a4d31a..cf2b69cbc512 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java @@ -215,6 +215,17 @@ public interface PermissionManagerServiceInternal extends PermissionManagerInter void writeLegacyPermissionsTEMP(@NonNull LegacyPermissionSettings legacyPermissionSettings); /** + * Get the fingerprint for default permission grants. + */ + @Nullable + String getDefaultPermissionGrantFingerprint(@UserIdInt int userId); + + /** + * Set the fingerprint for default permission grants. + */ + void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, @UserIdInt int userId); + + /** * Callback when the system is ready. */ //@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java index 83ddde5245ce..7f98e2163178 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java @@ -356,6 +356,20 @@ public class PermissionManagerServiceLoggingDecorator implements PermissionManag mService.writeLegacyPermissionsTEMP(legacyPermissionSettings); } + @Nullable + @Override + public String getDefaultPermissionGrantFingerprint(int userId) { + Log.i(LOG_TAG, "getDefaultPermissionGrantFingerprint(userId = " + userId + ")"); + return mService.getDefaultPermissionGrantFingerprint(userId); + } + + @Override + public void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, int userId) { + Log.i(LOG_TAG, "setDefaultPermissionGrantFingerprint(fingerprint = " + fingerprint + + ", userId = " + userId + ")"); + mService.setDefaultPermissionGrantFingerprint(fingerprint, userId); + } + @Override public void onSystemReady() { Log.i(LOG_TAG, "onSystemReady()"); @@ -412,7 +426,7 @@ public class PermissionManagerServiceLoggingDecorator implements PermissionManag @Override public void onPackageUninstalled(@NonNull String packageName, int appId, - @NonNull PackageState packageState, @NonNull AndroidPackage pkg, + @NonNull PackageState packageState, @Nullable AndroidPackage pkg, @NonNull List<AndroidPackage> sharedUserPkgs, int userId) { Log.i(LOG_TAG, "onPackageUninstalled(packageName = " + packageName + ", appId = " + appId + ", packageState = " + packageState + ", pkg = " + pkg + ", sharedUserPkgs = " diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java index 317fbe77ba6a..d4c6d42deeaa 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java @@ -22,6 +22,7 @@ import android.annotation.UserIdInt; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.permission.SplitPermissionInfoParcelable; +import android.os.Build; import android.permission.IOnPermissionsChangeListener; import com.android.server.pm.pkg.AndroidPackage; @@ -483,6 +484,26 @@ public class PermissionManagerServiceTestingShim implements PermissionManagerSer mNewImplementation.writeLegacyPermissionsTEMP(legacyPermissionSettings); } + @Nullable + @Override + public String getDefaultPermissionGrantFingerprint(@UserIdInt int userId) { + String oldVal = mOldImplementation.getDefaultPermissionGrantFingerprint(userId); + String newVal = mNewImplementation.getDefaultPermissionGrantFingerprint(userId); + + if (Objects.equals(oldVal, Build.FINGERPRINT) + != Objects.equals(newVal, Build.FINGERPRINT)) { + signalImplDifference("getDefaultPermissionGrantFingerprint"); + } + return newVal; + } + + @Override + public void setDefaultPermissionGrantFingerprint(@NonNull String fingerprint, + @UserIdInt int userId) { + mOldImplementation.setDefaultPermissionGrantFingerprint(fingerprint, userId); + mNewImplementation.setDefaultPermissionGrantFingerprint(fingerprint, userId); + } + @Override public void onSystemReady() { mOldImplementation.onSystemReady(); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 9ff5431bbdf9..f86d68a1cd9c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5933,7 +5933,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case HapticFeedbackConstants.CONTEXT_CLICK: case HapticFeedbackConstants.GESTURE_END: case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE: - case HapticFeedbackConstants.ROTARY_SCROLL_TICK: + case HapticFeedbackConstants.SCROLL_TICK: case HapticFeedbackConstants.SEGMENT_TICK: return VibrationEffect.get(VibrationEffect.EFFECT_TICK); @@ -5958,8 +5958,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case HapticFeedbackConstants.CALENDAR_DATE: case HapticFeedbackConstants.CONFIRM: case HapticFeedbackConstants.GESTURE_START: - case HapticFeedbackConstants.ROTARY_SCROLL_ITEM_FOCUS: - case HapticFeedbackConstants.ROTARY_SCROLL_LIMIT: + case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: + case HapticFeedbackConstants.SCROLL_LIMIT: return VibrationEffect.get(VibrationEffect.EFFECT_CLICK); case HapticFeedbackConstants.LONG_PRESS: @@ -6037,9 +6037,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { return PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES; case HapticFeedbackConstants.ASSISTANT_BUTTON: case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: - case HapticFeedbackConstants.ROTARY_SCROLL_TICK: - case HapticFeedbackConstants.ROTARY_SCROLL_ITEM_FOCUS: - case HapticFeedbackConstants.ROTARY_SCROLL_LIMIT: + case HapticFeedbackConstants.SCROLL_TICK: + case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: + case HapticFeedbackConstants.SCROLL_LIMIT: return HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES; default: return TOUCH_VIBRATION_ATTRIBUTES; diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index d0fb25a5f773..3caeeaeedd1b 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -40,13 +40,17 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManager; -import android.app.AppOpsManager; import android.app.SynchronousUserSwitchObserver; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.PermissionChecker; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; @@ -62,6 +66,7 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.BatterySaverPolicyConfig; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; @@ -277,6 +282,18 @@ public final class PowerManagerService extends SystemService */ private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L; + /** + * Apps targeting Android U and above need to define + * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for + * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect. + * Note that most applications should use {@link android.R.attr#turnScreenOn} or + * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the + * previous foreground app from being resumed first when the screen turns on. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L; + /** Reason ID for holding display suspend blocker. */ private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display"; @@ -304,8 +321,9 @@ public final class PowerManagerService extends SystemService private final SystemPropertiesWrapper mSystemProperties; private final Clock mClock; private final Injector mInjector; + private final PermissionCheckerWrapper mPermissionCheckerWrapper; + private final PowerPropertiesWrapper mPowerPropertiesWrapper; - private AppOpsManager mAppOpsManager; private LightsManager mLightsManager; private BatteryManagerInternal mBatteryManagerInternal; private DisplayManagerInternal mDisplayManagerInternal; @@ -1029,11 +1047,66 @@ public final class PowerManagerService extends SystemService return new LowPowerStandbyController(context, looper); } - AppOpsManager createAppOpsManager(Context context) { - return context.getSystemService(AppOpsManager.class); + PermissionCheckerWrapper createPermissionCheckerWrapper() { + return PermissionChecker::checkPermissionForDataDelivery; + } + + PowerPropertiesWrapper createPowerPropertiesWrapper() { + return new PowerPropertiesWrapper() { + @Override + public boolean waive_target_sdk_check_for_turn_screen_on() { + return PowerProperties.waive_target_sdk_check_for_turn_screen_on().orElse( + false); + } + + @Override + public boolean permissionless_turn_screen_on() { + return PowerProperties.permissionless_turn_screen_on().orElse(false); + } + }; } } + /** Interface for checking an app op permission */ + @VisibleForTesting + interface PermissionCheckerWrapper { + /** + * Checks whether a given data access chain described by the given {@link AttributionSource} + * has a given permission and whether the app op that corresponds to this permission + * is allowed. + * See {@link PermissionChecker#checkPermissionForDataDelivery} for more details. + * + * @param context Context for accessing resources. + * @param permission The permission to check. + * @param pid The process id for which to check. Use {@link PermissionChecker#PID_UNKNOWN} + * if the PID is not known. + * @param attributionSource the permission identity + * @param message A message describing the reason the permission was checked + * @return The permission check result which is any of + * {@link PermissionChecker#PERMISSION_GRANTED}, + * {@link PermissionChecker#PERMISSION_SOFT_DENIED}, + * or {@link PermissionChecker#PERMISSION_HARD_DENIED}. + */ + int checkPermissionForDataDelivery(@NonNull Context context, @NonNull String permission, + int pid, @NonNull AttributionSource attributionSource, @Nullable String message); + } + + /** Interface for querying {@link PowerProperties} */ + @VisibleForTesting + interface PowerPropertiesWrapper { + /** + * Waives the minimum target-sdk check for android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP + * and only allows the flag for apps holding android.permission.TURN_SCREEN_ON + */ + boolean waive_target_sdk_check_for_turn_screen_on(); + + /** + * Allows apps to turn the screen on with android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP + * without being granted android.app.AppOpsManager#OP_TURN_SCREEN_ON. + */ + boolean permissionless_turn_screen_on(); + } + final Constants mConstants; private native void nativeInit(); @@ -1086,8 +1159,8 @@ public final class PowerManagerService extends SystemService Looper.getMainLooper()); mInattentiveSleepWarningOverlayController = mInjector.createInattentiveSleepWarningController(); - - mAppOpsManager = injector.createAppOpsManager(mContext); + mPermissionCheckerWrapper = mInjector.createPermissionCheckerWrapper(); + mPowerPropertiesWrapper = mInjector.createPowerPropertiesWrapper(); mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener(); @@ -1562,19 +1635,29 @@ public final class PowerManagerService extends SystemService } @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) - private boolean isAcquireCausesWakeupFlagAllowed(String opPackageName, int opUid) { + private boolean isAcquireCausesWakeupFlagAllowed(String opPackageName, int opUid, int opPid) { if (opPackageName == null) { return false; } - if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName) - == AppOpsManager.MODE_ALLOWED) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON) - == PackageManager.PERMISSION_GRANTED) { - Slog.i(TAG, "Allowing device wake-up from app " + opPackageName); - return true; - } + if (PermissionChecker.PERMISSION_GRANTED + == mPermissionCheckerWrapper.checkPermissionForDataDelivery(mContext, + android.Manifest.permission.TURN_SCREEN_ON, opPid, + new AttributionSource(opUid, opPackageName, /* attributionTag= */ null), + /* message= */ "ACQUIRE_CAUSES_WAKEUP for " + opPackageName)) { + Slog.i(TAG, "Allowing device wake-up from app " + opPackageName); + return true; + } + // CompatChanges#isChangeEnabled() returns false for apps with targetSdk < UDC and ensures + // backwards compatibility. + // waive_target_sdk_check_for_turn_screen_on() returns false by default and may be set to + // true on form factors with a more strict policy (e.g. TV) + if (!CompatChanges.isChangeEnabled(REQUIRE_TURN_SCREEN_ON_PERMISSION, opUid) + && !mPowerPropertiesWrapper.waive_target_sdk_check_for_turn_screen_on()) { + Slog.i(TAG, "Allowing device wake-up without android.permission.TURN_SCREEN_ON for " + + opPackageName); + return true; } - if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { + if (mPowerPropertiesWrapper.permissionless_turn_screen_on()) { Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on"); return true; } @@ -1589,6 +1672,7 @@ public final class PowerManagerService extends SystemService && isScreenLock(wakeLock)) { String opPackageName; int opUid; + int opPid = PermissionChecker.PID_UNKNOWN; if (wakeLock.mWorkSource != null && !wakeLock.mWorkSource.isEmpty()) { WorkSource workSource = wakeLock.mWorkSource; WorkChain workChain = getFirstNonEmptyWorkChain(workSource); @@ -1603,10 +1687,12 @@ public final class PowerManagerService extends SystemService } else { opPackageName = wakeLock.mPackageName; opUid = wakeLock.mOwnerUid; + opPid = wakeLock.mOwnerPid; } Integer powerGroupId = wakeLock.getPowerGroupId(); // powerGroupId is null if the wakelock associated display is no longer available - if (powerGroupId != null && isAcquireCausesWakeupFlagAllowed(opPackageName, opUid)) { + if (powerGroupId != null + && isAcquireCausesWakeupFlagAllowed(opPackageName, opUid, opPid)) { if (powerGroupId == Display.INVALID_DISPLAY_GROUP) { // wake up all display groups if (DEBUG_SPEW) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index d0088457a9f2..6e43690ad080 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -45,6 +45,7 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.AppOpsManager; +import android.app.ApplicationExitInfo; import android.app.ILocalWallpaperColorConsumer; import android.app.IWallpaperManager; import android.app.IWallpaperManagerCallback; @@ -187,6 +188,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub /** Tracks wallpaper being migrated from system+lock to lock when setting static wp. */ WallpaperDestinationChangeHandler mPendingMigrationViaStatic; + private static final double LMK_LOW_THRESHOLD_MEMORY_PERCENTAGE = 10; + private static final int LMK_RECONNECT_REBIND_RETRIES = 3; + private static final long LMK_RECONNECT_DELAY_MS = 5000; + /** * Minimum time between crashes of a wallpaper service for us to consider * restarting it vs. just reverting to the static wallpaper. @@ -988,6 +993,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub /** Time in milliseconds until we expect the wallpaper to reconnect (unless we're in the * middle of an update). If exceeded, the wallpaper gets reset to the system default. */ private static final long WALLPAPER_RECONNECT_TIMEOUT_MS = 10000; + private int mLmkLimitRebindRetries = LMK_RECONNECT_REBIND_RETRIES; final WallpaperInfo mInfo; IWallpaperService mService; @@ -1209,20 +1215,51 @@ public class WallpaperManagerService extends IWallpaperManager.Stub && mWallpaper.userId == mCurrentUserId && !Objects.equals(mDefaultWallpaperComponent, wpService) && !Objects.equals(mImageWallpaper, wpService)) { - // There is a race condition which causes - // {@link #mWallpaper.wallpaperUpdating} to be false even if it is - // currently updating since the broadcast notifying us is async. - // This race is overcome by the general rule that we only reset the - // wallpaper if its service was shut down twice - // during {@link #MIN_WALLPAPER_CRASH_TIME} millis. - if (mWallpaper.lastDiedTime != 0 - && mWallpaper.lastDiedTime + MIN_WALLPAPER_CRASH_TIME - > SystemClock.uptimeMillis()) { - Slog.w(TAG, "Reverting to built-in wallpaper!"); - clearWallpaperLocked(true, FLAG_SYSTEM, mWallpaper.userId, null); + List<ApplicationExitInfo> reasonList = + mActivityManager.getHistoricalProcessExitReasons( + wpService.getPackageName(), 0, 1); + int exitReason = ApplicationExitInfo.REASON_UNKNOWN; + if (reasonList != null && !reasonList.isEmpty()) { + ApplicationExitInfo info = reasonList.get(0); + exitReason = info.getReason(); + } + Slog.d(TAG, "exitReason: " + exitReason); + // If exit reason is LOW_MEMORY_KILLER + // delay the mTryToRebindRunnable for 10s + if (exitReason == ApplicationExitInfo.REASON_LOW_MEMORY) { + if (isRunningOnLowMemory()) { + Slog.i(TAG, "Rebind is delayed due to lmk"); + mContext.getMainThreadHandler().postDelayed(mTryToRebindRunnable, + LMK_RECONNECT_DELAY_MS); + mLmkLimitRebindRetries = LMK_RECONNECT_REBIND_RETRIES; + } else { + if (mLmkLimitRebindRetries <= 0) { + Slog.w(TAG, "Reverting to built-in wallpaper due to lmk!"); + clearWallpaperLocked(true, FLAG_SYSTEM, mWallpaper.userId, + null); + mLmkLimitRebindRetries = LMK_RECONNECT_REBIND_RETRIES; + return; + } + mLmkLimitRebindRetries--; + mContext.getMainThreadHandler().postDelayed(mTryToRebindRunnable, + LMK_RECONNECT_DELAY_MS); + } } else { - mWallpaper.lastDiedTime = SystemClock.uptimeMillis(); - tryToRebind(); + // There is a race condition which causes + // {@link #mWallpaper.wallpaperUpdating} to be false even if it is + // currently updating since the broadcast notifying us is async. + // This race is overcome by the general rule that we only reset the + // wallpaper if its service was shut down twice + // during {@link #MIN_WALLPAPER_CRASH_TIME} millis. + if (mWallpaper.lastDiedTime != 0 + && mWallpaper.lastDiedTime + MIN_WALLPAPER_CRASH_TIME + > SystemClock.uptimeMillis()) { + Slog.w(TAG, "Reverting to built-in wallpaper!"); + clearWallpaperLocked(true, FLAG_SYSTEM, mWallpaper.userId, null); + } else { + mWallpaper.lastDiedTime = SystemClock.uptimeMillis(); + tryToRebind(); + } } } } else { @@ -1233,6 +1270,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } }; + private boolean isRunningOnLowMemory() { + ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); + mActivityManager.getMemoryInfo(memoryInfo); + double availableMBsInPercentage = memoryInfo.availMem / (double)memoryInfo.totalMem * + 100.0; + return availableMBsInPercentage < LMK_LOW_THRESHOLD_MEMORY_PERCENTAGE; + } + /** * Called by a live wallpaper if its colors have changed. * @param primaryColors representation of wallpaper primary colors @@ -1775,21 +1820,23 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (record.exists()) { Slog.w(TAG, "User:" + userID + ", wallpaper tyep = " + type + ", wallpaper fail detect!! reset to default wallpaper"); - clearWallpaperData(userID, type); + clearWallpaperBitmaps(userID, type); record.delete(); } }); } - private void clearWallpaperData(int userID, int wallpaperType) { + private void clearWallpaperBitmaps(int userID, int wallpaperType) { final WallpaperData wallpaper = new WallpaperData(userID, wallpaperType); - if (wallpaper.sourceExists()) { - wallpaper.wallpaperFile.delete(); - } - if (wallpaper.cropExists()) { - wallpaper.cropFile.delete(); - } + clearWallpaperBitmaps(wallpaper); + } + private boolean clearWallpaperBitmaps(WallpaperData wallpaper) { + boolean sourceExists = wallpaper.sourceExists(); + boolean cropExists = wallpaper.cropExists(); + if (sourceExists) wallpaper.wallpaperFile.delete(); + if (cropExists) wallpaper.cropFile.delete(); + return sourceExists || cropExists; } @Override @@ -1966,10 +2013,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // files from the previous static wallpaper may still be stored in memory. // delete them in order to show the default wallpaper. - if (wallpaper.wallpaperFile.exists()) { - wallpaper.wallpaperFile.delete(); - wallpaper.cropFile.delete(); - } + clearWallpaperBitmaps(wallpaper); bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply); if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true; @@ -2034,9 +2078,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final long ident = Binder.clearCallingIdentity(); try { - if (wallpaper.wallpaperFile.exists()) { - wallpaper.wallpaperFile.delete(); - wallpaper.cropFile.delete(); + if (clearWallpaperBitmaps(wallpaper)) { if (which == FLAG_LOCK) { mLockWallpaperMap.remove(userId); final IWallpaperManagerCallback cb = mKeyguardListener; @@ -3107,9 +3149,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } catch (ErrnoException e) { Slog.e(TAG, "Can't migrate system wallpaper: " + e.getMessage()); - lockWP.wallpaperFile.delete(); - lockWP.cropFile.delete(); - return; + clearWallpaperBitmaps(lockWP); } } @@ -3251,6 +3291,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub }); } } + if (!mImageWallpaper.equals(newWallpaper.wallpaperComponent)) { + clearWallpaperBitmaps(newWallpaper); + } newWallpaper.wallpaperId = makeWallpaperIdLocked(); notifyCallbacksLocked(newWallpaper); shouldNotifyColors = true; diff --git a/services/core/java/com/android/server/webkit/OWNERS b/services/core/java/com/android/server/webkit/OWNERS index 00e540a46ab2..e7fd7a5d1096 100644 --- a/services/core/java/com/android/server/webkit/OWNERS +++ b/services/core/java/com/android/server/webkit/OWNERS @@ -1,3 +1,3 @@ -changwan@google.com -tobiasjs@google.com +# Bug component: 76427 +ntfschr@google.com torne@google.com diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java index bd07622ee5ca..10a2b9717555 100644 --- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java +++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java @@ -167,6 +167,9 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, } final TaskSnapshot recordSnapshotInner(TYPE source, boolean allowSnapshotHome) { + if (shouldDisableSnapshots()) { + return null; + } final boolean snapshotHome = allowSnapshotHome && source.isActivityTypeHome(); final TaskSnapshot snapshot = captureSnapshot(source, snapshotHome); if (snapshot == null) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9af34bcb448b..79a6af90fbae 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4295,6 +4295,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.getActivityMetricsLogger().notifyActivityRemoved(this); mTaskSupervisor.mStoppingActivities.remove(this); + mLetterboxUiController.destroy(); waitingToShow = false; // Defer removal of this activity when either a child is animating, or app transition is on @@ -4364,8 +4365,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mWmService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/); } - mLetterboxUiController.destroy(); - if (!delayed) { updateReportedVisibilityLocked(); } diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 90af4c6236aa..1eb56f1b7d1c 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -280,6 +280,10 @@ class ActivityStartInterceptor { return false; } + if (isKeepProfilesRunningEnabled() && !isPackageSuspended()) { + return false; + } + IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT); @@ -322,8 +326,7 @@ class ActivityStartInterceptor { private boolean interceptSuspendedPackageIfNeeded() { // Do not intercept if the package is not suspended - if (mAInfo == null || mAInfo.applicationInfo == null || - (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) { + if (!isPackageSuspended()) { return false; } final PackageManagerInternal pmi = mService.getPackageManagerInternalLocked(); @@ -467,6 +470,17 @@ class ActivityStartInterceptor { return true; } + private boolean isPackageSuspended() { + return mAInfo != null && mAInfo.applicationInfo != null + && (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) != 0; + } + + private static boolean isKeepProfilesRunningEnabled() { + DevicePolicyManagerInternal dpmi = + LocalServices.getService(DevicePolicyManagerInternal.class); + return dpmi == null || dpmi.isKeepProfilesRunningEnabled(); + } + /** * Called when an activity is successfully launched. */ diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 0171c200b56c..3c976725cfee 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2367,6 +2367,10 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { final ActivityRecord prevTopActivity = mTopResumedActivity; final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask(); if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) { + if (topRootTask == null) { + // There's no focused task and there won't have any resumed activity either. + scheduleTopResumedActivityStateLossIfNeeded(); + } if (mService.isSleepingLocked()) { // There won't be a next resumed activity. The top process should still be updated // according to the current top focused activity. @@ -2376,16 +2380,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } // Ask previous activity to release the top state. - final boolean prevActivityReceivedTopState = - prevTopActivity != null && !mTopResumedActivityWaitingForPrev; - // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity - // before the prevTopActivity one hasn't reported back yet. So server never sent the top - // resumed state change message to prevTopActivity. - if (prevActivityReceivedTopState - && prevTopActivity.scheduleTopResumedActivityChanged(false /* onTop */)) { - scheduleTopResumedStateLossTimeout(prevTopActivity); - mTopResumedActivityWaitingForPrev = true; - } + scheduleTopResumedActivityStateLossIfNeeded(); // Update the current top activity. mTopResumedActivity = topRootTask.getTopResumedActivity(); @@ -2410,6 +2405,23 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mService.updateTopApp(mTopResumedActivity); } + /** Schedule current top resumed activity state loss */ + private void scheduleTopResumedActivityStateLossIfNeeded() { + if (mTopResumedActivity == null) { + return; + } + + // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity + // before the prevTopActivity one hasn't reported back yet. So server never sent the top + // resumed state change message to prevTopActivity. + if (!mTopResumedActivityWaitingForPrev + && mTopResumedActivity.scheduleTopResumedActivityChanged(false /* onTop */)) { + scheduleTopResumedStateLossTimeout(mTopResumedActivity); + mTopResumedActivityWaitingForPrev = true; + mTopResumedActivity = null; + } + } + /** Schedule top resumed state change if previous top activity already reported back. */ private void scheduleTopResumedActivityStateIfNeeded() { if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev) { diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java index 123a74dbf597..f7ccc0d91969 100644 --- a/services/core/java/com/android/server/wm/AppWarnings.java +++ b/services/core/java/com/android/server/wm/AppWarnings.java @@ -23,6 +23,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.os.Build; import android.os.Handler; @@ -178,6 +179,14 @@ class AppWarnings { * @param r activity record for which the warning may be displayed */ public void showDeprecatedAbiDialogIfNeeded(ActivityRecord r) { + final boolean isUsingAbiOverride = (r.info.applicationInfo.privateFlagsExt + & ApplicationInfo.PRIVATE_FLAG_EXT_CPU_OVERRIDE) != 0; + if (isUsingAbiOverride) { + // The abiOverride flag was specified during installation, which means that if the app + // is currently running in 32-bit mode, it is intended. Do not show the warning dialog. + return; + } + // The warning dialog can also be disabled for debugging purpose final boolean disableDeprecatedAbiDialog = SystemProperties.getBoolean( "debug.wm.disable_deprecated_abi_dialog", false); if (disableDeprecatedAbiDialog) { diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java index 31b1069dd022..db4762e5f877 100644 --- a/services/core/java/com/android/server/wm/DeviceStateController.java +++ b/services/core/java/com/android/server/wm/DeviceStateController.java @@ -111,9 +111,13 @@ final class DeviceStateController { } /** - * @return true if the rotation direction on the Z axis should be reversed. + * @return true if the rotation direction on the Z axis should be reversed for the default + * display. */ - boolean shouldReverseRotationDirectionAroundZAxis() { + boolean shouldReverseRotationDirectionAroundZAxis(@NonNull DisplayContent displayContent) { + if (!displayContent.isDefaultDisplay) { + return false; + } return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 6aec96988a77..7fc86b0fdc01 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -5602,17 +5602,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ void requestTransitionAndLegacyPrepare(@WindowManager.TransitionType int transit, @WindowManager.TransitionFlags int flags) { - prepareAppTransition(transit, flags); - mTransitionController.requestTransitionIfNeeded(transit, flags, - null /* trigger */, this); + requestTransitionAndLegacyPrepare(transit, flags, null /* trigger */); } /** @see #requestTransitionAndLegacyPrepare(int, int) */ void requestTransitionAndLegacyPrepare(@WindowManager.TransitionType int transit, - @Nullable WindowContainer trigger) { - prepareAppTransition(transit); - mTransitionController.requestTransitionIfNeeded(transit, 0 /* flags */, - trigger, this); + @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger) { + prepareAppTransition(transit, flags); + mTransitionController.requestTransitionIfNeeded(transit, flags, trigger, this); } void executeAppTransition() { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 8be36f07a040..b681c198538f 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -950,7 +950,7 @@ public class DisplayRotation { } void freezeRotation(int rotation) { - if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) { + if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mDisplayContent)) { rotation = RotationUtils.reverseRotationDirectionAroundZAxis(rotation); } @@ -1225,7 +1225,7 @@ public class DisplayRotation { if (mFoldController != null && mFoldController.shouldIgnoreSensorRotation()) { sensorRotation = -1; } - if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) { + if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mDisplayContent)) { sensorRotation = RotationUtils.reverseRotationDirectionAroundZAxis(sensorRotation); } mLastSensorRotation = sensorRotation; diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 7f845e6c1ead..21004ab8d75a 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -272,15 +272,17 @@ class InsetsSourceProvider { /** @return A new source computed by the specified window frame in the given display frames. */ InsetsSource createSimulatedSource(DisplayFrames displayFrames, Rect frame) { - // Don't copy visible frame because it might not be calculated in the provided display - // frames and it is not significant for this usage. - final InsetsSource source = new InsetsSource(mSource.getId(), mSource.getType()); - source.setVisible(mSource.isVisible()); + final InsetsSource source = new InsetsSource(mSource); mTmpRect.set(frame); if (mFrameProvider != null) { mFrameProvider.apply(displayFrames, mWindowContainer, mTmpRect); } source.setFrame(mTmpRect); + + // Don't copy visible frame because it might not be calculated in the provided display + // frames and it is not significant for this usage. + source.setVisibleFrame(null); + return source; } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 99878a3d0ffe..ad9c3b274267 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -19,17 +19,21 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_LAUNCHER_CLEAR_SNAPSHOT; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_SUBTLE_WINDOW_ANIMATIONS; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; @@ -171,10 +175,11 @@ class KeyguardController { final KeyguardDisplayState state = getDisplayState(displayId); final boolean aodChanged = aodShowing != state.mAodShowing; final boolean aodRemoved = state.mAodShowing && !aodShowing; + final boolean goingAwayRemoved = state.mKeyguardGoingAway && keyguardShowing; // If keyguard is going away, but SystemUI aborted the transition, need to reset state. // Do not reset keyguardChanged status when only AOD is removed. final boolean keyguardChanged = (keyguardShowing != state.mKeyguardShowing) - || (state.mKeyguardGoingAway && keyguardShowing && !aodRemoved); + || (goingAwayRemoved && !aodRemoved); if (aodRemoved) { updateDeferTransitionForAod(false /* waiting */); } @@ -214,6 +219,15 @@ class KeyguardController { if (keyguardShowing) { state.mDismissalRequested = false; } + if (goingAwayRemoved) { + // Keyguard dismiss is canceled. Send a transition to undo the changes and clean up + // before holding the sleep token again. + final DisplayContent dc = mRootWindowContainer.getDefaultDisplay(); + dc.requestTransitionAndLegacyPrepare( + TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); + dc.mWallpaperController.showWallpaperInTransition(false /* showHome */); + mWindowManager.executeAppTransition(); + } } // Update the sleep token first such that ensureActivitiesVisible has correct sleep token @@ -269,7 +283,7 @@ class KeyguardController { updateKeyguardSleepToken(); // Make the home wallpaper visible - dc.mWallpaperController.showHomeWallpaperInTransition(); + dc.mWallpaperController.showWallpaperInTransition(true /* showHome */); // Some stack visibility might change (e.g. docked stack) mRootWindowContainer.resumeFocusedTasksTopActivities(); mRootWindowContainer.ensureActivitiesVisible(null, 0, !PRESERVE_WINDOWS); @@ -413,10 +427,12 @@ class KeyguardController { if (isDisplayOccluded(DEFAULT_DISPLAY)) { mRootWindowContainer.getDefaultDisplay().requestTransitionAndLegacyPrepare( TRANSIT_KEYGUARD_OCCLUDE, + TRANSIT_FLAG_KEYGUARD_OCCLUDING, topActivity != null ? topActivity.getRootTask() : null); } else { mRootWindowContainer.getDefaultDisplay().requestTransitionAndLegacyPrepare( - TRANSIT_KEYGUARD_UNOCCLUDE, 0 /* flags */); + TRANSIT_KEYGUARD_UNOCCLUDE, + TRANSIT_FLAG_KEYGUARD_UNOCCLUDING); } updateKeyguardSleepToken(DEFAULT_DISPLAY); mWindowManager.executeAppTransition(); diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index afef85e34fe0..58e1c544202d 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -314,6 +314,11 @@ class SnapshotPersistQueue { } final Bitmap swBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false /* isMutable */); + if (swBitmap == null) { + Slog.e(TAG, "Bitmap conversion from (config=" + bitmap.getConfig() + ", isMutable=" + + bitmap.isMutable() + ") to (config=ARGB_8888, isMutable=false) failed."); + return false; + } final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId); try { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ce4f44540285..1ae1e03a6c4a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1847,6 +1847,9 @@ class Task extends TaskFragment { td.setEnsureStatusBarContrastWhenTransparent( atd.getEnsureStatusBarContrastWhenTransparent()); } + if (td.getStatusBarAppearance() == 0) { + td.setStatusBarAppearance(atd.getStatusBarAppearance()); + } if (td.getNavigationBarColor() == 0) { td.setNavigationBarColor(atd.getNavigationBarColor()); td.setEnsureNavigationBarContrastWhenTransparent( diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index b938b9e4ec51..9a5f766989ca 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -52,7 +52,6 @@ import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; -import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TASK_LAUNCHING_BEHIND; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -2820,9 +2819,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } } - if (occludesKeyguard(wc)) { - flags |= FLAG_OCCLUDES_KEYGUARD; - } if ((mFlags & FLAG_CHANGE_NO_ANIMATION) != 0 && (mFlags & FLAG_CHANGE_YES_ANIMATION) == 0) { flags |= FLAG_NO_ANIMATION; diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 1c6a412e5450..0cb6f14b38f2 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -16,10 +16,10 @@ package com.android.server.wm; +import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; @@ -619,9 +619,9 @@ class TransitionController { } // Make the collecting transition wait until this request is ready. mCollectingTransition.setReady(readyGroupRef, false); - if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { - // Add keyguard flag to dismiss keyguard - mCollectingTransition.addFlag(flags); + if ((flags & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) { + // Add keyguard flags to affect keyguard visibility + mCollectingTransition.addFlag(flags & KEYGUARD_VISIBILITY_TRANSIT_FLAGS); } } else { newTransition = requestStartTransition(createTransition(type, flags), diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 64038355b1df..f54a962e4897 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -339,12 +339,12 @@ class WallpaperController { } /** - * Change the visibility if wallpaper is home screen only. + * Make one wallpaper visible, according to {@attr showHome}. * This is called during the keyguard unlocking transition - * (see {@link KeyguardController#keyguardGoingAway(int, int)}) and thus assumes that if the - * system wallpaper is shared with lock, then it needs no animation. + * (see {@link KeyguardController#keyguardGoingAway(int, int)}), + * or when a keyguard unlock is cancelled (see {@link KeyguardController}) */ - public void showHomeWallpaperInTransition() { + public void showWallpaperInTransition(boolean showHome) { updateWallpaperWindowsTarget(mFindResults); if (!mFindResults.hasTopShowWhenLockedWallpaper()) { @@ -357,9 +357,9 @@ class WallpaperController { // Shared wallpaper, ensure its visibility showWhenLocked.mToken.asWallpaperToken().updateWallpaperWindows(true); } else { - // Separate lock and home wallpapers: show home wallpaper and hide lock - hideWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(true); - showWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(false); + // Separate lock and home wallpapers: show the correct wallpaper in transition + hideWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(showHome); + showWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(!showHome); } } @@ -401,6 +401,19 @@ class WallpaperController { // swiping through Launcher pages). final Rect wallpaperFrame = wallpaperWin.getFrame(); + final int diffWidth = wallpaperFrame.width() - lastWallpaperBounds.width(); + final int diffHeight = wallpaperFrame.height() - lastWallpaperBounds.height(); + if ((wallpaperWin.mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0 + && Math.abs(diffWidth) > 1 && Math.abs(diffHeight) > 1) { + Slog.d(TAG, "Skip wallpaper offset with inconsistent orientation, bounds=" + + lastWallpaperBounds + " frame=" + wallpaperFrame); + // With FLAG_SCALED, the requested size should at least make the frame match one of + // side. If both sides contain differences, the client side may not have updated the + // latest size according to the current orientation. So skip calculating the offset to + // avoid the wallpaper not filling the screen. + return false; + } + int newXOffset = 0; int newYOffset = 0; boolean rawChanged = false; @@ -417,7 +430,7 @@ class WallpaperController { float wpxs = mLastWallpaperXStep >= 0 ? mLastWallpaperXStep : -1.0f; // Difference between width of wallpaper image, and the last size of the wallpaper. // This is the horizontal surplus from the prior configuration. - int availw = wallpaperFrame.width() - lastWallpaperBounds.width(); + int availw = diffWidth; int displayOffset = getDisplayWidthOffset(availw, lastWallpaperBounds, wallpaperWin.isRtl()); @@ -442,9 +455,7 @@ class WallpaperController { float wpy = mLastWallpaperY >= 0 ? mLastWallpaperY : 0.5f; float wpys = mLastWallpaperYStep >= 0 ? mLastWallpaperYStep : -1.0f; - int availh = wallpaperWin.getFrame().bottom - wallpaperWin.getFrame().top - - lastWallpaperBounds.height(); - offset = availh > 0 ? -(int)(availh * wpy + .5f) : 0; + offset = diffHeight > 0 ? -(int) (diffHeight * wpy + .5f) : 0; if (mLastWallpaperDisplayOffsetY != Integer.MIN_VALUE) { offset += mLastWallpaperDisplayOffsetY; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index a2fde621a06f..c4e3aae0a7c9 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7161,15 +7161,45 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) { - mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS, - "requestAppKeyboardShortcuts"); + enforceRegisterWindowManagerListenersPermission("requestAppKeyboardShortcuts"); + WindowState focusedWindow = getFocusedWindow(); + if (focusedWindow == null || focusedWindow.mClient == null) { + notifyReceiverWithEmptyBundle(receiver); + return; + } try { - WindowState focusedWindow = getFocusedWindow(); - if (focusedWindow != null && focusedWindow.mClient != null) { - getFocusedWindow().mClient.requestAppKeyboardShortcuts(receiver, deviceId); - } + focusedWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId); + } catch (RemoteException e) { + notifyReceiverWithEmptyBundle(receiver); + } + } + + @Override + public void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId) { + enforceRegisterWindowManagerListenersPermission("requestImeKeyboardShortcuts"); + + WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); + if (imeWindow == null || imeWindow.mClient == null) { + notifyReceiverWithEmptyBundle(receiver); + return; + } + try { + imeWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId); + } catch (RemoteException e) { + notifyReceiverWithEmptyBundle(receiver); + } + } + + private void enforceRegisterWindowManagerListenersPermission(String message) { + mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS, message); + } + + private static void notifyReceiverWithEmptyBundle(IResultReceiver receiver) { + try { + receiver.send(0, Bundle.EMPTY); } catch (RemoteException e) { + ProtoLog.e(WM_ERROR, "unable to call receiver for empty keyboard shortcuts"); } } @@ -8246,6 +8276,13 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void addTrustedTaskOverlay(int taskId, SurfaceControlViewHost.SurfacePackage overlay) { + if (overlay == null) { + throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId); + } else if (overlay.getSurfaceControl() == null + || !overlay.getSurfaceControl().isValid()) { + throw new IllegalArgumentException( + "Invalid overlay surfacecontrol passed in for task=" + taskId); + } synchronized (mGlobalLock) { final Task task = mRoot.getRootTask(taskId); if (task == null) { @@ -8258,6 +8295,13 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void removeTrustedTaskOverlay(int taskId, SurfaceControlViewHost.SurfacePackage overlay) { + if (overlay == null) { + throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId); + } else if (overlay.getSurfaceControl() == null + || !overlay.getSurfaceControl().isValid()) { + throw new IllegalArgumentException( + "Invalid overlay surfacecontrol passed in for task=" + taskId); + } synchronized (mGlobalLock) { final Task task = mRoot.getRootTask(taskId); if (task == null) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 7cd7f6975bbd..b52935e9da9b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1445,6 +1445,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP Slog.v(TAG_WM, "Win " + this + " config changed: " + getConfiguration()); } + final boolean dragResizingChanged = !mDragResizingChangeReported && isDragResizeChanged(); + final boolean attachedFrameChanged = LOCAL_LAYOUT && mLayoutAttached && getParentWindow().frameChanged(); @@ -1458,6 +1460,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (didFrameInsetsChange || configChanged || insetsChanged + || dragResizingChanged || shouldSendRedrawForSync() || attachedFrameChanged) { ProtoLog.v(WM_DEBUG_RESIZE, @@ -1478,7 +1481,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Reset the drawn state if the window need to redraw for the change, so the transition // can wait until it has finished drawing to start. - if ((configChanged || getOrientationChanging()) && isVisibleRequested()) { + if ((configChanged || getOrientationChanging() || dragResizingChanged) + && isVisibleRequested()) { winAnimator.mDrawState = DRAW_PENDING; if (mActivityRecord != null) { mActivityRecord.clearAllDrawn(); @@ -3883,13 +3887,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** - * @return true if activity bounds are letterboxed or letterboxed for diplay cutout. + * @return {@code true} if activity bounds are letterboxed or letterboxed for display cutout. + * Note that it's always {@code false} if the activity is in pip mode. * * <p>Note that letterbox UI may not be shown even when this returns {@code true}. See {@link * LetterboxUiController#shouldShowLetterboxUi} for more context. */ boolean areAppWindowBoundsLetterboxed() { return mActivityRecord != null + && !mActivityRecord.inPinnedWindowingMode() && (mActivityRecord.areBoundsLetterboxed() || isLetterboxedForDisplayCutout()); } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index d5217c8295bd..101af4d36162 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -177,7 +177,7 @@ cc_defaults { "android.hardware.power@1.1", "android.hardware.power@1.2", "android.hardware.power@1.3", - "android.hardware.power-V4-cpp", + "android.hardware.power-V4-ndk", "android.hardware.power.stats@1.0", "android.hardware.power.stats-V1-ndk", "android.hardware.thermal@1.0", diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp index e322fa23e5b4..e148b94bb0a5 100644 --- a/services/core/jni/com_android_server_hint_HintManagerService.cpp +++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp @@ -18,77 +18,78 @@ //#define LOG_NDEBUG 0 +#include <aidl/android/hardware/power/IPower.h> #include <android-base/stringprintf.h> -#include <android/hardware/power/IPower.h> -#include <android_runtime/AndroidRuntime.h> #include <nativehelper/JNIHelp.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <powermanager/PowerHalController.h> #include <utils/Log.h> -#include <unistd.h> -#include <cinttypes> - -#include <sys/types.h> +#include <unordered_map> #include "jni.h" -using android::hardware::power::IPowerHintSession; -using android::hardware::power::SessionHint; -using android::hardware::power::WorkDuration; +using aidl::android::hardware::power::IPowerHintSession; +using aidl::android::hardware::power::SessionHint; +using aidl::android::hardware::power::WorkDuration; using android::base::StringPrintf; namespace android { static power::PowerHalController gPowerHalController; +static std::unordered_map<jlong, std::shared_ptr<IPowerHintSession>> gSessionMap; +static std::mutex gSessionMapLock; static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid, std::vector<int32_t> threadIds, int64_t durationNanos) { - auto result = - gPowerHalController.createHintSession(tgid, uid, std::move(threadIds), durationNanos); + auto result = gPowerHalController.createHintSession(tgid, uid, threadIds, durationNanos); if (result.isOk()) { - sp<IPowerHintSession> appSession = result.value(); - if (appSession) appSession->incStrong(env); - return reinterpret_cast<jlong>(appSession.get()); + auto session_ptr = reinterpret_cast<jlong>(result.value().get()); + { + std::unique_lock<std::mutex> sessionLock(gSessionMapLock); + auto res = gSessionMap.insert({session_ptr, result.value()}); + return res.second ? session_ptr : 0; + } } return 0; } static void pauseHintSession(JNIEnv* env, int64_t session_ptr) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->pause(); } static void resumeHintSession(JNIEnv* env, int64_t session_ptr) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->resume(); } static void closeHintSession(JNIEnv* env, int64_t session_ptr) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->close(); - appSession->decStrong(env); + std::unique_lock<std::mutex> sessionLock(gSessionMapLock); + gSessionMap.erase(session_ptr); } static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->updateTargetWorkDuration(targetDurationNanos); } static void reportActualWorkDuration(int64_t session_ptr, const std::vector<WorkDuration>& actualDurations) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->reportActualWorkDuration(actualDurations); } static void sendHint(int64_t session_ptr, SessionHint hint) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->sendHint(hint); } static void setThreads(int64_t session_ptr, const std::vector<int32_t>& threadIds) { - sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); appSession->setThreads(threadIds); } diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp index fe86ff1ef7ef..6ab98feff210 100644 --- a/services/core/jni/com_android_server_power_PowerManagerService.cpp +++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp @@ -18,48 +18,40 @@ //#define LOG_NDEBUG 0 +#include "com_android_server_power_PowerManagerService.h" + +#include <aidl/android/hardware/power/Boost.h> +#include <aidl/android/hardware/power/Mode.h> #include <aidl/android/system/suspend/ISystemSuspend.h> #include <aidl/android/system/suspend/IWakeLock.h> -#include <android/hardware/power/1.1/IPower.h> -#include <android/hardware/power/Boost.h> -#include <android/hardware/power/IPower.h> -#include <android/hardware/power/Mode.h> -#include <android/system/suspend/ISuspendControlService.h> -#include <android/system/suspend/internal/ISuspendControlServiceInternal.h> -#include <nativehelper/JNIHelp.h> -#include "jni.h" - -#include <nativehelper/ScopedUtfChars.h> -#include <powermanager/PowerHalController.h> - -#include <limits.h> - #include <android-base/chrono_utils.h> #include <android/binder_manager.h> +#include <android/system/suspend/ISuspendControlService.h> +#include <android/system/suspend/internal/ISuspendControlServiceInternal.h> #include <android_runtime/AndroidRuntime.h> #include <android_runtime/Log.h> #include <binder/IServiceManager.h> #include <gui/SurfaceComposerClient.h> -#include <hardware/power.h> #include <hardware_legacy/power.h> #include <hidl/ServiceManagement.h> +#include <limits.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedUtfChars.h> +#include <powermanager/PowerHalController.h> #include <utils/Log.h> #include <utils/String8.h> #include <utils/Timers.h> #include <utils/misc.h> -#include "com_android_server_power_PowerManagerService.h" +#include "jni.h" +using aidl::android::hardware::power::Boost; +using aidl::android::hardware::power::Mode; using aidl::android::system::suspend::ISystemSuspend; using aidl::android::system::suspend::IWakeLock; using aidl::android::system::suspend::WakeLockType; using android::String8; -using android::hardware::power::Boost; -using android::hardware::power::Mode; using android::system::suspend::ISuspendControlService; -using IPowerV1_1 = android::hardware::power::V1_1::IPower; -using IPowerV1_0 = android::hardware::power::V1_0::IPower; -using IPowerAidl = android::hardware::power::IPower; namespace android { diff --git a/services/core/jni/com_android_server_power_PowerManagerService.h b/services/core/jni/com_android_server_power_PowerManagerService.h index a2f335c74870..36aaceb029c7 100644 --- a/services/core/jni/com_android_server_power_PowerManagerService.h +++ b/services/core/jni/com_android_server_power_PowerManagerService.h @@ -18,9 +18,10 @@ #define _ANDROID_SERVER_POWER_MANAGER_SERVICE_H #include <nativehelper/JNIHelp.h> -#include "jni.h" - #include <powermanager/PowerManager.h> +#include <utils/Timers.h> + +#include "jni.h" namespace android { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 5ba22830eec9..c918fb87154f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -16,6 +16,7 @@ package com.android.server.devicepolicy; +import static android.app.admin.DevicePolicyIdentifiers.USER_CONTROL_DISABLED_PACKAGES_POLICY; import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_TARGET_USER_ID; import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY; import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_CONFLICTING_ADMIN_POLICY; @@ -176,6 +177,16 @@ final class DevicePolicyEngine { } boolean policyEnforced = Objects.equals( localPolicyState.getCurrentResolvedPolicy(), value); + // TODO(b/285532044): remove hack and handle properly + if (!policyEnforced + && policyDefinition.getPolicyKey().getIdentifier().equals( + USER_CONTROL_DISABLED_PACKAGES_POLICY)) { + PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value; + PolicyValue<Set<String>> parsedResolvedValue = + (PolicyValue<Set<String>>) localPolicyState.getCurrentResolvedPolicy(); + policyEnforced = (parsedResolvedValue != null && parsedValue != null + && parsedResolvedValue.getValue().containsAll(parsedValue.getValue())); + } sendPolicyResultToAdmin( enforcingAdmin, policyDefinition, @@ -418,6 +429,17 @@ final class DevicePolicyEngine { boolean policyAppliedGlobally = Objects.equals( globalPolicyState.getCurrentResolvedPolicy(), value); + // TODO(b/285532044): remove hack and handle properly + if (!policyAppliedGlobally + && policyDefinition.getPolicyKey().getIdentifier().equals( + USER_CONTROL_DISABLED_PACKAGES_POLICY)) { + PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value; + PolicyValue<Set<String>> parsedResolvedValue = + (PolicyValue<Set<String>>) globalPolicyState.getCurrentResolvedPolicy(); + policyAppliedGlobally = (parsedResolvedValue != null && parsedValue != null + && parsedResolvedValue.getValue().containsAll(parsedValue.getValue())); + } + boolean policyApplied = policyAppliedGlobally && policyAppliedOnAllUsers; sendPolicyResultToAdmin( @@ -539,8 +561,20 @@ final class DevicePolicyEngine { userId); } - isAdminPolicyApplied &= Objects.equals( - value, localPolicyState.getCurrentResolvedPolicy()); + // TODO(b/285532044): remove hack and handle properly + if (policyDefinition.getPolicyKey().getIdentifier().equals( + USER_CONTROL_DISABLED_PACKAGES_POLICY)) { + if (!Objects.equals(value, localPolicyState.getCurrentResolvedPolicy())) { + PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value; + PolicyValue<Set<String>> parsedResolvedValue = + (PolicyValue<Set<String>>) localPolicyState.getCurrentResolvedPolicy(); + isAdminPolicyApplied &= (parsedResolvedValue != null && parsedValue != null + && parsedResolvedValue.getValue().containsAll(parsedValue.getValue())); + } + } else { + isAdminPolicyApplied &= Objects.equals( + value, localPolicyState.getCurrentResolvedPolicy()); + } } return isAdminPolicyApplied; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index acc984cb06dc..3453e2b1ef5f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -49,6 +49,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK_TASK; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MICROPHONE; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MOBILE_NETWORK; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MODIFY_USERS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MTE; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY; @@ -73,7 +74,6 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_SYSTEM_UPDATES; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_TIME; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER; -import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USERS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_VPN; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WALLPAPER; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI; @@ -13725,7 +13725,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_ADD_CLONE_PROFILE, new String[]{MANAGE_DEVICE_POLICY_PROFILES}); USER_RESTRICTION_PERMISSIONS.put( - UserManager.DISALLOW_ADD_USER, new String[]{MANAGE_DEVICE_POLICY_USERS}); + UserManager.DISALLOW_ADD_USER, new String[]{MANAGE_DEVICE_POLICY_MODIFY_USERS}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_ADD_WIFI_CONFIG, new String[]{MANAGE_DEVICE_POLICY_WIFI}); USER_RESTRICTION_PERMISSIONS.put( @@ -13815,13 +13815,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_PRINTING, new String[]{MANAGE_DEVICE_POLICY_PRINTING}); USER_RESTRICTION_PERMISSIONS.put( - UserManager.DISALLOW_REMOVE_USER, new String[]{MANAGE_DEVICE_POLICY_USERS}); + UserManager.DISALLOW_REMOVE_USER, new String[]{MANAGE_DEVICE_POLICY_MODIFY_USERS}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_RUN_IN_BACKGROUND, new String[]{MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_SAFE_BOOT, new String[]{MANAGE_DEVICE_POLICY_SAFE_BOOT}); USER_RESTRICTION_PERMISSIONS.put( - UserManager.DISALLOW_SET_USER_ICON, new String[]{MANAGE_DEVICE_POLICY_USERS}); + UserManager.DISALLOW_SET_USER_ICON, new String[]{MANAGE_DEVICE_POLICY_MODIFY_USERS}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_SET_WALLPAPER, new String[]{MANAGE_DEVICE_POLICY_WALLPAPER}); USER_RESTRICTION_PERMISSIONS.put( @@ -13847,7 +13847,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_USB_FILE_TRANSFER, new String[]{MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER}); USER_RESTRICTION_PERMISSIONS.put( - UserManager.DISALLOW_USER_SWITCH, new String[]{MANAGE_DEVICE_POLICY_USERS}); + UserManager.DISALLOW_USER_SWITCH, new String[]{MANAGE_DEVICE_POLICY_MODIFY_USERS}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_WIFI_DIRECT, new String[]{MANAGE_DEVICE_POLICY_WIFI}); USER_RESTRICTION_PERMISSIONS.put( @@ -23036,6 +23036,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_LOCK_TASK, MANAGE_DEVICE_POLICY_MICROPHONE, MANAGE_DEVICE_POLICY_MOBILE_NETWORK, + MANAGE_DEVICE_POLICY_MODIFY_USERS, MANAGE_DEVICE_POLICY_MTE, MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, @@ -23059,7 +23060,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_TIME, MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING, MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER, - MANAGE_DEVICE_POLICY_USERS, MANAGE_DEVICE_POLICY_VPN, MANAGE_DEVICE_POLICY_WALLPAPER, MANAGE_DEVICE_POLICY_WIFI, @@ -23080,12 +23080,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_KEYGUARD, MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, MANAGE_DEVICE_POLICY_LOCK_TASK, + MANAGE_DEVICE_POLICY_MODIFY_USERS, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS, MANAGE_DEVICE_POLICY_SAFE_BOOT, MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE, MANAGE_DEVICE_POLICY_TIME, - MANAGE_DEVICE_POLICY_USERS, MANAGE_DEVICE_POLICY_WIPE_DATA ); @@ -23171,6 +23171,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_FUN, MANAGE_DEVICE_POLICY_LOCK_TASK, MANAGE_DEVICE_POLICY_MOBILE_NETWORK, + MANAGE_DEVICE_POLICY_MODIFY_USERS, MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA, MANAGE_DEVICE_POLICY_PRINTING, MANAGE_DEVICE_POLICY_PROFILES, @@ -23179,7 +23180,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_SMS, MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS, MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER, - MANAGE_DEVICE_POLICY_USERS, MANAGE_DEVICE_POLICY_WINDOWS, SET_TIME, SET_TIME_ZONE @@ -23369,6 +23369,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_LOCK_TASK, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); + CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MODIFY_USERS, + MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, @@ -23389,8 +23391,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); - CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_USERS, - MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_VPN, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_WALLPAPER, diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index cbf0d12cbb56..44a29e546914 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -372,6 +372,8 @@ class AccessPolicy private constructor( forEachTag { when (tagName) { TAG_PACKAGE_VERSIONS -> parsePackageVersions(state, userId) + TAG_DEFAULT_PERMISSION_GRANT -> + parseDefaultPermissionGrant(state, userId) else -> { forEachSchemePolicy { with(it) { parseUserState(state, userId) } @@ -416,12 +418,24 @@ class AccessPolicy private constructor( packageVersions[packageName] = version } + private fun BinaryXmlPullParser.parseDefaultPermissionGrant( + state: MutableAccessState, + userId: Int + ) { + val userState = state.mutateUserState(userId, WriteMode.NONE)!! + val fingerprint = getAttributeValueOrThrow(ATTR_FINGERPRINT).intern() + userState.setDefaultPermissionGrantFingerprint(fingerprint) + } + fun BinaryXmlSerializer.serializeUserState(state: AccessState, userId: Int) { tag(TAG_ACCESS) { + serializePackageVersions(state.userStates[userId]!!.packageVersions) + serializeDefaultPermissionGrantFingerprint( + state.userStates[userId]!!.defaultPermissionGrantFingerprint + ) forEachSchemePolicy { with(it) { serializeUserState(state, userId) } } - serializePackageVersions(state.userStates[userId]!!.packageVersions) } } @@ -438,6 +452,16 @@ class AccessPolicy private constructor( } } + private fun BinaryXmlSerializer.serializeDefaultPermissionGrantFingerprint( + fingerprint: String? + ) { + if (fingerprint != null) { + tag(TAG_DEFAULT_PERMISSION_GRANT) { + attributeInterned(ATTR_FINGERPRINT, fingerprint) + } + } + } + private fun getSchemePolicy(subject: AccessUri, `object`: AccessUri): SchemePolicy = getSchemePolicy(subject.scheme, `object`.scheme) @@ -455,9 +479,11 @@ class AccessPolicy private constructor( internal const val VERSION_LATEST = 14 private const val TAG_ACCESS = "access" + private const val TAG_DEFAULT_PERMISSION_GRANT = "default-permission-grant" private const val TAG_PACKAGE_VERSIONS = "package-versions" private const val TAG_PACKAGE = "package" + private const val ATTR_FINGERPRINT = "fingerprint" private const val ATTR_NAME = "name" private const val ATTR_VERSION = "version" } diff --git a/services/permission/java/com/android/server/permission/access/AccessState.kt b/services/permission/java/com/android/server/permission/access/AccessState.kt index 77c31944651e..4ec32ea53f28 100644 --- a/services/permission/java/com/android/server/permission/access/AccessState.kt +++ b/services/permission/java/com/android/server/permission/access/AccessState.kt @@ -348,6 +348,7 @@ sealed class UserState( internal val appIdPermissionFlagsReference: AppIdPermissionFlagsReference, internal val appIdAppOpModesReference: AppIdAppOpModesReference, internal val packageAppOpModesReference: PackageAppOpModesReference, + defaultPermissionGrantFingerprint: String?, writeMode: Int ) : WritableState, Immutable<MutableUserState> { val packageVersions: IndexedMap<String, Int> @@ -362,6 +363,9 @@ sealed class UserState( val packageAppOpModes: PackageAppOpModes get() = packageAppOpModesReference.get() + var defaultPermissionGrantFingerprint: String? = defaultPermissionGrantFingerprint + protected set + override var writeMode: Int = writeMode protected set @@ -373,12 +377,14 @@ class MutableUserState private constructor( appIdPermissionFlagsReference: AppIdPermissionFlagsReference, appIdAppOpModesReference: AppIdAppOpModesReference, packageAppOpModesReference: PackageAppOpModesReference, + defaultPermissionGrantFingerprint: String?, writeMode: Int ) : UserState( packageVersionsReference, appIdPermissionFlagsReference, appIdAppOpModesReference, packageAppOpModesReference, + defaultPermissionGrantFingerprint, writeMode ), MutableWritableState { constructor() : this( @@ -386,6 +392,7 @@ class MutableUserState private constructor( AppIdPermissionFlagsReference(MutableAppIdPermissionFlags()), AppIdAppOpModesReference(MutableAppIdAppOpModes()), PackageAppOpModesReference(MutablePackageAppOpModes()), + null, WriteMode.NONE ) @@ -394,6 +401,7 @@ class MutableUserState private constructor( userState.appIdPermissionFlagsReference.toImmutable(), userState.appIdAppOpModesReference.toImmutable(), userState.packageAppOpModesReference.toImmutable(), + userState.defaultPermissionGrantFingerprint, WriteMode.NONE ) @@ -406,6 +414,11 @@ class MutableUserState private constructor( fun mutatePackageAppOpModes(): MutablePackageAppOpModes = packageAppOpModesReference.mutate() + @JvmName("setDefaultPermissionGrantFingerprintPublic") + fun setDefaultPermissionGrantFingerprint(defaultPermissionGrantFingerprint: String?) { + this.defaultPermissionGrantFingerprint = defaultPermissionGrantFingerprint + } + override fun requestWriteMode(writeMode: Int) { this.writeMode = maxOf(this.writeMode, writeMode) } diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index fa2bccf03379..82fe0a43af20 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -1992,6 +1992,15 @@ class PermissionService( override fun writeLegacyPermissionStateTEMP() {} + override fun getDefaultPermissionGrantFingerprint(userId: Int): String? = + service.getState { state.userStates[userId]!!.defaultPermissionGrantFingerprint } + + override fun setDefaultPermissionGrantFingerprint(fingerprint: String, userId: Int) { + service.mutateState { + newState.mutateUserState(userId)!!.setDefaultPermissionGrantFingerprint(fingerprint) + } + } + override fun onSystemReady() { service.onSystemReady() permissionControllerManager = PermissionControllerManager( diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index 92e4560bf582..a1d846e0f426 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -105,6 +105,10 @@ android_test { ":PackageParserTestApp6", ], resource_zips: [":PackageManagerServiceServerTests_apks_as_resources"], + + data: [ + ":StubTestApp", + ], } // Rules to copy all the test apks to the intermediate raw resource directory diff --git a/services/tests/PackageManagerServiceTests/server/AndroidTest.xml b/services/tests/PackageManagerServiceTests/server/AndroidTest.xml index 1b93527065ec..a0ef03c69e3a 100644 --- a/services/tests/PackageManagerServiceTests/server/AndroidTest.xml +++ b/services/tests/PackageManagerServiceTests/server/AndroidTest.xml @@ -23,6 +23,14 @@ <option name="install-arg" value="-g" /> <option name="test-file-name" value="PackageManagerServiceServerTests.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="settings put global verifier_engprod 1" /> + </target_preparer> + + <!-- Load additional APKs onto device --> + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="push" value="StubTestApp.apk->/data/local/tmp/servicestests/StubTestApp.apk"/> + </target_preparer> <option name="test-tag" value="PackageManagerServiceServerTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java index b82ffb4d0b39..435f0d7f5f15 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java @@ -32,7 +32,6 @@ import android.app.AppGlobals; import android.content.IIntentReceiver; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; @@ -70,7 +69,7 @@ import java.util.regex.Pattern; @RunWith(AndroidJUnit4.class) public class PackageManagerServiceTest { - private static final String PACKAGE_NAME = "com.android.frameworks.servicestests"; + private static final String PACKAGE_NAME = "com.android.server.pm.test.service.server"; private static final String TEST_DATA_PATH = "/data/local/tmp/servicestests/"; private static final String TEST_APP_APK = "StubTestApp.apk"; diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java index dc92376263a6..ca4a4048cc00 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java @@ -66,6 +66,7 @@ import android.system.StructStat; import android.test.AndroidTestCase; import android.util.Log; +import androidx.test.filters.FlakyTest; import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; import androidx.test.filters.Suppress; @@ -2506,6 +2507,7 @@ public class PackageManagerTests extends AndroidTestCase { } @LargeTest + @FlakyTest(bugId = 283797480) public void testCheckSignaturesRotatedAgainstRotated() throws Exception { // checkSignatures should be successful when both apps have been signed with the same // rotated key since the initial signature comparison between the two apps should diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 4989f841a275..03231ecfb723 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -1904,6 +1904,34 @@ public class BroadcastQueueTest { } @Test + public void testReplacePending_diffReceivers() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp); + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp); + final BroadcastFilter receiverYellow = makeRegisteredReceiver(receiverYellowApp); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) + .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of( + withPriority(receiverGreen, 10), + withPriority(receiverBlue, 5), + withPriority(receiverYellow, 0)))); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of( + withPriority(receiverGreen, 10), + withPriority(receiverBlue, 5)))); + + waitForIdle(); + + verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, airplane); + verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane); + verifyScheduleRegisteredReceiver(never(), receiverYellowApp, airplane); + } + + @Test public void testIdleAndBarrier() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java index 13e16d6614fd..22ad7c46193b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java @@ -85,22 +85,17 @@ public final class CachedAppOptimizerTest { @Mock private PackageManagerInternal mPackageManagerInt; - private final TestableDeviceConfig mDeviceConfig = new TestableDeviceConfig(); - @Rule public final ApplicationExitInfoTest.ServiceThreadRule mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) - .configureSessionBuilder( - sessionBuilder -> mDeviceConfig.setUpMockedClasses(sessionBuilder)) - .build(); + .addStaticMockFixtures(TestableDeviceConfig::new).build(); @Before public void setUp() { System.loadLibrary("mockingservicestestjni"); - mDeviceConfig.setUpMockBehaviors(); mHandlerThread = new HandlerThread(""); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); @@ -131,7 +126,6 @@ public final class CachedAppOptimizerTest { mHandlerThread.quit(); mThread.quit(); mCountDown = null; - mDeviceConfig.tearDown(); } private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, String processName, diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index b7dbaf93b9e2..f89f73c98cfd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Rect; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -1009,6 +1010,72 @@ public class LocalDisplayAdapterTest { 0.001f); } + @Test + public void test_getDisplayDeviceInfoLocked_internalDisplay_usesCutoutAndCorners() + throws Exception { + setupCutoutAndRoundedCorners(); + FakeDisplay display = new FakeDisplay(PORT_A); + display.info.isInternal = true; + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + + // Turn on / initialize + Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, + 0); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + mListener.changedDisplays.clear(); + + DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + + assertThat(info.displayCutout).isNotNull(); + assertThat(info.displayCutout.getBoundingRectTop()).isEqualTo(new Rect(507, 33, 573, 99)); + assertThat(info.roundedCorners).isNotNull(); + assertThat(info.roundedCorners.getRoundedCorner(0).getRadius()).isEqualTo(5); + } + + @Test public void test_getDisplayDeviceInfoLocked_externalDisplay_doesNotUseCutoutOrCorners() + throws Exception { + setupCutoutAndRoundedCorners(); + FakeDisplay display = new FakeDisplay(PORT_A); + display.info.isInternal = false; + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + + // Turn on / initialize + Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, + 0); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + mListener.changedDisplays.clear(); + + DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + + assertThat(info.displayCutout).isNull(); + assertThat(info.roundedCorners).isNull(); + } + + private void setupCutoutAndRoundedCorners() { + String sampleCutout = "M 507,66\n" + + "a 33,33 0 1 0 66,0 33,33 0 1 0 -66,0\n" + + "Z\n" + + "@left\n"; + // Setup some default cutout + when(mMockedResources.getString( + com.android.internal.R.string.config_mainBuiltInDisplayCutout)) + .thenReturn(sampleCutout); + when(mMockedResources.getDimensionPixelSize( + com.android.internal.R.dimen.rounded_corner_radius)).thenReturn(5); + } + private void assertDisplayDpi(DisplayDeviceInfo info, int expectedPort, float expectedXdpi, float expectedYDpi, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt index cfef0b23b3e7..5fd270ecb2b4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt @@ -48,6 +48,7 @@ open class PackageHelperTestBase { const val UNINSTALLER_PACKAGE = "com.android.test.known.uninstaller" const val VERIFIER_PACKAGE = "com.android.test.known.verifier" const val PERMISSION_CONTROLLER_PACKAGE = "com.android.test.known.permission" + const val MGMT_ROLE_HOLDER_PACKAGE = "com.android.test.know.device_management" const val TEST_USER_ID = 0 } @@ -119,6 +120,8 @@ open class PackageHelperTestBase { Mockito.doReturn(arrayOf(PERMISSION_CONTROLLER_PACKAGE)).`when`(pms) .getKnownPackageNamesInternal(any(), eq(KnownPackages.PACKAGE_PERMISSION_CONTROLLER), eq(TEST_USER_ID)) + Mockito.doReturn(MGMT_ROLE_HOLDER_PACKAGE).`when`(pms) + .getDevicePolicyManagementRoleHolderPackageName(eq(TEST_USER_ID)) } private fun createPackageManagerService(vararg stageExistingPackages: String): diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt index f9a8ead9cd4a..5cca5fa8ea0b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt @@ -128,13 +128,14 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { fun setPackagesSuspended_forQuietMode() { val knownPackages = arrayOf(DEVICE_ADMIN_PACKAGE, DEFAULT_HOME_PACKAGE, DIALER_PACKAGE, INSTALLER_PACKAGE, UNINSTALLER_PACKAGE, VERIFIER_PACKAGE, - PERMISSION_CONTROLLER_PACKAGE) + PERMISSION_CONTROLLER_PACKAGE, MGMT_ROLE_HOLDER_PACKAGE) val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), knownPackages, true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, true /* forQuietMode */)!! - assertThat(failedNames.size).isEqualTo(0) + assertThat(failedNames.size).isEqualTo(1) + assertThat(failedNames[0]).isEqualTo(MGMT_ROLE_HOLDER_PACKAGE) } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java index 43f77bfb4c95..e8e1dacd6fcf 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java @@ -109,8 +109,8 @@ public class ScreenUndimDetectorTest { public void recordScreenPolicy_disabledByFlag_noop() { DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_ENABLED, Boolean.FALSE.toString(), false /*makeDefault*/); + mScreenUndimDetector.readValuesFromDeviceConfig(); - setup(); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_DIM); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_BRIGHT); @@ -120,7 +120,7 @@ public class ScreenUndimDetectorTest { @Test public void recordScreenPolicy_samePolicy_noop() { for (int policy : ALL_POLICIES) { - setup(); + resetDetector(); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, policy); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, policy); @@ -154,7 +154,7 @@ public class ScreenUndimDetectorTest { if (from == POLICY_DIM && to == POLICY_BRIGHT) { continue; } - setup(); + resetDetector(); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, from); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, to); @@ -295,7 +295,8 @@ public class ScreenUndimDetectorTest { @Test public void recordScreenPolicy_dimToNonBright_resets() { for (int to : Arrays.asList(POLICY_OFF, POLICY_DOZE)) { - setup(); + resetDetector(); + mScreenUndimDetector.mUndimCounter = 1; mScreenUndimDetector.mUndimCounterStartedMillis = 123; mScreenUndimDetector.mWakeLock.acquire(); @@ -313,7 +314,8 @@ public class ScreenUndimDetectorTest { @Test public void recordScreenPolicy_brightToNonDim_resets() { for (int to : Arrays.asList(POLICY_OFF, POLICY_DOZE)) { - setup(); + resetDetector(); + mScreenUndimDetector.mUndimCounter = 1; mScreenUndimDetector.mUndimCounterStartedMillis = 123; mScreenUndimDetector.mWakeLock.acquire(); @@ -356,4 +358,9 @@ public class ScreenUndimDetectorTest { } } } + + private void resetDetector() { + mScreenUndimDetector.reset(); + mScreenUndimDetector.mCurrentScreenPolicy = 0; + } } diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 19af8dc23329..4edb167a54c7 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -116,7 +116,6 @@ android_test { ":SimpleServiceTestApp1", ":SimpleServiceTestApp2", ":SimpleServiceTestApp3", - ":StubTestApp", ":SuspendTestApp", ":MediaButtonReceiverHolderTestHelperApp", "data/broken_shortcut.xml", diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index b304968f3e69..fbb0ca108ecd 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -44,11 +44,6 @@ <option name="teardown-command" value="rm -rf /data/local/tmp/servicestests"/> </target_preparer> - <!-- Load additional APKs onto device --> - <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> - <option name="push" value="StubTestApp.apk->/data/local/tmp/servicestests/StubTestApp.apk"/> - </target_preparer> - <option name="test-tag" value="FrameworksServicesTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.frameworks.servicestests" /> diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java index c872a11e7988..5f19887f15b6 100644 --- a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java @@ -18,17 +18,31 @@ package com.android.server.contentcapture; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.annotation.NonNull; +import android.content.ComponentName; import android.content.ContentCaptureOptions; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; +import android.service.contentcapture.ContentCaptureServiceInfo; +import android.view.contentcapture.ContentCaptureEvent; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.server.LocalServices; +import com.android.server.contentprotection.ContentProtectionBlocklistManager; +import com.android.server.contentprotection.RemoteContentProtectionService; import com.android.server.pm.UserManagerInternal; import com.google.common.collect.ImmutableList; @@ -56,12 +70,39 @@ public class ContentCaptureManagerServiceTest { private static final String PACKAGE_NAME = "com.test.package"; + private static final ComponentName COMPONENT_NAME = + new ComponentName(PACKAGE_NAME, "TestClass"); + + private static final ContentCaptureEvent EVENT = + new ContentCaptureEvent(/* sessionId= */ 100, /* type= */ 200); + + private static final ParceledListSlice<ContentCaptureEvent> PARCELED_EVENTS = + new ParceledListSlice<>(ImmutableList.of(EVENT)); + private static final Context sContext = ApplicationProvider.getApplicationContext(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private UserManagerInternal mMockUserManagerInternal; + @Mock private ContentProtectionBlocklistManager mMockContentProtectionBlocklistManager; + + @Mock private ContentCaptureServiceInfo mMockContentCaptureServiceInfo; + + @Mock private RemoteContentProtectionService mMockRemoteContentProtectionService; + + private boolean mDevCfgEnableContentProtectionReceiver; + + private int mContentProtectionBlocklistManagersCreated; + + private int mContentProtectionServiceInfosCreated; + + private int mRemoteContentProtectionServicesCreated; + + private String mConfigDefaultContentProtectionService = COMPONENT_NAME.flattenToString(); + + private boolean mContentProtectionServiceInfoConstructorShouldThrow; + private ContentCaptureManagerService mContentCaptureManagerService; @Before @@ -69,20 +110,107 @@ public class ContentCaptureManagerServiceTest { when(mMockUserManagerInternal.getUserInfos()).thenReturn(new UserInfo[0]); LocalServices.removeServiceForTest(UserManagerInternal.class); LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal); - mContentCaptureManagerService = new ContentCaptureManagerService(sContext); + mContentCaptureManagerService = new TestContentCaptureManagerService(); + } + + @Test + public void constructor_contentProtection_flagDisabled_noBlocklistManager() { + assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0); + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0); + verifyZeroInteractions(mMockContentProtectionBlocklistManager); + } + + @Test + public void constructor_contentProtection_componentNameNull_noBlocklistManager() { + mConfigDefaultContentProtectionService = null; + + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0); + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0); + verifyZeroInteractions(mMockContentProtectionBlocklistManager); + } + + @Test + public void constructor_contentProtection_componentNameBlank_noBlocklistManager() { + mConfigDefaultContentProtectionService = " "; + + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0); + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0); + verifyZeroInteractions(mMockContentProtectionBlocklistManager); + } + + @Test + public void constructor_contentProtection_serviceInfoThrows_noBlocklistManager() { + mDevCfgEnableContentProtectionReceiver = true; + mContentProtectionServiceInfoConstructorShouldThrow = true; + + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0); + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1); + verifyZeroInteractions(mMockContentProtectionBlocklistManager); } @Test - public void getOptions_notAllowlisted() { + public void constructor_contentProtection_enabled_createsBlocklistManager() { + mDevCfgEnableContentProtectionReceiver = true; + + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(1); + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1); + verify(mMockContentProtectionBlocklistManager).updateBlocklist(anyInt()); + } + + @Test + public void setFineTuneParamsFromDeviceConfig_doesNotUpdateContentProtectionBlocklist() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + mContentCaptureManagerService.mDevCfgContentProtectionAppsBlocklistSize += 100; + verify(mMockContentProtectionBlocklistManager).updateBlocklist(anyInt()); + + mContentCaptureManagerService.setFineTuneParamsFromDeviceConfig(); + + verifyNoMoreInteractions(mMockContentProtectionBlocklistManager); + } + + @Test + public void getOptions_contentCaptureDisabled_contentProtectionDisabled() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + ContentCaptureOptions actual = mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions( USER_ID, PACKAGE_NAME); assertThat(actual).isNull(); + verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME); } @Test - public void getOptions_allowlisted_contentCaptureReceiver() { + public void getOptions_contentCaptureDisabled_contentProtectionEnabled() { + when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true); + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + ContentCaptureOptions actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isNotNull(); + assertThat(actual.enableReceiver).isFalse(); + assertThat(actual.contentProtectionOptions).isNotNull(); + assertThat(actual.contentProtectionOptions.enableReceiver).isTrue(); + assertThat(actual.whitelistedComponents).isNull(); + } + + @Test + public void getOptions_contentCaptureEnabled_contentProtectionDisabled() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist( USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null); @@ -92,13 +220,17 @@ public class ContentCaptureManagerServiceTest { assertThat(actual).isNotNull(); assertThat(actual.enableReceiver).isTrue(); + assertThat(actual.contentProtectionOptions).isNotNull(); assertThat(actual.contentProtectionOptions.enableReceiver).isFalse(); assertThat(actual.whitelistedComponents).isNull(); + verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME); } @Test - public void getOptions_allowlisted_bothReceivers() { - mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = true; + public void getOptions_contentCaptureEnabled_contentProtectionEnabled() { + when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true); + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist( USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null); @@ -108,7 +240,185 @@ public class ContentCaptureManagerServiceTest { assertThat(actual).isNotNull(); assertThat(actual.enableReceiver).isTrue(); + assertThat(actual.contentProtectionOptions).isNotNull(); assertThat(actual.contentProtectionOptions.enableReceiver).isTrue(); assertThat(actual.whitelistedComponents).isNull(); } + + @Test + public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionDisabled() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isFalse(); + verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME); + } + + @Test + public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionEnabled() { + when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true); + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isTrue(); + } + + @Test + public void isWhitelisted_packageName_contentCaptureEnabled_contentProtectionNotChecked() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist( + USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isTrue(); + verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString()); + } + + @Test + public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionDisabled() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, COMPONENT_NAME); + + assertThat(actual).isFalse(); + verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME); + } + + @Test + public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionEnabled() { + when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true); + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, COMPONENT_NAME); + + assertThat(actual).isTrue(); + } + + @Test + public void isWhitelisted_componentName_contentCaptureEnabled_contentProtectionNotChecked() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist( + USER_ID, /* packageNames= */ null, ImmutableList.of(COMPONENT_NAME)); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, COMPONENT_NAME); + + assertThat(actual).isTrue(); + verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString()); + } + + @Test + public void isContentProtectionReceiverEnabled_withoutBlocklistManager() { + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isFalse(); + verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString()); + } + + @Test + public void isContentProtectionReceiverEnabled_disabledWithFlag() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = false; + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isFalse(); + verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString()); + } + + @Test + public void onLoginDetected_disabledAfterConstructor() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = false; + + mContentCaptureManagerService + .getContentCaptureManagerServiceStub() + .onLoginDetected(PARCELED_EVENTS); + + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1); + assertThat(mRemoteContentProtectionServicesCreated).isEqualTo(0); + verifyZeroInteractions(mMockRemoteContentProtectionService); + } + + @Test + public void onLoginDetected_enabled() { + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + mContentCaptureManagerService + .getContentCaptureManagerServiceStub() + .onLoginDetected(PARCELED_EVENTS); + + assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1); + assertThat(mRemoteContentProtectionServicesCreated).isEqualTo(1); + verify(mMockRemoteContentProtectionService).onLoginDetected(PARCELED_EVENTS); + } + + private class TestContentCaptureManagerService extends ContentCaptureManagerService { + + TestContentCaptureManagerService() { + super(sContext); + this.mDevCfgEnableContentProtectionReceiver = + ContentCaptureManagerServiceTest.this.mDevCfgEnableContentProtectionReceiver; + } + + @Override + protected boolean getEnableContentProtectionReceiverLocked() { + return ContentCaptureManagerServiceTest.this.mDevCfgEnableContentProtectionReceiver; + } + + @Override + protected ContentProtectionBlocklistManager createContentProtectionBlocklistManager() { + mContentProtectionBlocklistManagersCreated++; + return mMockContentProtectionBlocklistManager; + } + + @Override + protected String getContentProtectionServiceFlatComponentName() { + return mConfigDefaultContentProtectionService; + } + + @Override + protected ContentCaptureServiceInfo createContentProtectionServiceInfo( + @NonNull ComponentName componentName) throws PackageManager.NameNotFoundException { + mContentProtectionServiceInfosCreated++; + if (mContentProtectionServiceInfoConstructorShouldThrow) { + throw new RuntimeException("TEST RUNTIME EXCEPTION"); + } + return mMockContentCaptureServiceInfo; + } + + @Override + protected RemoteContentProtectionService createRemoteContentProtectionService( + @NonNull ComponentName componentName) { + mRemoteContentProtectionServicesCreated++; + return mMockRemoteContentProtectionService; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/credentials/MetricUtilitiesTest.java b/services/tests/servicestests/src/com/android/server/credentials/MetricUtilitiesTest.java index b46f1a61c745..50ea48719948 100644 --- a/services/tests/servicestests/src/com/android/server/credentials/MetricUtilitiesTest.java +++ b/services/tests/servicestests/src/com/android/server/credentials/MetricUtilitiesTest.java @@ -15,14 +15,27 @@ */ package com.android.server.credentials; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.credentials.metrics.BrowsedAuthenticationMetric; +import com.android.server.credentials.metrics.CandidateAggregateMetric; +import com.android.server.credentials.metrics.CandidateBrowsingPhaseMetric; +import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric; import com.android.server.credentials.metrics.InitialPhaseMetric; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Given the secondary-system nature of the MetricUtilities, we expect absolutely nothing to * throw an error. If one presents itself, that is problematic. @@ -33,6 +46,11 @@ import org.junit.runner.RunWith; @SmallTest public final class MetricUtilitiesTest { + @Before + public void setUp() throws CertificateException { + final Context context = ApplicationProvider.getApplicationContext(); + } + @Test public void logApiCalledInitialPhase_nullInitPhaseMetricAndNegativeSequence_success() { MetricUtilities.logApiCalledInitialPhase(null, -1); @@ -49,4 +67,102 @@ public final class MetricUtilitiesTest { MetricUtilities.getHighlyUniqueInteger()); MetricUtilities.logApiCalledInitialPhase(validInitPhaseMetric, 1); } + + @Test + public void logApiCalledTotalCandidate_nullCandidateNegativeSequence_success() { + MetricUtilities.logApiCalledAggregateCandidate(null, -1); + } + + @Test + public void logApiCalledTotalCandidate_invalidCandidatePhasePositiveSequence_success() { + MetricUtilities.logApiCalledAggregateCandidate(new CandidateAggregateMetric(-1), 1); + } + + @Test + public void logApiCalledTotalCandidate_validPhaseMetric_success() { + MetricUtilities.logApiCalledAggregateCandidate( + new CandidateAggregateMetric(MetricUtilities.getHighlyUniqueInteger()), 1); + } + + @Test + public void logApiCalledNoUidFinal_nullNoUidFinalNegativeSequenceAndStatus_success() { + MetricUtilities.logApiCalledNoUidFinal(null, null, + -1, -1); + } + + @Test + public void logApiCalledNoUidFinal_invalidNoUidFinalPhasePositiveSequenceAndStatus_success() { + MetricUtilities.logApiCalledNoUidFinal(new ChosenProviderFinalPhaseMetric(-1, -1), + List.of(new CandidateBrowsingPhaseMetric()), 1, 1); + } + + @Test + public void logApiCalledNoUidFinal_validNoUidFinalMetric_success() { + MetricUtilities.logApiCalledNoUidFinal( + new ChosenProviderFinalPhaseMetric(MetricUtilities.getHighlyUniqueInteger(), + MetricUtilities.getHighlyUniqueInteger()), + List.of(new CandidateBrowsingPhaseMetric()), 1, 1); + } + + @Test + public void logApiCalledCandidate_nullMapNullInitFinalNegativeSequence_success() { + MetricUtilities.logApiCalledCandidatePhase(null, -1, + null); + } + + @Test + public void logApiCalledCandidate_invalidProvidersCandidatePositiveSequence_success() { + Map<String, ProviderSession> testMap = new HashMap<>(); + testMap.put("s", null); + MetricUtilities.logApiCalledCandidatePhase(testMap, 1, + null); + } + + @Test + public void logApiCalledCandidateGet_nullMapFinalNegativeSequence_success() { + MetricUtilities.logApiCalledCandidateGetMetric(null, -1); + } + + @Test + public void logApiCalledCandidateGet_invalidProvidersCandidatePositiveSequence_success() { + Map<String, ProviderSession> testMap = new HashMap<>(); + testMap.put("s", null); + MetricUtilities.logApiCalledCandidateGetMetric(testMap, 1); + } + + @Test + public void logApiCalledAuthMetric_nullAuthMetricNegativeSequence_success() { + MetricUtilities.logApiCalledAuthenticationMetric(null, -1); + } + + @Test + public void logApiCalledAuthMetric_invalidAuthMetricPositiveSequence_success() { + MetricUtilities.logApiCalledAuthenticationMetric(new BrowsedAuthenticationMetric(-1), 1); + } + + @Test + public void logApiCalledAuthMetric_nullAuthMetricPositiveSequence_success() { + MetricUtilities.logApiCalledAuthenticationMetric( + new BrowsedAuthenticationMetric(MetricUtilities.getHighlyUniqueInteger()), -1); + } + + @Test + public void logApiCalledFinal_nullFinalNegativeSequenceAndStatus_success() { + MetricUtilities.logApiCalledFinalPhase(null, null, + -1, -1); + } + + @Test + public void logApiCalledFinal_invalidFinalPhasePositiveSequenceAndStatus_success() { + MetricUtilities.logApiCalledFinalPhase(new ChosenProviderFinalPhaseMetric(-1, -1), + List.of(new CandidateBrowsingPhaseMetric()), 1, 1); + } + + @Test + public void logApiCalledFinal_validFinalMetric_success() { + MetricUtilities.logApiCalledFinalPhase( + new ChosenProviderFinalPhaseMetric(MetricUtilities.getHighlyUniqueInteger(), + MetricUtilities.getHighlyUniqueInteger()), + List.of(new CandidateBrowsingPhaseMetric()), 1, 1); + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java new file mode 100644 index 000000000000..920c376d5dfb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java @@ -0,0 +1,142 @@ +/* + * 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.server.hdmi; + +import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.os.Looper; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Collections; + +/** + * TV specific tests for {@link HdmiControlService} class. + */ +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public class HdmiControlServiceTvTest { + + private static final String TAG = "HdmiControlServiceTvTest"; + private HdmiControlService mHdmiControlService; + private HdmiCecController mHdmiCecController; + private FakeNativeWrapper mNativeWrapper; + private HdmiEarcController mHdmiEarcController; + private FakeEarcNativeWrapper mEarcNativeWrapper; + private Looper mMyLooper; + private TestLooper mTestLooper = new TestLooper(); + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + mMyLooper = mTestLooper.getLooper(); + + FakeAudioFramework audioFramework = new FakeAudioFramework(); + + mHdmiControlService = + new HdmiControlService(InstrumentationRegistry.getTargetContext(), + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { + @Override + int pathToPortId(int path) { + return Constants.INVALID_PORT_ID + 1; + } + }; + + mMyLooper = mTestLooper.getLooper(); + mHdmiControlService.setIoLooper(mMyLooper); + mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); + mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper()); + mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); + + mNativeWrapper = new FakeNativeWrapper(); + mHdmiCecController = HdmiCecController.createWithNativeWrapper( + mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); + mHdmiControlService.setCecController(mHdmiCecController); + mEarcNativeWrapper = new FakeEarcNativeWrapper(); + mHdmiEarcController = HdmiEarcController.createWithNativeWrapper( + mHdmiControlService, mEarcNativeWrapper); + mHdmiControlService.setEarcController(mHdmiEarcController); + mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create( + mHdmiControlService)); + mHdmiControlService.initService(); + + mTestLooper.dispatchAll(); + } + + @Test + public void onCecMessage_shortPhysicalAddress_featureAbortInvalidOperand() { + // Invalid <Inactive Source> message. + HdmiCecMessage message = HdmiUtils.buildMessage("40:9D:14"); + + mNativeWrapper.onCecMessage(message); + mTestLooper.dispatchAll(); + + HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand( + Constants.ADDR_TV, Constants.ADDR_PLAYBACK_1, Constants.MESSAGE_INACTIVE_SOURCE, + Constants.ABORT_INVALID_OPERAND); + assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort); + } + + @Test + public void handleCecCommand_shortPhysicalAddress_returnsAbortInvalidOperand() { + // Invalid <Active Source> message. + HdmiCecMessage message = HdmiUtils.buildMessage("4F:82:10"); + + // In case of a broadcasted message <Feature Abort> is not expected. + // See CEC 1.4b specification, 12.2 Protocol General Rules for detail. + assertThat(mHdmiControlService.handleCecCommand(message)) + .isEqualTo(Constants.ABORT_INVALID_OPERAND); + } + + @Test + public void test_verifyPhysicalAddresses() { + // <Routing Change> + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("0F:80:10:00:40:00"))).isTrue(); + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("0F:80:10:00:40"))).isFalse(); + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("0F:80:10"))).isFalse(); + + // <System Audio Mode Request> + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("40:70:00:00"))).isTrue(); + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("40:70:00"))).isFalse(); + + // <Active Source> + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("4F:82:10:00"))).isTrue(); + assertThat(mHdmiControlService + .verifyPhysicalAddresses(HdmiUtils.buildMessage("4F:82:10"))).isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt new file mode 100644 index 000000000000..98b46286e977 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt @@ -0,0 +1,355 @@ +/* + * Copyright 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Resources +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.hardware.display.DisplayManagerInternal +import android.hardware.input.InputSensorInfo +import android.os.Handler +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.util.TypedValue +import android.view.Display +import android.view.DisplayInfo +import androidx.test.core.app.ApplicationProvider +import com.android.internal.R +import com.android.server.LocalServices +import com.android.server.input.AmbientKeyboardBacklightController.HYSTERESIS_THRESHOLD +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit + +/** + * Tests for {@link AmbientKeyboardBacklightController}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:AmbientKeyboardBacklightControllerTests + */ +@Presubmit +class AmbientKeyboardBacklightControllerTests { + + companion object { + const val DEFAULT_DISPLAY_UNIQUE_ID = "uniqueId_1" + const val SENSOR_NAME = "test_sensor_name" + const val SENSOR_TYPE = "test_sensor_type" + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var ambientController: AmbientKeyboardBacklightController + + @Mock + private lateinit var resources: Resources + + @Mock + private lateinit var lightSensorInfo: InputSensorInfo + + @Mock + private lateinit var sensorManager: SensorManager + + @Mock + private lateinit var displayManagerInternal: DisplayManagerInternal + private lateinit var lightSensor: Sensor + + private var currentDisplayInfo = DisplayInfo() + private var lastBrightnessCallback: Int = 0 + private var listenerRegistered: Boolean = false + private var listenerRegistrationCount: Int = 0 + + @Before + fun setup() { + context = spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + `when`(context.resources).thenReturn(resources) + setupBrightnessSteps() + setupSensor() + testLooper = TestLooper() + ambientController = AmbientKeyboardBacklightController(context, testLooper.looper) + ambientController.systemRunning() + testLooper.dispatchAll() + } + + private fun setupBrightnessSteps() { + val brightnessValues = intArrayOf(100, 200, 0) + val decreaseThresholds = intArrayOf(-1, 900, 1900) + val increaseThresholds = intArrayOf(1000, 2000, -1) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightBrightnessValues)) + .thenReturn(brightnessValues) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightDecreaseLuxThreshold)) + .thenReturn(decreaseThresholds) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold)) + .thenReturn(increaseThresholds) + `when`( + resources.getValue( + eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), + any(TypedValue::class.java), + anyBoolean() + ) + ).then { + val args = it.arguments + val outValue = args[1] as TypedValue + outValue.data = java.lang.Float.floatToRawIntBits(1.0f) + Unit + } + } + + private fun setupSensor() { + LocalServices.removeServiceForTest(DisplayManagerInternal::class.java) + LocalServices.addService(DisplayManagerInternal::class.java, displayManagerInternal) + currentDisplayInfo.uniqueId = DEFAULT_DISPLAY_UNIQUE_ID + `when`(displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn( + currentDisplayInfo + ) + val sensorData = DisplayManagerInternal.AmbientLightSensorData(SENSOR_NAME, SENSOR_TYPE) + `when`(displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY)) + .thenReturn(sensorData) + + `when`(lightSensorInfo.name).thenReturn(SENSOR_NAME) + `when`(lightSensorInfo.stringType).thenReturn(SENSOR_TYPE) + lightSensor = Sensor(lightSensorInfo) + `when`(context.getSystemService(eq(Context.SENSOR_SERVICE))).thenReturn(sensorManager) + `when`(sensorManager.getSensorList(anyInt())).thenReturn(listOf(lightSensor)) + `when`( + sensorManager.registerListener( + any(), + eq(lightSensor), + anyInt(), + any(Handler::class.java) + ) + ).then { + listenerRegistered = true + listenerRegistrationCount++ + true + } + `when`( + sensorManager.unregisterListener( + any(SensorEventListener::class.java), + eq(lightSensor) + ) + ).then { + listenerRegistered = false + Unit + } + } + + private fun setupSensorWithInitialLux(luxValue: Float) { + ambientController.registerAmbientBacklightListener { brightnessValue: Int -> + lastBrightnessCallback = brightnessValue + } + sendAmbientLuxValue(luxValue) + testLooper.dispatchAll() + } + + @Test + fun testInitialAmbientLux_sendsCallbackImmediately() { + setupSensorWithInitialLux(500F) + + assertEquals( + "Should receive immediate callback for first lux change", + 100, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessIncrease_afterInitialLuxChanges() { + setupSensorWithInitialLux(500F) + + // Current state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(1500F) + } + testLooper.dispatchAll() + + assertEquals( + "Should receive brightness change callback for increasing lux change", + 200, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessDecrease_afterInitialLuxChanges() { + setupSensorWithInitialLux(1500F) + + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(500F) + } + testLooper.dispatchAll() + + assertEquals( + "Should receive brightness change callback for decreasing lux change", + 100, + lastBrightnessCallback + ) + } + + @Test + fun testRegisterAmbientListener_throwsExceptionOnRegisteringDuplicate() { + val ambientListener = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener) + + assertThrows(IllegalStateException::class.java) { + ambientController.registerAmbientBacklightListener( + ambientListener + ) + } + } + + @Test + fun testUnregisterAmbientListener_throwsExceptionOnUnregisteringNonExistent() { + val ambientListener = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + assertThrows(IllegalStateException::class.java) { + ambientController.unregisterAmbientBacklightListener( + ambientListener + ) + } + } + + @Test + fun testSensorListenerRegistered_onRegisterUnregisterAmbientListener() { + assertEquals( + "Should not have a sensor listener registered at init", + 0, + listenerRegistrationCount + ) + assertFalse("Should not have a sensor listener registered at init", listenerRegistered) + + val ambientListener1 = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener1) + assertEquals( + "Should register a new sensor listener", 1, listenerRegistrationCount + ) + assertTrue("Should have sensor listener registered", listenerRegistered) + + val ambientListener2 = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener2) + assertEquals( + "Should not register a new sensor listener when adding a second ambient listener", + 1, + listenerRegistrationCount + ) + assertTrue("Should have sensor listener registered", listenerRegistered) + + ambientController.unregisterAmbientBacklightListener(ambientListener1) + assertTrue("Should have sensor listener registered", listenerRegistered) + + ambientController.unregisterAmbientBacklightListener(ambientListener2) + assertFalse( + "Should not have sensor listener registered if there are no ambient listeners", + listenerRegistered + ) + } + + @Test + fun testDisplayChange_shouldNotReRegisterListener_ifUniqueIdSame() { + setupSensorWithInitialLux(0F) + + val count = listenerRegistrationCount + ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY) + testLooper.dispatchAll() + + assertEquals( + "Should not re-register listener on display change if unique is same", + count, + listenerRegistrationCount + ) + } + + @Test + fun testDisplayChange_shouldReRegisterListener_ifUniqueIdChanges() { + setupSensorWithInitialLux(0F) + + val count = listenerRegistrationCount + currentDisplayInfo.uniqueId = "xyz" + ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY) + testLooper.dispatchAll() + + assertEquals( + "Should re-register listener on display change if unique id changed", + count + 1, + listenerRegistrationCount + ) + } + + @Test + fun testBrightnessDoesntChange_betweenIncreaseAndDecreaseThresholds() { + setupSensorWithInitialLux(1001F) + + // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + lastBrightnessCallback = -1 + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(999F) + } + testLooper.dispatchAll() + + assertEquals( + "Should not receive any callback for brightness change", + -1, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessDoesntChange_onChangeOccurringLessThanHysteresisThreshold() { + setupSensorWithInitialLux(1001F) + + // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + lastBrightnessCallback = -1 + repeat(HYSTERESIS_THRESHOLD - 1) { + sendAmbientLuxValue(2001F) + } + testLooper.dispatchAll() + + assertEquals( + "Should not receive any callback for brightness change", + -1, + lastBrightnessCallback + ) + } + + private fun sendAmbientLuxValue(luxValue: Float) { + ambientController.onSensorChanged(SensorEvent(lightSensor, 0, 0, floatArrayOf(luxValue))) + } +} diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt index ef15ccba4c74..67158f24839d 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -37,6 +37,8 @@ import com.android.server.input.KeyboardBacklightController.MAX_BRIGHTNESS_CHANG import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -154,7 +156,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -168,7 +174,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardWithoutBacklight() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) @@ -182,7 +192,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardWithMultipleLight() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) @@ -204,7 +218,11 @@ class KeyboardBacklightControllerTests { @Test fun testRestoreBacklightOnInputDeviceAdded() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -233,7 +251,11 @@ class KeyboardBacklightControllerTests { @Test fun testRestoreBacklightOnInputDeviceChanged() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -265,7 +287,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklight_registerUnregisterListener() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1 @@ -314,7 +340,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklight_userActivity() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -346,7 +376,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklight_displayOnOff() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -436,7 +470,11 @@ class KeyboardBacklightControllerTests { @Test @UiThreadTest fun testKeyboardBacklightAnimation_onChangeLevels() { - KeyboardBacklightFlags(animationEnabled = true, customLevelsEnabled = false).use { + KeyboardBacklightFlags( + animationEnabled = true, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) @@ -459,7 +497,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightPreferredLevels() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, @@ -475,7 +517,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) } val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, @@ -491,7 +537,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(22, 63, 135, 196) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, @@ -508,7 +558,11 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() { - KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, @@ -523,10 +577,148 @@ class KeyboardBacklightControllerTests { } } + @Test + fun testAmbientBacklightControl_doesntRestoreBacklightLevel() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1] + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertNull( + "Keyboard backlight level should not be restored to the saved level", + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_doesntBackupBacklightLevel() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + assertFalse( + "Light value should not be backed up if ambient control is enabled", + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, LIGHT_ID + ).isPresent + ) + } + } + + @Test + fun testAmbientBacklightControl_incrementLevel_afterAmbientChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(1) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(1, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + incrementKeyboardBacklight(DEVICE_ID) + + assertEquals( + "Light value for level after increment post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_decrementLevel_afterAmbientChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(254) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(254, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + decrementKeyboardBacklight(DEVICE_ID) + + val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size + assertEquals( + "Light value for level after decrement post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_ambientChanges_afterManualChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value should be changed to the first level", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + sendAmbientBacklightValue(100) + assertNotEquals( + "Light value should not change based on ambient changes after manual changes", + Color.argb(100, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + private fun assertIncrementDecrementForLevels( - device: InputDevice, - light: Light, - expectedLevels: IntArray + device: InputDevice, + light: Light, + expectedLevels: IntArray ) { val deviceId = device.id val lightId = light.id @@ -612,6 +804,12 @@ class KeyboardBacklightControllerTests { testLooper.dispatchAll() } + private fun sendAmbientBacklightValue(brightnessValue: Int) { + keyboardBacklightController.handleAmbientLightValueChanged(brightnessValue) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchAll() + } + class KeyboardBacklightState( val deviceId: Int, val brightnessLevel: Int, @@ -620,12 +818,15 @@ class KeyboardBacklightControllerTests { ) private inner class KeyboardBacklightFlags constructor( - animationEnabled: Boolean, - customLevelsEnabled: Boolean + animationEnabled: Boolean, + customLevelsEnabled: Boolean, + ambientControlEnabled: Boolean ) : AutoCloseable { init { InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(animationEnabled) InputFeatureFlagProvider.setKeyboardBacklightCustomLevelsEnabled(customLevelsEnabled) + InputFeatureFlagProvider + .setAmbientKeyboardBacklightControlEnabled(ambientControlEnabled) } override fun close() { diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index e6c527be9b8f..70cd0dcf97d3 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -18,8 +18,6 @@ package com.android.server.power; import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_ERRORED; import static android.os.PowerManager.USER_ACTIVITY_EVENT_BUTTON; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; @@ -44,6 +42,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -51,13 +50,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManagerInternal; -import android.app.AppOpsManager; import android.attention.AttentionManagerInternal; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.AttributionSource; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; +import android.content.PermissionChecker; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.display.AmbientDisplayConfiguration; @@ -96,7 +96,6 @@ import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.PowerManagerService.BatteryReceiver; import com.android.server.power.PowerManagerService.BinderService; -import com.android.server.power.PowerManagerService.Injector; import com.android.server.power.PowerManagerService.NativeWrapper; import com.android.server.power.PowerManagerService.UserSwitchedReceiver; import com.android.server.power.PowerManagerService.WakeLock; @@ -106,9 +105,14 @@ import com.android.server.power.batterysaver.BatterySaverStateMachine; import com.android.server.power.batterysaver.BatterySavingStats; import com.android.server.testutils.OffsettableClock; +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; @@ -151,12 +155,13 @@ public class PowerManagerServiceTest { @Mock private WirelessChargerDetector mWirelessChargerDetectorMock; @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock; @Mock private SystemPropertiesWrapper mSystemPropertiesMock; - @Mock private AppOpsManager mAppOpsManagerMock; @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock; @Mock private Callable<Void> mInvalidateInteractiveCachesMock; + @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; + @Mock private PowerManagerService.PermissionCheckerWrapper mPermissionCheckerWrapperMock; + @Mock private PowerManagerService.PowerPropertiesWrapper mPowerPropertiesWrapper; - @Mock - private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; + @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); private PowerManagerService mService; private ContextWrapper mContextSpy; @@ -232,7 +237,7 @@ public class PowerManagerServiceTest { } private PowerManagerService createService() { - mService = new PowerManagerService(mContextSpy, new Injector() { + mService = new PowerManagerService(mContextSpy, new PowerManagerService.Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, SuspendBlocker suspendBlocker, WindowManagerPolicy policy, @@ -328,8 +333,13 @@ public class PowerManagerServiceTest { } @Override - AppOpsManager createAppOpsManager(Context context) { - return mAppOpsManagerMock; + PowerManagerService.PermissionCheckerWrapper createPermissionCheckerWrapper() { + return mPermissionCheckerWrapperMock; + } + + @Override + PowerManagerService.PowerPropertiesWrapper createPowerPropertiesWrapper() { + return mPowerPropertiesWrapper; } }); return mService; @@ -590,6 +600,7 @@ public class PowerManagerServiceTest { } @Test + @EnableCompatChanges({PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION}) public void testWakefulnessAwake_AcquireCausesWakeup_turnScreenOnAllowed() { createService(); startSystem(); @@ -598,11 +609,12 @@ public class PowerManagerServiceTest { IBinder token = new Binder(); String tag = "acq_causes_wakeup"; String packageName = "pkg.name"; - when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, - Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mContextSpy.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( - PackageManager.PERMISSION_GRANTED); + AttributionSource attrSrc = new AttributionSource(Binder.getCallingUid(), + packageName, /* attributionTag= */ null); + + doReturn(PermissionChecker.PERMISSION_GRANTED).when( + mPermissionCheckerWrapperMock).checkPermissionForDataDelivery(any(), + eq(android.Manifest.permission.TURN_SCREEN_ON), anyInt(), eq(attrSrc), anyString()); // First, ensure that a normal full wake lock does not cause a wakeup int flags = PowerManager.FULL_WAKE_LOCK; @@ -627,6 +639,35 @@ public class PowerManagerServiceTest { } @Test + @DisableCompatChanges({PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION}) + public void testWakefulnessAwake_AcquireCausesWakeupOldSdk_turnScreenOnAllowed() { + createService(); + startSystem(); + forceSleep(); + + IBinder token = new Binder(); + String tag = "acq_causes_wakeup"; + String packageName = "pkg.name"; + AttributionSource attrSrc = new AttributionSource(Binder.getCallingUid(), + packageName, /* attributionTag= */ null); + + // verify that the wakeup is allowed for apps targeting older sdks, and therefore won't have + // the TURN_SCREEN_ON permission granted + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + mPermissionCheckerWrapperMock).checkPermissionForDataDelivery(any(), + eq(android.Manifest.permission.TURN_SCREEN_ON), anyInt(), eq(attrSrc), anyString()); + + doReturn(false).when(mPowerPropertiesWrapper).waive_target_sdk_check_for_turn_screen_on(); + + int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; + mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, + null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); + } + + @Test + @EnableCompatChanges({PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION}) public void testWakefulnessAwake_AcquireCausesWakeup_turnScreenOnDenied() { createService(); startSystem(); @@ -635,30 +676,43 @@ public class PowerManagerServiceTest { IBinder token = new Binder(); String tag = "acq_causes_wakeup"; String packageName = "pkg.name"; - when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, - Binder.getCallingUid(), packageName)).thenReturn(MODE_ERRORED); + AttributionSource attrSrc = new AttributionSource(Binder.getCallingUid(), + packageName, /* attributionTag= */ null); + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + mPermissionCheckerWrapperMock).checkPermissionForDataDelivery(any(), + eq(android.Manifest.permission.TURN_SCREEN_ON), anyInt(), eq(attrSrc), anyString()); + doReturn(false).when(mPowerPropertiesWrapper).waive_target_sdk_check_for_turn_screen_on(); + doReturn(false).when(mPowerPropertiesWrapper).permissionless_turn_screen_on(); - // Verify that flag has no effect when OP_TURN_SCREEN_ON is not allowed + // Verify that flag has no effect when TURN_SCREEN_ON is not allowed for apps targeting U+ int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); - } else { - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); - } + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); + } + + @Test + @EnableCompatChanges({PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION}) + public void testWakefulnessAwake_AcquireCausesWakeupOldSdk_turnScreenOnDenied() { + createService(); + startSystem(); + forceSleep(); + + IBinder token = new Binder(); + String tag = "acq_causes_wakeup"; + String packageName = "pkg.name"; + AttributionSource attrSrc = new AttributionSource(Binder.getCallingUid(), + packageName, /* attributionTag= */ null); + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when( + mPermissionCheckerWrapperMock).checkPermissionForDataDelivery(any(), + eq(android.Manifest.permission.TURN_SCREEN_ON), anyInt(), eq(attrSrc), anyString()); - when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, - Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mContextSpy.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( - PackageManager.PERMISSION_DENIED); + doReturn(true).when(mPowerPropertiesWrapper).waive_target_sdk_check_for_turn_screen_on(); - // Verify that the flag has no effect when OP_TURN_SCREEN_ON is allowed but - // android.permission.TURN_SCREEN_ON is denied - flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; + // Verify that flag has no effect when TURN_SCREEN_ON is not allowed for apps targeting U+ + int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { diff --git a/services/tests/servicestests/src/com/android/server/webkit/OWNERS b/services/tests/servicestests/src/com/android/server/webkit/OWNERS index 00e540a46ab2..e7fd7a5d1096 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/OWNERS +++ b/services/tests/servicestests/src/com/android/server/webkit/OWNERS @@ -1,3 +1,3 @@ -changwan@google.com -tobiasjs@google.com +# Bug component: 76427 +ntfschr@google.com torne@google.com diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml index f44c1d18614d..4315254f68a9 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -20,6 +20,7 @@ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <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.UPDATE_DEVICE_STATS" /> <uses-permission android:name="android.permission.MANAGE_USERS" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" /> @@ -36,6 +37,7 @@ <uses-permission android:name="android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG" /> <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> 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 8f7f2f66308b..4debbb4d38c1 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -61,6 +61,7 @@ import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.O_MR1; import static android.os.Build.VERSION_CODES.P; +import static android.os.PowerManager.PARTIAL_WAKE_LOCK; import static android.os.UserHandle.USER_SYSTEM; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; @@ -80,6 +81,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ALLOW_DISMISS_ONGOING; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI; +import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.WAKE_LOCK_FOR_POSTING_NOTIFICATION; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED; @@ -120,12 +122,14 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import android.Manifest; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -182,11 +186,14 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; +import android.os.WorkSource; import android.permission.PermissionManager; import android.provider.DeviceConfig; import android.provider.MediaStore; @@ -352,6 +359,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private PermissionManager mPermissionManager; @Mock private DevicePolicyManagerInternal mDevicePolicyManager; + @Mock + private PowerManager mPowerManager; + private final ArrayList<WakeLock> mAcquiredWakeLocks = new ArrayList<>(); private final TestPostNotificationTrackerFactory mPostNotificationTrackerFactory = new TestPostNotificationTrackerFactory(); @@ -432,8 +442,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private final List<PostNotificationTracker> mCreatedTrackers = new ArrayList<>(); @Override - public PostNotificationTracker newTracker() { - PostNotificationTracker tracker = PostNotificationTrackerFactory.super.newTracker(); + public PostNotificationTracker newTracker(@Nullable WakeLock optionalWakeLock) { + PostNotificationTracker tracker = PostNotificationTrackerFactory.super.newTracker( + optionalWakeLock); mCreatedTrackers.add(tracker); return tracker; } @@ -564,6 +575,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mAssistants.isAdjustmentAllowed(anyString())).thenReturn(true); + // Use the real PowerManager to back up the mock w.r.t. creating WakeLocks. + // This is because 1) we need a mock to verify() calls and tracking the created WakeLocks, + // but 2) PowerManager and WakeLock perform their own checks (e.g. correct arguments, don't + // call release twice, etc) and we want the test to fail if such misuse happens, too. + PowerManager realPowerManager = mContext.getSystemService(PowerManager.class); + when(mPowerManager.newWakeLock(anyInt(), anyString())).then( + (Answer<WakeLock>) invocation -> { + WakeLock wl = realPowerManager.newWakeLock(invocation.getArgument(0), + invocation.getArgument(1)); + mAcquiredWakeLocks.add(wl); + return wl; + }); + mTestFlagResolver.setFlagOverride(WAKE_LOCK_FOR_POSTING_NOTIFICATION, true); + // apps allowed as convos mService.setStringArrayResourceValue(PKG_O); @@ -580,7 +605,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mock(TelephonyManager.class), mAmi, mToastRateLimiter, mPermissionHelper, mock(UsageStatsManagerInternal.class), mTelecomManager, mLogger, mTestFlagResolver, mPermissionManager, - mPostNotificationTrackerFactory); + mPowerManager, mPostNotificationTrackerFactory); // Return first true for RoleObserver main-thread check when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false); mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper); @@ -688,6 +713,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @After + public void assertAllWakeLocksReleased() { + for (WakeLock wakeLock : mAcquiredWakeLocks) { + assertThat(wakeLock.isHeld()).isFalse(); + } + } + + @After public void tearDown() throws Exception { if (mFile != null) mFile.delete(); clearDeviceConfig(); @@ -1488,7 +1520,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -1509,7 +1541,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -1805,6 +1837,112 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void enqueueNotification_acquiresAndReleasesWakeLock() throws Exception { + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_acquiresAndReleasesWakeLock", 0, + generateNotificationRecord(null).getNotification(), 0); + + verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isTrue(); + + waitForIdle(); + + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isFalse(); + } + + @Test + public void enqueueNotification_throws_acquiresAndReleasesWakeLock() throws Exception { + // Simulate not enqueued due to rejected inputs. + assertThrows(Exception.class, + () -> mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_throws_acquiresAndReleasesWakeLock", 0, + /* notification= */ null, 0)); + + verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isFalse(); + } + + @Test + public void enqueueNotification_notEnqueued_acquiresAndReleasesWakeLock() throws Exception { + // Simulate not enqueued due to snoozing inputs. + when(mSnoozeHelper.getSnoozeContextForUnpostedNotification(anyInt(), any(), any())) + .thenReturn("zzzzzzz"); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_notEnqueued_acquiresAndReleasesWakeLock", 0, + generateNotificationRecord(null).getNotification(), 0); + + verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isTrue(); + + waitForIdle(); + + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isFalse(); + } + + @Test + public void enqueueNotification_notPosted_acquiresAndReleasesWakeLock() throws Exception { + // Simulate enqueued but not posted due to missing small icon. + Notification notif = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setContentTitle("foo") + .build(); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_notPosted_acquiresAndReleasesWakeLock", 0, + notif, 0); + + verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isTrue(); + + waitForIdle(); + + // NLSes were not called. + verify(mListeners, never()).prepareNotifyPostedLocked(any(), any(), anyBoolean()); + + assertThat(mAcquiredWakeLocks).hasSize(1); + assertThat(mAcquiredWakeLocks.get(0).isHeld()).isFalse(); + } + + @Test + public void enqueueNotification_setsWakeLockWorkSource() throws Exception { + // Use a "full" mock for the PowerManager (instead of the one that delegates to the real + // service) so we can return a mocked WakeLock that we can verify() on. + reset(mPowerManager); + WakeLock wakeLock = mock(WakeLock.class); + when(mPowerManager.newWakeLock(anyInt(), anyString())).thenReturn(wakeLock); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_setsWakeLockWorkSource", 0, + generateNotificationRecord(null).getNotification(), 0); + waitForIdle(); + + InOrder inOrder = inOrder(mPowerManager, wakeLock); + inOrder.verify(mPowerManager).newWakeLock(eq(PARTIAL_WAKE_LOCK), anyString()); + inOrder.verify(wakeLock).setWorkSource(eq(new WorkSource(mUid, PKG))); + inOrder.verify(wakeLock).acquire(anyLong()); + inOrder.verify(wakeLock).release(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void enqueueNotification_wakeLockFlagOff_noWakeLock() throws Exception { + mTestFlagResolver.setFlagOverride(WAKE_LOCK_FOR_POSTING_NOTIFICATION, false); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "enqueueNotification_setsWakeLockWorkSource", 0, + generateNotificationRecord(null).getNotification(), 0); + waitForIdle(); + + verifyZeroInteractions(mPowerManager); + } + + @Test public void testCancelNonexistentNotification() throws Exception { mBinderService.cancelNotificationWithTag(PKG, PKG, "testCancelNonexistentNotification", 0, 0); @@ -4363,7 +4501,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -4382,7 +4520,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(update.getKey(), update.getSbn().getPackageName(), update.getUid(), - mPostNotificationTrackerFactory.newTracker()); + mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -4402,7 +4540,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(update.getKey(), update.getSbn().getPackageName(), update.getUid(), - mPostNotificationTrackerFactory.newTracker()); + mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -4422,7 +4560,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(update.getKey(), update.getSbn().getPackageName(), - update.getUid(), mPostNotificationTrackerFactory.newTracker()); + update.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -4436,13 +4574,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); r = generateNotificationRecord(mTestNotificationChannel, 1, null, false); r.setCriticality(CriticalNotificationExtractor.CRITICAL); runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); mService.addEnqueuedNotification(r); runnable.run(); @@ -5092,7 +5230,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.new PostNotificationRunnable(original.getKey(), original.getSbn().getPackageName(), original.getUid(), - mPostNotificationTrackerFactory.newTracker()); + mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -5116,7 +5254,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.new PostNotificationRunnable(update.getKey(), update.getSbn().getPackageName(), update.getUid(), - mPostNotificationTrackerFactory.newTracker()); + mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -7538,7 +7676,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(update.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -10236,7 +10374,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); NotificationManagerService.PostNotificationRunnable runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -10253,7 +10391,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -10270,7 +10408,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); runnable = mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker()); + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); runnable.run(); waitForIdle(); @@ -10363,7 +10501,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // normal blocked notifications - blocked mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats).registerBlocked(any()); @@ -10381,7 +10519,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats).registerBlocked(any()); @@ -10394,7 +10532,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats, never()).registerBlocked(any()); @@ -10408,7 +10546,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats, never()).registerBlocked(any()); @@ -10422,7 +10560,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats).registerBlocked(any()); @@ -10437,7 +10575,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats).registerBlocked(any()); @@ -10450,7 +10588,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addEnqueuedNotification(r); mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), r.getUid(), - mPostNotificationTrackerFactory.newTracker()).run(); + mPostNotificationTrackerFactory.newTracker(null)).run(); waitForIdle(); verify(mUsageStats).registerBlocked(any()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java index 66c1e35754c5..81c573d8fb1e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java @@ -48,6 +48,7 @@ import android.content.Context; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.os.Looper; +import android.os.PowerManager; import android.os.UserHandle; import android.os.UserManager; import android.permission.PermissionManager; @@ -169,6 +170,7 @@ public class RoleObserverTest extends UiServiceTestCase { mock(UsageStatsManagerInternal.class), mock(TelecomManager.class), mock(NotificationChannelLogger.class), new TestableFlagResolver(), mock(PermissionManager.class), + mock(PowerManager.class), new NotificationManagerService.PostNotificationTrackerFactory() {}); } catch (SecurityException e) { if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 77e944f35cb2..41fcd6935567 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1996,7 +1996,8 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(activity.isSnapshotCompatible(snapshot)); - setRotatedScreenOrientationSilently(activity); + doReturn(task.getWindowConfiguration().getRotation() + 1).when(mDisplayContent) + .rotationForActivityInDifferentOrientation(activity); assertFalse(activity.isSnapshotCompatible(snapshot)); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index 4890f3e6cbf1..bcb0c6b5c269 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -250,9 +250,22 @@ public class ActivityStartInterceptorTest { } @Test - public void testInterceptQuietProfile() { - // GIVEN that the user the activity is starting as is currently in quiet mode + public void testInterceptQuietProfile_keepProfilesRunningEnabled() { + // GIVEN that the user the activity is starting as is currently in quiet mode and + // profiles are kept running when in quiet mode. when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); + when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); + + // THEN calling intercept returns false because package also has to be suspended. + assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + } + + @Test + public void testInterceptQuietProfile_keepProfilesRunningDisabled() { + // GIVEN that the user the activity is starting as is currently in quiet mode and + // profiles are stopped when in quiet mode (pre-U behavior, no profile app suspension). + when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); + when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); // THEN calling intercept returns true assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); @@ -263,10 +276,28 @@ public class ActivityStartInterceptorTest { } @Test - public void testInterceptQuietProfileWhenPackageSuspended() { + public void testInterceptQuietProfileWhenPackageSuspended_keepProfilesRunningEnabled() { + // GIVEN that the user the activity is starting as is currently in quiet mode, + // the package is suspended and profiles are kept running while in quiet mode. + suspendPackage("com.test.suspending.package"); + when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); + when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); + + // THEN calling intercept returns true + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); + + // THEN the returned intent is the quiet mode intent + assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) + .filterEquals(mInterceptor.mIntent)); + } + + @Test + public void testInterceptQuietProfileWhenPackageSuspended_keepProfilesRunningDisabled() { + // GIVEN that the user the activity is starting as is currently in quiet mode, + // the package is suspended and profiles are stopped while in quiet mode. suspendPackage("com.test.suspending.package"); - // GIVEN that the user the activity is starting as is currently in quiet mode when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); + when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); // THEN calling intercept returns true assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null)); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 04e1d9c07a07..2a8f0ffc4d49 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -59,10 +59,10 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; import android.os.IBinder; import android.os.PowerManagerInternal; import android.os.SystemClock; -import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.view.DisplayAddress; @@ -518,7 +518,8 @@ public class DisplayRotationTests { mBuilder.build(); configureDisplayRotation(SCREEN_ORIENTATION_PORTRAIT, false, false); - when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()).thenReturn(true); + when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mMockDisplayContent)) + .thenReturn(true); thawRotation(); @@ -544,7 +545,8 @@ public class DisplayRotationTests { @Test public void testFreezeRotation_reverseRotationDirectionAroundZAxis_yes() throws Exception { mBuilder.build(); - when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()).thenReturn(true); + when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mMockDisplayContent)) + .thenReturn(true); freezeRotation(Surface.ROTATION_90); assertEquals(Surface.ROTATION_270, mTarget.getUserRotation()); @@ -553,7 +555,8 @@ public class DisplayRotationTests { @Test public void testFreezeRotation_reverseRotationDirectionAroundZAxis_no() throws Exception { mBuilder.build(); - when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()).thenReturn(false); + when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mMockDisplayContent)) + .thenReturn(false); freezeRotation(Surface.ROTATION_90); assertEquals(Surface.ROTATION_90, mTarget.getUserRotation()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index d91be16f2538..27e6e31ec152 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -407,7 +407,6 @@ public class SizeCompatTests extends WindowTestsBase { clearInvocations(translucentActivity.mLetterboxUiController); // We destroy the first opaque activity - mActivity.setState(DESTROYED, "testing"); mActivity.removeImmediately(); // Check that updateInheritedLetterbox() is invoked again @@ -4655,14 +4654,6 @@ public class SizeCompatTests extends WindowTestsBase { return c; } - private static void resizeDisplay(DisplayContent displayContent, int width, int height) { - displayContent.updateBaseDisplayMetrics(width, height, displayContent.mBaseDisplayDensity, - displayContent.mBaseDisplayPhysicalXDpi, displayContent.mBaseDisplayPhysicalYDpi); - final Configuration c = new Configuration(); - displayContent.computeScreenConfiguration(c); - displayContent.onRequestedOverrideConfigurationChanged(c); - } - private static void setNeverConstrainDisplayApisFlag(@Nullable String value, boolean makeDefault) { DeviceConfig.setProperty(NAMESPACE_CONSTRAIN_DISPLAY_APIS, diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 01ddcca99300..f3d8114b9e94 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -127,6 +127,10 @@ public class WallpaperControllerTests extends WindowTestsBase { public void testWallpaperSizeWithFixedTransform() { // No wallpaper final DisplayContent dc = mDisplayContent; + if (dc.mBaseDisplayHeight == dc.mBaseDisplayWidth) { + // Make sure the size is different when changing orientation. + resizeDisplay(dc, 500, 1000); + } // No wallpaper WSA Surface final WindowState wallpaperWindow = createWallpaperWindow(dc); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 68640fc0eae8..5b7b1b297a1d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -53,6 +53,7 @@ import static com.google.common.truth.Truth.assertThat; 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.assertThrows; import static org.junit.Assert.assertTrue; @@ -100,6 +101,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.AdoptShellPermissionsRule; +import com.android.internal.os.IResultReceiver; import org.junit.Rule; import org.junit.Test; @@ -913,6 +915,56 @@ public class WindowManagerServiceTests extends WindowTestsBase { argThat(h -> (h.inputConfig & InputConfig.SPY) == InputConfig.SPY)); } + @Test + public void testRequestKeyboardShortcuts_noWindow() { + doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString()); + doReturn(null).when(mWm).getFocusedWindowLocked(); + doReturn(null).when(mWm.mRoot).getCurrentInputMethodWindow(); + + TestResultReceiver receiver = new TestResultReceiver(); + mWm.requestAppKeyboardShortcuts(receiver, 0); + assertNotNull(receiver.resultData); + assertTrue(receiver.resultData.isEmpty()); + + receiver = new TestResultReceiver(); + mWm.requestImeKeyboardShortcuts(receiver, 0); + assertNotNull(receiver.resultData); + assertTrue(receiver.resultData.isEmpty()); + } + + @Test + public void testRequestKeyboardShortcuts() throws RemoteException { + final IWindow window = mock(IWindow.class); + final IBinder binder = mock(IBinder.class); + doReturn(binder).when(window).asBinder(); + final WindowState windowState = + createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "appWin", window); + doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString()); + doReturn(windowState).when(mWm).getFocusedWindowLocked(); + doReturn(windowState).when(mWm.mRoot).getCurrentInputMethodWindow(); + + TestResultReceiver receiver = new TestResultReceiver(); + mWm.requestAppKeyboardShortcuts(receiver, 0); + mWm.requestImeKeyboardShortcuts(receiver, 0); + verify(window, times(2)).requestAppKeyboardShortcuts(receiver, 0); + } + + class TestResultReceiver implements IResultReceiver { + public android.os.Bundle resultData; + private final IBinder mBinder = mock(IBinder.class); + + @Override + public void send(int resultCode, android.os.Bundle resultData) + throws android.os.RemoteException { + this.resultData = resultData; + } + + @Override + public android.os.IBinder asBinder() { + return mBinder; + } + } + private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); when(remoteToken.toWindowContainerToken()).thenReturn(wct); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ddc729f773b2..be8ee7832a5d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -71,6 +71,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -946,6 +947,14 @@ class WindowTestsBase extends SystemServiceTestsBase { dc.setRotationAnimation(null); } + static void resizeDisplay(DisplayContent displayContent, int width, int height) { + displayContent.updateBaseDisplayMetrics(width, height, displayContent.mBaseDisplayDensity, + displayContent.mBaseDisplayPhysicalXDpi, displayContent.mBaseDisplayPhysicalYDpi); + final Configuration c = new Configuration(); + displayContent.computeScreenConfiguration(c); + displayContent.onRequestedOverrideConfigurationChanged(c); + } + // The window definition for UseTestDisplay#addWindows. The test can declare to add only // necessary windows, that avoids adding unnecessary overhead of unused windows. static final int W_NOTIFICATION_SHADE = TYPE_NOTIFICATION_SHADE; diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java index a834e2bbd0d1..b5c1d7d9e5f4 100644 --- a/telephony/java/android/telephony/data/DataCallResponse.java +++ b/telephony/java/android/telephony/data/DataCallResponse.java @@ -461,10 +461,6 @@ public final class DataCallResponse implements Parcelable { DataCallResponse other = (DataCallResponse) o; - final boolean isQosSame = (mDefaultQos == null || other.mDefaultQos == null) - ? mDefaultQos == other.mDefaultQos - : mDefaultQos.equals(other.mDefaultQos); - final boolean isQosBearerSessionsSame = (mQosBearerSessions == null || other.mQosBearerSessions == null) ? mQosBearerSessions == other.mQosBearerSessions @@ -496,7 +492,7 @@ public final class DataCallResponse implements Parcelable { && mMtuV6 == other.mMtuV6 && mHandoverFailureMode == other.mHandoverFailureMode && mPduSessionId == other.mPduSessionId - && isQosSame + && Objects.equals(mDefaultQos, other.mDefaultQos) && isQosBearerSessionsSame && Objects.equals(mSliceInfo, other.mSliceInfo) && isTrafficDescriptorsSame; @@ -557,15 +553,7 @@ public final class DataCallResponse implements Parcelable { dest.writeInt(mMtuV6); dest.writeInt(mHandoverFailureMode); dest.writeInt(mPduSessionId); - if (mDefaultQos != null) { - if (mDefaultQos.getType() == Qos.QOS_TYPE_EPS) { - dest.writeParcelable((EpsQos) mDefaultQos, flags); - } else { - dest.writeParcelable((NrQos) mDefaultQos, flags); - } - } else { - dest.writeParcelable(null, flags); - } + dest.writeParcelable(mDefaultQos, flags); dest.writeList(mQosBearerSessions); dest.writeParcelable(mSliceInfo, flags); dest.writeList(mTrafficDescriptors); diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt new file mode 100644 index 000000000000..00316ea249b7 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -0,0 +1,140 @@ +/* + * 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.server.wm.flicker.activityembedding + +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized +import android.tools.common.datatypes.Rect + +/** + * Test launching an activity with AlwaysExpand rule. + * + * Setup: Launch A|B in split with B being the secondary activity. + * Transitions: + * A start C with alwaysExpand=true, expect C to launch in fullscreen and cover split A|B. + * + * To run this test: `atest FlickerTests:MainActivityStartsSecondaryWithAlwaysExpandTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: FlickerTest) : + ActivityEmbeddingTestBase(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit = { + setup { + tapl.setExpectedRotationCheckEnabled(false) + // Launch a split + testApp.launchViaIntent(wmHelper) + testApp.launchSecondaryActivity(wmHelper) + startDisplayBounds = + wmHelper.currentState.layerState.physicalDisplayBounds ?: error("Display not found") + } + transitions { + // Launch C with alwaysExpand + testApp.launchAlwaysExpandActivity(wmHelper) + } + teardown { + tapl.goHome() + testApp.exit(wmHelper) + } + } + + /** Transition begins with a split. */ + @Presubmit + @Test + fun startsWithSplit() { + flicker.assertWmStart { + this.isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + } + flicker.assertWmStart { + this.isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + + /** Main activity should become invisible after being covered by always expand activity. */ + @Presubmit + @Test + fun mainActivityLayerBecomesInvisible() { + flicker.assertLayers { + isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .then() + .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + } + } + + /** Secondary activity should become invisible after being covered by always expand activity. */ + @Presubmit + @Test + fun secondaryActivityLayerBecomesInvisible() { + flicker.assertLayers { + isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + } + + /** At the end of transition always expand activity is in fullscreen. */ + @Presubmit + @Test + fun endsWithAlwaysExpandActivityCoveringFullScreen() { + flicker.assertWmEnd { + this.visibleRegion(ActivityEmbeddingAppHelper.ALWAYS_EXPAND_ACTIVITY_COMPONENT) + .coversExactly(startDisplayBounds) + } + } + + /** Always expand activity is on top of the split. */ + @Presubmit + @Test + fun endsWithAlwaysExpandActivityOnTop() { + flicker.assertWmEnd { + this.isAppWindowOnTop( + ActivityEmbeddingAppHelper.ALWAYS_EXPAND_ACTIVITY_COMPONENT) + } + } + + companion object { + /** {@inheritDoc} */ + private var startDisplayBounds = Rect.EMPTY + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} + diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt index ed17059e79e7..ed17059e79e7 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt index 863828881d36..863828881d36 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index e019b2b22680..a21965e0d7d5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -75,6 +75,29 @@ constructor( .StateSyncBuilder() .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) .waitForAndVerify() + } + + /** + * Clicks the button to launch a secondary activity with alwaysExpand enabled, which will launch + * a fullscreen window on top of the visible region. + */ + fun launchAlwaysExpandActivity(wmHelper: WindowManagerStateHelper) { + val launchButton = + uiDevice.wait( + Until.findObject( + By.res(getPackage(), + "launch_always_expand_activity_button")), + FIND_TIMEOUT + ) + require(launchButton != null) { + "Can't find launch always expand activity button on screen." + } + launchButton.click() + wmHelper + .StateSyncBuilder() + .withActivityState(ALWAYS_EXPAND_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_PAUSED) + .waitForAndVerify() } /** @@ -105,6 +128,9 @@ constructor( val SECONDARY_ACTIVITY_COMPONENT = ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT.toFlickerComponent() + val ALWAYS_EXPAND_ACTIVITY_COMPONENT = + ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT.toFlickerComponent() + val PLACEHOLDER_PRIMARY_COMPONENT = ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT .toFlickerComponent() diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 1ec9ec9b0eda..dc9ff3b01822 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -198,6 +198,13 @@ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"/> <activity + android:name=".ActivityEmbeddingAlwaysExpandActivity" + android:label="ActivityEmbedding AlwaysExpand" + android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" + android:theme="@style/CutoutShortEdges" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" + android:exported="false"/> + <activity android:name=".ActivityEmbeddingPlaceholderPrimaryActivity" android:label="ActivityEmbedding Placeholder Primary" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml index d78b9a836a37..f5241cae8fa8 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml @@ -37,4 +37,12 @@ android:onClick="launchPlaceholderSplit" android:text="Launch Placeholder Split" /> + <Button + android:id="@+id/launch_always_expand_activity_button" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_centerHorizontal="true" + android:onClick="launchAlwaysExpandActivity" + android:text="Launch Always Expand Activity" /> + </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java new file mode 100644 index 000000000000..d9b24ed23424 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java @@ -0,0 +1,33 @@ +/* + * 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.server.wm.flicker.testapp; + +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; + +/** + * Activity with alwaysExpand=true (launched via R.id.launch_always_expand_activity_button) + */ +public class ActivityEmbeddingAlwaysExpandActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_embedding_base_layout); + findViewById(R.id.root_activity_layout).setBackgroundColor(Color.GREEN); + } + +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java index 6a7a2ccd3378..61202545f407 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java @@ -23,6 +23,9 @@ import android.util.ArraySet; import android.util.Log; import android.view.View; +import androidx.window.embedding.ActivityFilter; +import androidx.window.embedding.ActivityRule; +import androidx.window.embedding.RuleController; import androidx.window.extensions.embedding.ActivityEmbeddingComponent; import androidx.window.extensions.embedding.EmbeddingRule; import androidx.window.extensions.embedding.SplitPairRule; @@ -30,6 +33,7 @@ import androidx.window.extensions.embedding.SplitPlaceholderRule; import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper; +import java.util.HashSet; import java.util.Set; /** Main activity of the ActivityEmbedding test app to launch other embedding activities. */ @@ -50,6 +54,23 @@ public class ActivityEmbeddingMainActivity extends Activity { ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT)); } + /** R.id.launch_always_expand_activity_button onClick */ + public void launchAlwaysExpandActivity(View view) { + final Set<ActivityFilter> activityFilters = new HashSet<>(); + activityFilters.add( + new ActivityFilter(ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT, + null)); + final ActivityRule activityRule = new ActivityRule.Builder(activityFilters) + .setAlwaysExpand(true) + .build(); + + RuleController rc = RuleController.getInstance(this); + + rc.addRule(activityRule); + startActivity(new Intent().setComponent( + ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT)); + } + /** R.id.launch_placeholder_split_button onClick */ public void launchPlaceholderSplit(View view) { initializeSplitRules(createSplitPlaceholderRules()); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index 9c3226b5292c..0f5c003f12fd 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -99,6 +99,12 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".ActivityEmbeddingSecondaryActivity"); } + public static class AlwaysExpandActivity { + public static final String LABEL = "ActivityEmbeddingAlwaysExpandActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".ActivityEmbeddingAlwaysExpandActivity"); + } + public static class PlaceholderPrimaryActivity { public static final String LABEL = "ActivityEmbeddingPlaceholderPrimaryActivity"; public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, diff --git a/tests/InputMethodStressTest/Android.bp b/tests/InputMethodStressTest/Android.bp index 27640a5f51bc..84845c69fb27 100644 --- a/tests/InputMethodStressTest/Android.bp +++ b/tests/InputMethodStressTest/Android.bp @@ -30,7 +30,6 @@ android_test { ], test_suites: [ "general-tests", - "vts", ], data: [ ":SimpleTestIme", diff --git a/tests/InputMethodStressTest/AndroidTest.xml b/tests/InputMethodStressTest/AndroidTest.xml index bedf0990a188..bfebb42ad244 100644 --- a/tests/InputMethodStressTest/AndroidTest.xml +++ b/tests/InputMethodStressTest/AndroidTest.xml @@ -19,6 +19,7 @@ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="setprop debug.wm.disable_deprecated_abi_dialog 1" /> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" /> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" /> </target_preparer> diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java index 3d257b29287f..807f0c63668c 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java @@ -199,8 +199,7 @@ public final class AutoShowTest { Collections.singletonList(REQUEST_FOCUS_ON_CREATE)); TestActivity firstActivity = TestActivity.start(intent1); // Show Ime with InputMethodManager to ensure the keyboard is on. - boolean succ = callOnMainSync(firstActivity::showImeWithInputMethodManager); - assertThat(succ).isTrue(); + callOnMainSync(firstActivity::showImeWithInputMethodManager); SystemClock.sleep(1000); mInstrumentation.waitForIdleSync(); @@ -264,8 +263,7 @@ public final class AutoShowTest { Collections.singletonList(REQUEST_FOCUS_ON_CREATE)); ImeStressTestUtil.TestActivity secondActivity = activity.startSecondTestActivity(intent2); // Show Ime with InputMethodManager to ensure the keyboard is shown on the second activity - boolean succ = callOnMainSync(secondActivity::showImeWithInputMethodManager); - assertThat(succ).isTrue(); + callOnMainSync(secondActivity::showImeWithInputMethodManager); SystemClock.sleep(1000); mInstrumentation.waitForIdleSync(); // Close the second activity diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java index 299cbf1a84c7..0c267b27490b 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java @@ -26,8 +26,6 @@ import static com.android.inputmethod.stresstest.ImeStressTestUtil.verifyWindowA import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsHidden; import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsShown; -import static com.google.common.truth.Truth.assertThat; - import android.content.Intent; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; @@ -83,14 +81,11 @@ public final class DefaultImeVisibilityTest { ImeStressTestUtil.TestActivity activity = ImeStressTestUtil.TestActivity.start(intent); EditText editText = activity.getEditText(); for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { - - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isTrue(); + callOnMainSync(activity::showImeWithInputMethodManager); verifyWindowAndViewFocus(editText, true, true); waitOnMainUntilImeIsShown(editText); - boolean hideResult = callOnMainSync(activity::hideImeWithInputMethodManager); - assertThat(hideResult).isTrue(); + callOnMainSync(activity::hideImeWithInputMethodManager); waitOnMainUntilImeIsHidden(editText); } } diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java index b76a4eb8c0e6..5c0212400ff1 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java @@ -102,12 +102,10 @@ public final class ImeOpenCloseStressTest { for (int i = 0; i < iterNum; i++) { String msgPrefix = "Iteration #" + i + " "; Log.i(TAG, msgPrefix + "start"); - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isEqualTo(!(hasUnfocusableWindowFlags(activity))); + callOnMainSync(activity::showImeWithInputMethodManager); verifyShowBehavior(activity); - boolean hideResult = callOnMainSync(activity::hideImeWithInputMethodManager); - assertThat(hideResult).isEqualTo(!(hasUnfocusableWindowFlags(activity))); + callOnMainSync(activity::hideImeWithInputMethodManager); verifyHideBehavior(activity); } @@ -128,14 +126,12 @@ public final class ImeOpenCloseStressTest { for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { String msgPrefix = "Iteration #" + i + " "; Log.i(TAG, msgPrefix + "start"); - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isTrue(); + callOnMainSync(activity::showImeWithInputMethodManager); waitOnMainUntil( msgPrefix + "IME should be visible", () -> !activity.isAnimating() && isImeShown(editText)); - boolean hideResult = callOnMainSync(activity::hideImeWithInputMethodManager); - assertThat(hideResult).isTrue(); + callOnMainSync(activity::hideImeWithInputMethodManager); waitOnMainUntil( msgPrefix + "IME should be hidden", () -> !activity.isAnimating() && !isImeShown(editText)); @@ -156,17 +152,13 @@ public final class ImeOpenCloseStressTest { List<Integer> intervals = new ArrayList<>(); for (int i = 10; i < 100; i += 10) intervals.add(i); for (int i = 100; i < 1000; i += 50) intervals.add(i); - boolean firstHide = false; for (int intervalMillis : intervals) { String msgPrefix = "Interval = " + intervalMillis + " "; Log.i(TAG, msgPrefix + " start"); - boolean hideResult = callOnMainSync(activity::hideImeWithInputMethodManager); - assertThat(hideResult).isEqualTo(firstHide); - firstHide = true; + callOnMainSync(activity::hideImeWithInputMethodManager); SystemClock.sleep(intervalMillis); - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isTrue(); + callOnMainSync(activity::showImeWithInputMethodManager); verifyShowBehavior(activity); } } @@ -247,8 +239,7 @@ public final class ImeOpenCloseStressTest { TestActivity activity = TestActivity.start(intent); // Show InputMethodManager without requesting focus - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isFalse(); + callOnMainSync(activity::showImeWithInputMethodManager); int windowFlags = activity.getWindow().getAttributes().flags; EditText editText = activity.getEditText(); @@ -474,8 +465,7 @@ public final class ImeOpenCloseStressTest { Collections.singletonList(REQUEST_FOCUS_ON_CREATE)); TestActivity activity = TestActivity.start(intent1); // Show Ime with InputMethodManager to ensure the keyboard is shown on the second activity - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isEqualTo(!(hasUnfocusableWindowFlags(activity))); + callOnMainSync(activity::showImeWithInputMethodManager); Thread.sleep(1000); verifyShowBehavior(activity); @@ -503,8 +493,7 @@ public final class ImeOpenCloseStressTest { Collections.singletonList(REQUEST_FOCUS_ON_CREATE)); TestActivity activity = TestActivity.start(intent); // Show Ime with InputMethodManager to ensure the keyboard is shown on the second activity - boolean showResult = callOnMainSync(activity::showImeWithInputMethodManager); - assertThat(showResult).isEqualTo(!(hasUnfocusableWindowFlags(activity))); + callOnMainSync(activity::showImeWithInputMethodManager); Thread.sleep(2000); verifyShowBehavior(activity); diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp index 9328b67795cb..c60c519ec4d4 100644 --- a/tests/UsbTests/Android.bp +++ b/tests/UsbTests/Android.bp @@ -29,7 +29,7 @@ android_test { static_libs: [ "frameworks-base-testutils", "androidx.test.rules", - "mockito-target-inline-minus-junit4", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "services.core", "services.net", @@ -37,7 +37,12 @@ android_test { "truth-prebuilt", "UsbManagerTestLib", ], - jni_libs: ["libdexmakerjvmtiagent"], + jni_libs: [ + // Required for ExtendedMockito + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", + ], certificate: "platform", platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java index 210e3ea2a9b2..c2d0f7c05b91 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,23 +28,29 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.debug.AdbManagerInternal; +import android.debug.AdbTransportType; import android.hardware.usb.UsbManager; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.provider.Settings; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.FgThread; +import com.android.server.LocalServices; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import java.util.HashMap; import java.util.Locale; @@ -68,6 +75,8 @@ public class UsbHandlerTest { private SharedPreferences mSharedPreferences; @Mock private SharedPreferences.Editor mEditor; + @Mock + private AdbManagerInternal mAdbManagerInternal; private MockUsbHandler mUsbHandler; @@ -83,6 +92,7 @@ public class UsbHandlerTest { private Map<String, String> mMockProperties; private Map<String, Integer> mMockGlobalSettings; + private MockitoSession mStaticMockSession; private class MockUsbHandler extends UsbDeviceManager.UsbHandler { boolean mIsUsbTransferAllowed; @@ -157,6 +167,10 @@ public class UsbHandlerTest { @Before public void before() { MockitoAnnotations.initMocks(this); + mStaticMockSession = ExtendedMockito.mockitoSession() + .mockStatic(LocalServices.class) + .strictness(Strictness.WARN) + .startMocking(); mMockProperties = new HashMap<>(); mMockGlobalSettings = new HashMap<>(); when(mSharedPreferences.edit()).thenReturn(mEditor); @@ -164,6 +178,16 @@ public class UsbHandlerTest { mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, mUsbSettingsManager, mUsbPermissionManager); + + when(LocalServices.getService(eq(AdbManagerInternal.class))) + .thenReturn(mAdbManagerInternal); + } + + @After + public void tearDown() throws Exception { + if (mStaticMockSession != null) { + mStaticMockSession.finishMocking(); + } } @SmallTest @@ -234,8 +258,8 @@ public class UsbHandlerTest { assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); assertEquals(mMockProperties.get(UsbDeviceManager.UsbHandler .USB_PERSISTENT_CONFIG_PROPERTY), UsbManager.USB_FUNCTION_ADB); - assertTrue(mUsbHandler.isAdbEnabled()); + when(mAdbManagerInternal.isAdbEnabled(eq(AdbTransportType.USB))).thenReturn(true); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_STATE, 1, 1)); assertTrue(mUsbHandler.mBroadcastedIntent.getBooleanExtra(UsbManager.USB_CONNECTED, false)); @@ -271,20 +295,6 @@ public class UsbHandlerTest { @SmallTest @Test - public void bootCompletedAdbEnabled() { - mMockProperties.put(UsbDeviceManager.UsbHandler.USB_PERSISTENT_CONFIG_PROPERTY, "adb"); - mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager, mUsbPermissionManager); - - sendBootCompleteMessages(mUsbHandler); - assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); - assertEquals(mMockGlobalSettings.get(Settings.Global.ADB_ENABLED).intValue(), 1); - assertTrue(mUsbHandler.isAdbEnabled()); - } - - @SmallTest - @Test public void userSwitchedDisablesMtp() { mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_SET_CURRENT_FUNCTIONS, UsbManager.FUNCTION_MTP)); |